Counting Bits
Problem Description
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 bits in the binary representation of i.
Examples
Example 1:Input: n = 2 Output: [0,1,1] Explanation: 0 —> 0 (0 set bits) 1 —> 1 (1 set bit) 2 —> 10 (1 set bit)
Input: n = 5 Output: [0,1,1,2,1,2] Explanation: 0 —> 000 (0) 1 —> 001 (1) 2 —> 010 (1) 3 —> 011 (2) 4 —> 100 (1) 5 —> 101 (2)
Constraints
0 ≤ n ≤ 10⁵
Extending Bit Counts from Shipped Subproblems
The naive solution runs the “Number of 1 Bits” algorithm on each number from 0 to n. Counting bits for each number independently takes O(n * log n) time.
To optimize to O(n) time, we can use dynamic programming by reusing the bit counts of smaller numbers we have already computed.
Notice the relationship between a number i and i / 2 (which is i >> 1 shifted right by one bit):
- Shifting
iright by one bit removes the least significant bit. - The number of set bits in
iis equal to the number of set bits ini >> 1, plus1ifiis odd (meaning its last bit was1). - This yields the O(1) state transition:
dp[i] = dp[i >> 1] + (i & 1).
By iterating from 1 to n, we build the entire output array in linear time.
Solution 1: Dynamic Programming (LSR Offset)
Populate the output array using the recurrence relation: ans[i] = ans[i >> 1] + (i & 1).
class Solution {
public int[] countBits(int n) {
int[] ans = new int[n + 1];
ans[0] = 0; // base case
for (int i = 1; i <= n; i++) {
// state transition: shift right + check least significant bit
ans[i] = ans[i >> 1] + (i & 1);
}
return ans;
}
}class Solution:
def countBits(self, n: int) -> list[int]:
ans = [0] * (n + 1)
ans[0] = 0 # base case
for i in range(1, n + 1):
# state transition: shift right + check least significant bit
ans[i] = ans[i >> 1] + (i & 1)
return ans#include <vector>
class Solution {
public:
std::vector<int> countBits(int n) {
std::vector<int> ans(n + 1, 0);
ans[0] = 0; // base case
for (int i = 1; i <= n; i++) {
// state transition: shift right + check least significant bit
ans[i] = ans[i >> 1] + (i & 1);
}
return ans;
}
};Complexity Analysis:
- Time Complexity: O(n). We populate each array slot in constant time.
- Space Complexity: O(1) auxiliary space (the output array of size n + 1 does not count towards complexity by convention).
Where it breaks: Nothing breaks. It handles all numbers within the range correctly and uses the minimum possible memory.
Common Mistakes
- Forgetting parentheses around bitwise operator checks. Bitwise operator precedence is lower than addition in C++ and Java. Writing
ans[i >> 1] + i & 1evaluates as(ans[i >> 1] + i) & 1, which is incorrect. Use parentheses:(i & 1). - Calculating values out of order. Because the state transition requires
ans[i >> 1], you must iterate forward from 1 tonso that the subproblem value is already computed when needed. - Using a full division
i / 2instead of bitwise right shifti >> 1. While they are mathematically identical, using bitwise shift operators is faster and cleaner for bit manipulation problems.
Frequently Asked Questions
Can we solve this using the most significant bit offset?
Yes. You can use the most significant bit (MSB) offset: ans[i] = 1 + ans[i - offset], where offset is the largest power of 2 less than or equal to i (which resets when i reaches a new power of 2, like 2, 4, 8, 16). This is also O(n) but requires tracking the offset value.
What is the benefit of the right-shift DP method over binary representation string counts?
Using bin(i).count('1') in Python translates the number to a string and scans it, which is O(log n) per number, making the total run time O(n log n). The DP method runs in pure O(n) without string conversions.
What does this problem test in interviews? It tests your ability to apply dynamic programming state-sharing principles to bit manipulation operations.