A clear explanation of converting a range of IPv4 addresses into the shortest list of CIDR blocks using greedy bit manipulation.
Problem Restatement
We are given a starting IPv4 address ip and an integer n.
We need to cover exactly n consecutive IP addresses starting from ip.
The answer should be a list of CIDR blocks with the smallest possible length.
A CIDR block looks like this:
"123.45.67.89/20"The number after / is the prefix length. A larger prefix length means a smaller block. For example, /32 covers exactly one IP address, while /29 covers 8 addresses.
The task is to split the range into the fewest valid CIDR blocks.
Input and Output
| Item | Meaning |
|---|---|
| Input | A valid IPv4 address ip and an integer n |
| Output | A list of CIDR block strings |
| Coverage | The blocks must cover exactly n consecutive addresses |
| Requirement | Use the smallest possible number of CIDR blocks |
Example function shape:
def ipToCIDR(ip: str, n: int) -> list[str]:
...Examples
Input:
ip = "255.0.0.7"
n = 10Output:
["255.0.0.7/32", "255.0.0.8/29", "255.0.0.16/32"]The range starts at:
255.0.0.7We need to cover 10 addresses.
The first address, 255.0.0.7, cannot form a larger aligned CIDR block because it ends in binary ...0111. It is only aligned for a block of size 1, so we emit:
255.0.0.7/32Now we move to 255.0.0.8.
The address 255.0.0.8 ends in binary ...1000, so it is aligned for a block of size 8.
We still need to cover 9 addresses, so we can take all 8:
255.0.0.8/29Now one address remains: 255.0.0.16.
So we emit:
255.0.0.16/32First Thought: Generate Every IP
One simple idea is to generate all n IP addresses one by one.
For example, for 255.0.0.7 and n = 10, we could generate:
255.0.0.7
255.0.0.8
255.0.0.9
...
255.0.0.16This covers the range, but it does not produce CIDR blocks.
The output would be too long because each single address would become a /32 block.
That is valid but not shortest.
Key Insight
A CIDR block always covers a power-of-two number of addresses.
| CIDR Prefix | Block Size |
|---|---|
/32 | 1 |
/31 | 2 |
/30 | 4 |
/29 | 8 |
/28 | 16 |
So the problem becomes:
At the current IP address, choose the largest power-of-two block that:
- starts exactly at the current address
- does not pass beyond the remaining
naddresses
The first rule is an alignment rule.
For example, an address ending in binary ...1000 is divisible by 8, so it can start a block of size 8.
An address ending in binary ...0111 is not divisible by 2, 4, or 8, so it can only start a block of size 1.
This is why bit manipulation is useful.
Convert IP to Integer
IPv4 addresses are 32-bit unsigned integers.
For example:
255.0.0.7can be interpreted as four 8-bit groups:
255 0 0 7To convert it into an integer:
value = 255 << 24
value += 0 << 16
value += 0 << 8
value += 7More generally:
def ip_to_int(ip: str) -> int:
value = 0
for part in ip.split("."):
value = value * 256 + int(part)
return valueThis gives us a number that can be incremented directly to move to the next IP address.
Convert Integer Back to IP
To convert a 32-bit integer back to IPv4 format, extract each 8-bit group.
def int_to_ip(x: int) -> str:
return ".".join(str((x >> shift) & 255) for shift in (24, 16, 8, 0))The mask 255 keeps only the lowest 8 bits after shifting.
Finding the Largest Valid Block
At a current integer address x, the expression:
x & -xreturns the largest power-of-two block size aligned at x.
For example:
| Binary Ending | x & -x | Meaning |
|---|---|---|
...0111 | 1 | Only block size 1 is aligned |
...1000 | 8 | Block sizes up to 8 are aligned |
...0000 | 16 or more | Larger blocks may be aligned |
But this aligned block may be larger than the number of addresses remaining.
So we shrink it until it fits:
block = x & -x
while block > n:
block //= 2Then block is the largest block we can safely use now.
The CIDR prefix length is:
prefix = 32 - log2(block)In Python, since block is a power of two:
prefix = 32 - (block.bit_length() - 1)Examples:
| Block Size | Prefix |
|---|---|
1 | 32 |
2 | 31 |
4 | 30 |
8 | 29 |
16 | 28 |
Algorithm
- Convert the starting IP address into a 32-bit integer.
- Create an empty answer list.
- While
n > 0:- Find the largest aligned block using
x & -x. - Shrink the block while it is larger than
n. - Convert the current integer address back to IP format.
- Append
ip/prefixto the answer. - Move
xforward byblock. - Decrease
nbyblock.
- Find the largest aligned block using
- Return the answer.
Correctness
At every step, the algorithm emits a CIDR block starting at the current first uncovered IP address.
The value x & -x gives the largest power-of-two block that is aligned at x. Therefore, any larger block would start before x or include addresses before the current uncovered range.
If that aligned block is larger than the number of remaining addresses, the algorithm repeatedly halves it until it fits inside the remaining range. Since CIDR block sizes are powers of two, the resulting block is still a valid CIDR block.
So each emitted block is valid and covers only uncovered addresses inside the target range.
The algorithm then moves x forward by exactly the number of addresses covered by that block and subtracts the same amount from n. Therefore, no address is skipped and no address is covered twice.
Because each step chooses the largest valid CIDR block starting at the current address, no shorter block list can replace that first choice. After choosing it, the same argument applies to the remaining suffix of the range. Thus the greedy method produces the smallest possible list.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(k) | k is the number of CIDR blocks returned |
| Space | O(k) | The answer list stores k blocks |
The conversion between IP string and integer is constant time because an IPv4 address always has four parts.
Implementation
class Solution:
def ipToCIDR(self, ip: str, n: int) -> list[str]:
def ip_to_int(ip: str) -> int:
value = 0
for part in ip.split("."):
value = value * 256 + int(part)
return value
def int_to_ip(x: int) -> str:
return ".".join(str((x >> shift) & 255) for shift in (24, 16, 8, 0))
x = ip_to_int(ip)
ans = []
while n > 0:
block = x & -x
while block > n:
block //= 2
prefix = 32 - (block.bit_length() - 1)
ans.append(f"{int_to_ip(x)}/{prefix}")
x += block
n -= block
return ansCode Explanation
The helper function ip_to_int converts an IPv4 string into a 32-bit integer.
value = value * 256 + int(part)Each IPv4 group has 8 bits, which means each group is base 256.
So "255.0.0.7" becomes:
255 * 256^3 + 0 * 256^2 + 0 * 256 + 7The helper function int_to_ip reverses the process.
(x >> shift) & 255This extracts one 8-bit group from the integer.
The main loop continues until all n addresses are covered.
block = x & -xThis finds the largest aligned block size at the current address.
Then:
while block > n:
block //= 2shrinks the block so we do not cover too many addresses.
The prefix is computed from the block size:
prefix = 32 - (block.bit_length() - 1)For example, if block = 8, then block.bit_length() - 1 = 3, so the prefix is:
32 - 3 = 29That gives /29, which covers 8 addresses.
Finally, we append the CIDR block and move forward:
x += block
n -= blockTesting
def run_tests():
s = Solution()
assert s.ipToCIDR("255.0.0.7", 10) == [
"255.0.0.7/32",
"255.0.0.8/29",
"255.0.0.16/32",
]
assert s.ipToCIDR("0.0.0.0", 1) == [
"0.0.0.0/32",
]
assert s.ipToCIDR("0.0.0.0", 2) == [
"0.0.0.0/31",
]
assert s.ipToCIDR("0.0.0.1", 1) == [
"0.0.0.1/32",
]
assert s.ipToCIDR("0.0.0.8", 8) == [
"0.0.0.8/29",
]
assert s.ipToCIDR("0.0.0.7", 2) == [
"0.0.0.7/32",
"0.0.0.8/32",
]
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
"255.0.0.7", 10 | Matches the main example |
"0.0.0.0", 1 | Single address becomes /32 |
"0.0.0.0", 2 | Aligned block of size 2 becomes /31 |
"0.0.0.1", 1 | Unaligned single address |
"0.0.0.8", 8 | Perfectly aligned block of size 8 |
"0.0.0.7", 2 | Cannot use /31 because start address is unaligned |