Counting Bits
Counting Bits: Dynamic Programming with Bitwise Magic
Welcome back to codinginterview.net. If you have been following our curriculum, you are getting comfortable with hash maps, binary search, intervals, and string traversals. Now, it is time to tackle a classic bit manipulation problem: Counting Bits. This problem is a popular gauge of whether a candidate can move beyond brute-force bit counting to identify repeating subproblems using dynamic programming. By the end of this guide, building linear-time bit counting arrays using state transitions will be second nature to you.
1. Understanding the Problem
Given an integer n, return an array ans of length n + 1 such that for each i (0 <= i <= n), ans[i] is the number of 1's in the binary representation of i.
| Input (n) | Integer Range | Binary Representation | Output Array (ans) |
|---|---|---|---|
| 2 | 0, 1, 2 | 0, 1, 10 | [0, 1, 1] |
| 5 | 0, 1, 2, 3, 4, 5 | 0, 1, 10, 11, 100, 101 | [0, 1, 1, 2, 1, 2] |
For example, when n = 5, the binary form of 3 is 11 (which has two 1-bits), so ans[3] = 2. The binary form of 4 is 100 (which has one 1-bit), so ans[4] = 1.
The Core Challenge:
Calculating set bits individually for each number takes extra work per integer. The real challenge is achieving an optimal solution that computes all results in a single pass in O(N) time without relying on built-in popcount functions.
2. The Naive Approach: Independent Popcount for Each Integer
The most intuitive, brute-force way to solve this problem is to iterate through every number from 0 to n and manually count its set bits using bitwise operations (like Kernighan's algorithm i & (i - 1)) or string conversions.
Trade-off Analysis
While this logic works fine for small inputs, it re-evaluates overlapping bit patterns repeatedly:
- Time Complexity: O(N log N) — For each of the N numbers, counting set bits takes O(log N) time (the number of bits in the integer).
- Space Complexity: O(1) — Excluding the output array of size O(N), no additional memory is allocated.
3. The Optimal Approach: Dynamic Programming via Right Shift (O(N) Time, O(1) Space)
To pass a senior-level technical interview, we want to compute the answer for each integer in O(1) time per element, yielding a total time complexity of O(N).
We can achieve this by recognizing a fundamental relationship between an integer i and its right-shifted version i >> 1 (which is equivalent to i // 2):
- Right-shifting a binary number by one bit removes its least significant bit (LSB).
- Therefore, the number of set bits in
iis equal to the number of set bits ini >> 1(a state we have already computed and stored), plus1if the LSB ofiwas set (i.e., ifiis odd).
Mathematically, the transition state is simply: ans[i] = ans[i >> 1] + (i & 1).
This single observation completely unifies the logic, allowing us to compute every entry in O(1) constant time building upon previous calculations!
4. The Logic Step-by-Step
- Initialize an integer array
ansof sizen + 1filled with0s. - Base Case:
ans[0] = 0(since 0 has zero set bits in binary). - Iterate Through Numbers: Loop from
i = 1up ton:- Look up the previously calculated set-bit count for
i >> 1(which isans[i >> 1]). - Add
(i & 1)to check if the current numberiends with a1bit (odd number). - Store the result at
ans[i].
- Look up the previously calculated set-bit count for
- Return the populated
ansarray.
5. Complexity Analysis
- Time Complexity: O(N) — We compute the set-bit count for each number from
0tonexactly once using a simple O(1) arithmetic lookup. - Space Complexity: O(1) — No auxiliary data structures are used beyond the required
ansoutput array.
6. Code Implementations
Expand the sections below to see the optimal O(N) time dynamic programming implementations across different languages.
View Python Solution
class Solution:
def countBits(self, n: int) -> list[int]:
ans = [0] * (n + 1)
for i in range(1, n + 1):
# ans[i] = set bits of (i // 2) + (1 if i is odd else 0)
ans[i] = ans[i >> 1] + (i & 1)
return ans
View Java Solution
class Solution {
public int[] countBits(int n) {
int[] ans = new int[n + 1];
for (int i = 1; i <= n; i++) {
// ans[i] = set bits of (i / 2) + (1 if i is odd else 0)
ans[i] = ans[i >> 1] + (i & 1);
}
return ans;
}
}
View C++ Solution
#include <vector>
class Solution {
public:
std::vector<int> countBits(int n) {
std::vector<int> ans(n + 1, 0);
for (int i = 1; i <= n; ++i) {
// ans[i] = set bits of (i / 2) + (1 if i is odd else 0)
ans[i] = ans[i >> 1] + (i & 1);
}
return ans;
}
};
7. Conclusion: You Are Ready
Congratulations, you have just mastered the "Counting Bits" dynamic programming pattern! The key takeaway from this problem is discovering the bitwise transition rule: a number's set-bit count depends directly on a smaller subproblem (i >> 1) plus its own least significant bit (i & 1). By leveraging this recurrence relation, you turned an O(N log N) repeated bit counting problem into an elegant O(N) linear-time dynamic programming solution. Keep this state-reuse technique in your mental toolkit, as it is a crucial bridge between low-level bit manipulation and top-tier DP interview problems!