TL;DR
DP that reuses a smaller already-counted number β O(n) time, O(1) extra space beyond the output array.
Approach 1 β Brute force: count every number
The naive intuition: for each i from 0 to n, count its set bits independently.
class Solution:
def countBits(self, n: int) -> list[int]:
return [bin(i).count("1") for i in range(n + 1)]
Complexity: O(n log n) time β each of the n + 1 numbers costs O(log i) to scan its ~log n bits β O(1) extra space.
Why we can do better: the constraints donβt forbid this (it passes), but it throws away structure. Each count reruns a bit scan even though a smaller number with almost the same bits was counted moments ago.
Approach 2 β DP by dropping the lowest bit (i >> 1)
The insight: shifting i right by one bit deletes its lowest bit and leaves a strictly smaller number, i >> 1, whose count is already stored. The bit you deleted is i & 1. So the number of set bits in i is the count of i >> 1 plus that one deleted bit β a table lookup plus an add.
class Solution:
def countBits(self, n: int) -> list[int]:
ans = [0] * (n + 1)
for i in range(1, n + 1):
ans[i] = ans[i >> 1] + (i & 1)
return ans
Walkthrough for n = 5:
| i | i >> 1 | ans[i >> 1] | i & 1 | ans[i] |
|---|
| 1 | 0 | 0 | 1 | 1 |
| 2 | 1 | 1 | 0 | 1 |
| 3 | 1 | 1 | 1 | 2 |
| 4 | 2 | 1 | 0 | 1 |
| 5 | 2 | 1 | 1 | 2 |
Result [0, 1, 1, 2, 1, 2]. β Because i >> 1 < i, its entry is always filled before we need it.
Complexity: O(n) time, O(1) extra space (beyond the required output).
Approach 3 β DP by clearing the lowest set bit (i & (i - 1))
The insight: i & (i - 1) removes the lowest set bit of i. That leaves a smaller number with exactly one fewer 1, so ans[i] is simply that numberβs count plus one. Where Approach 2 removed a positional bit (which might be 0), this removes an actual set bit.
Why does i & (i - 1) clear the lowest set bit? Subtracting 1 turns the lowest set bit into 0 and flips every zero below it to 1; ANDing with the original keeps all higher bits, kills that lowest set bit, and zeroes the flipped-up low bits.
class Solution:
def countBits(self, n: int) -> list[int]:
ans = [0] * (n + 1)
for i in range(1, n + 1):
ans[i] = ans[i & (i - 1)] + 1
return ans
Walkthrough for n = 5:
| i | binary | i & (i - 1) | ans[β¦] + 1 | ans[i] |
|---|
| 1 | 001 | 0 | 0 + 1 | 1 |
| 2 | 010 | 0 | 0 + 1 | 1 |
| 3 | 011 | 2 (010) | 1 + 1 | 2 |
| 4 | 100 | 0 | 0 + 1 | 1 |
| 5 | 101 | 4 (100) | 1 + 1 | 2 |
Result [0, 1, 1, 2, 1, 2]. β
Complexity: O(n) time, O(1) extra space.
Common pitfalls
- Array size off by one: the answer has
n + 1 entries (indices 0 through n), not n.
- Operator precedence: write
ans[i >> 1] + (i & 1) β & binds looser than +, so ans[i >> 1] + i & 1 would parse wrong. Keep the parentheses.
- Starting the loop at 0:
ans[0] is already 0; starting i at 1 avoids computing -1 in i & (i - 1).
- Reaching for a built-in:
bin(i).count("1") is fine for correctness but is the O(n log n) approach in disguise β not the intended O(n) DP.
Pattern takeaway
When a quantity over 0..n has a clean recurrence to a smaller value, build a table bottom-up instead of recomputing. For bit counts, the two go-to relations are βdrop the lowest bitβ (i >> 1, add i & 1) and βclear the lowest set bitβ (i & (i - 1), add 1) β both turn an O(n log n) scan into an O(n) DP.