600. Non-negative Integers without Consecutive Ones
Problem Description
Given a positive integer n
, you need to count how many integers in the range [0, n]
have binary representations that do not contain consecutive ones (i.e., no "11" pattern).
For example:
- If
n = 5
, the binary representations are:0
→0
(valid)1
→1
(valid)2
→10
(valid)3
→11
(invalid - has consecutive ones)4
→100
(valid)5
→101
(valid)
The answer would be 5
since only the number 3
has consecutive ones in its binary representation.
The solution uses Digit DP (Dynamic Programming) technique. The key idea is to build valid numbers digit by digit from the most significant bit, ensuring we never place two consecutive 1
s.
The dfs
function has three parameters:
i
: Current bit position (starting from the most significant bit)pre
: The previous bit value (0
or1
)limit
: Whether we're constrained by the upper boundn
The algorithm works as follows:
- Start from the most significant bit of
n
and work downward - At each position, try placing either
0
or1
- If the previous bit was
1
and we want to place1
, skip this case (would create "11") - The
limit
flag ensures we don't exceedn
:- If
limit
isTrue
, we can only place bits up to the corresponding bit inn
- If
limit
isFalse
, we can freely choose0
or1
- If
- Use memoization (
@cache
) to avoid recalculating the same states
The time complexity is O(log n)
since we process each bit position once, and space complexity is also O(log n)
for the recursion stack and memoization.
Intuition
When we need to count numbers with specific digit patterns in a range, a brute force approach would be to check every number from 0
to n
. However, this becomes inefficient for large values of n
.
The key insight is that we can build valid numbers digit by digit from left to right (most significant to least significant bit). At each step, we only need to track:
- What digit we placed previously (to avoid consecutive
1
s) - Whether we're still bounded by
n
(to ensure we don't exceed it)
Think of it like filling in blanks: _ _ _ _
for a 4-bit number. When filling each position:
- If the previous position has
0
, the current position can be either0
or1
- If the previous position has
1
, the current position must be0
(to avoid "11")
The "limit" concept is crucial here. Consider n = 1010
(binary):
- If we've built
10__
so far and stayed within the limit, the next bit can only be0
or1
(followingn
's pattern) - But if we've built
00__
, we've already gone belown
, so we can freely choose any valid combination for remaining bits
This naturally leads to a recursive solution where we:
- Process one bit at a time from left to right
- Make decisions based on the previous bit and whether we're still at the upper bound
- Count all valid combinations
The beauty of this approach is that many subproblems repeat. For instance, once we know how many valid numbers can be formed with the last k
bits (without limit), we can reuse this result. This is why memoization dramatically improves performance - we solve each unique state only once.
The approach transforms an O(n)
enumeration problem into an O(log n)
digit-building problem, making it efficient even for very large values of n
.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements Digit DP using memoized recursion. Let's walk through the implementation step by step:
Core Function Design
The main recursive function dfs(i, pre, limit)
has three parameters:
i
: Current bit position index (counting from right, starting at the most significant bit)pre
: The previous bit value (0
or1
)limit
: Boolean flag indicating if we're still bounded byn
Implementation Details
-
Getting the bit length:
n.bit_length() - 1
This gives us the index of the most significant bit. For example, if
n = 5
(binary:101
), the bit length is3
, so we start at index2
. -
Base case:
if i < 0: return 1
When we've processed all bits (index becomes negative), we've successfully formed one valid number.
-
Determining the upper bound for current bit:
up = (n >> i & 1) if limit else 1
- If
limit
isTrue
: We can only place bits up to whatn
has at positioni
- If
limit
isFalse
: We can place either0
or1
- If
-
Iterating through possible digits:
for j in range(up + 1): if pre and j: continue ans += dfs(i - 1, j, limit and j == up)
- We try each possible bit value
j
from0
toup
- Skip if both
pre
andj
are1
(would create consecutive ones) - Recurse with:
- Next bit position:
i - 1
- Current bit becomes the new previous:
j
- Update limit: remains
True
only if we're still at limit AND placed the maximum allowed bit
- Next bit position:
- We try each possible bit value
-
Memoization with
@cache
: The decorator automatically caches results for each unique(i, pre, limit)
combination, preventing redundant calculations.
Example Walkthrough
For n = 5
(binary: 101
):
- Start with
dfs(2, 0, True)
(bit position 2, no previous bit, limited by n) - At position 2: Can place
0
or1
(sincen
has1
at position 2)- If we place
1
: Continue withdfs(1, 1, True)
- If we place
0
: Continue withdfs(1, 0, False)
(no longer at limit)
- If we place
- At position 1: Depends on previous choices
- If previous was
1
and limit isTrue
: Can only place0
(sincen
has0
here) - If previous was
0
and no limit: Can place0
or1
- If previous was
- Continue until all positions are filled
The algorithm efficiently counts all valid numbers by exploring the decision tree of bit placements while pruning invalid branches (consecutive ones) and leveraging memoization to avoid recomputation.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with n = 5
(binary: 101
).
Initial Setup:
n = 5
→ binary:101
(3 bits)- Start position:
i = 2
(most significant bit index) - Initial call:
dfs(2, 0, True)
Step-by-step execution:
dfs(2, 0, True) // Position 2, no previous bit, limited by n ├─ Bit at position 2 in n = 1, so up = 1 ├─ Try j = 0: │ └─ dfs(1, 0, False) // Placed 0, no longer at limit (0 < 1) │ ├─ No limit, so up = 1 │ ├─ Try j = 0: │ │ └─ dfs(0, 0, False) │ │ ├─ Try j = 0: dfs(-1, 0, False) → returns 1 (number: 000 = 0) │ │ └─ Try j = 1: dfs(-1, 1, False) → returns 1 (number: 001 = 1) │ │ Total: 2 │ └─ Try j = 1: │ └─ dfs(0, 1, False) │ └─ Try j = 0: dfs(-1, 0, False) → returns 1 (number: 010 = 2) │ └─ Try j = 1: SKIP (consecutive 1s) │ Total: 1 │ Total from dfs(1, 0, False): 3 │ └─ Try j = 1: └─ dfs(1, 1, True) // Placed 1, still at limit ├─ Bit at position 1 in n = 0, so up = 0 └─ Try j = 0: └─ dfs(0, 0, False) // Placed 0, no longer at limit (101 → 100) ├─ Try j = 0: dfs(-1, 0, False) → returns 1 (number: 100 = 4) └─ Try j = 1: dfs(-1, 1, False) → returns 1 (number: 101 = 5) Total: 2 └─ Try j = 1: SKIP (would create 11) Total from dfs(1, 1, True): 2 Final result: 3 + 2 = 5
Valid numbers found:
000
(0) ✓001
(1) ✓010
(2) ✓100
(4) ✓101
(5) ✓
Invalid number skipped:
011
(3) ✗ - contains "11"
The algorithm correctly returns 5, counting all integers from 0 to 5 that don't contain consecutive 1s in their binary representation.
Solution Implementation
1class Solution:
2 def findIntegers(self, n: int) -> int:
3 """
4 Count the number of integers in range [0, n] that don't have
5 consecutive 1s in their binary representation.
6
7 Args:
8 n: Upper bound of the range (inclusive)
9
10 Returns:
11 Count of valid integers without consecutive 1s in binary
12 """
13 from functools import cache
14
15 @cache
16 def count_valid_numbers(bit_position: int,
17 previous_bit: int,
18 is_limit: bool) -> int:
19 """
20 Recursive function to count valid numbers using digit DP.
21
22 Args:
23 bit_position: Current bit position being processed (from MSB to LSB)
24 previous_bit: The bit value placed at the previous (higher) position
25 is_limit: Whether we're still bounded by the original number n
26
27 Returns:
28 Count of valid numbers from this state
29 """
30 # Base case: processed all bits successfully
31 if bit_position < 0:
32 return 1
33
34 # Determine the upper bound for current bit
35 # If we're at limit, we can only go up to the corresponding bit in n
36 # Otherwise, we can use 0 or 1
37 upper_bound = (n >> bit_position & 1) if is_limit else 1
38
39 # Count valid numbers by trying each possible bit value
40 total_count = 0
41 for current_bit in range(upper_bound + 1):
42 # Skip if we would have consecutive 1s (previous was 1 and current is 1)
43 if previous_bit == 1 and current_bit == 1:
44 continue
45
46 # Recursively count valid numbers with:
47 # - Next bit position (moving right)
48 # - Current bit becomes the previous for next recursion
49 # - Update limit flag (remains true only if we're at limit AND chose maximum bit)
50 total_count += count_valid_numbers(
51 bit_position - 1,
52 current_bit,
53 is_limit and (current_bit == upper_bound)
54 )
55
56 return total_count
57
58 # Start from the most significant bit
59 # Initial previous bit is 0 (no bit before MSB)
60 # Initially we're bounded by n (is_limit = True)
61 return count_valid_numbers(n.bit_length() - 1, 0, True)
62
1class Solution {
2 private int n;
3 private Integer[][] memo; // Memoization table: memo[bitPosition][previousBit]
4
5 /**
6 * Finds the count of integers in the range [0, n] that don't have consecutive 1s in binary
7 * @param n The upper bound of the range
8 * @return Count of valid integers without consecutive 1s
9 */
10 public int findIntegers(int n) {
11 this.n = n;
12
13 // Calculate the number of bits in n (highest bit position)
14 int bitCount = Integer.SIZE - Integer.numberOfLeadingZeros(n);
15
16 // Initialize memoization table
17 memo = new Integer[bitCount][2];
18
19 // Start DFS from the most significant bit
20 return dfs(bitCount - 1, 0, true);
21 }
22
23 /**
24 * Recursive function to count valid numbers using digit DP
25 * @param bitPosition Current bit position being processed (from MSB to LSB)
26 * @param previousBit The bit value placed at the previous (higher) position
27 * @param isLimit Whether we're still bounded by the original number n
28 * @return Count of valid integers from current state
29 */
30 private int dfs(int bitPosition, int previousBit, boolean isLimit) {
31 // Base case: processed all bits successfully
32 if (bitPosition < 0) {
33 return 1;
34 }
35
36 // Check memoization for non-limit states
37 if (!isLimit && memo[bitPosition][previousBit] != null) {
38 return memo[bitPosition][previousBit];
39 }
40
41 // Determine the upper bound for current bit
42 // If we're at limit, we can only go up to the corresponding bit in n
43 // Otherwise, we can use 0 or 1
44 int upperBound = isLimit ? (n >> bitPosition & 1) : 1;
45
46 int count = 0;
47
48 // Try placing each valid bit (0 or 1) at current position
49 for (int currentBit = 0; currentBit <= upperBound; currentBit++) {
50 // Skip if placing 1 would create consecutive 1s
51 if (currentBit == 1 && previousBit == 1) {
52 continue;
53 }
54
55 // Recursively count valid numbers with current bit placed
56 // Update limit flag: we remain limited only if we're currently limited AND placing the max bit
57 count += dfs(bitPosition - 1, currentBit, isLimit && currentBit == upperBound);
58 }
59
60 // Memoize result for non-limit states
61 if (!isLimit) {
62 memo[bitPosition][previousBit] = count;
63 }
64
65 return count;
66 }
67}
68
1class Solution {
2public:
3 int findIntegers(int n) {
4 // Calculate the number of bits needed to represent n
5 // __builtin_clz returns the number of leading zeros in a 32-bit integer
6 int numBits = 32 - __builtin_clz(n);
7
8 // DP memoization table
9 // dp[bitPosition][previousBit] = count of valid numbers
10 // -1 indicates uncomputed state
11 int dp[numBits][2];
12 memset(dp, -1, sizeof(dp));
13
14 // Recursive function with digit DP approach
15 // Parameters:
16 // - bitPos: current bit position (from MSB to LSB)
17 // - prevBit: the bit value at the previous (higher) position
18 // - isTight: whether we're still bounded by the limit n
19 auto countValidNumbers = [&](this auto&& countValidNumbers,
20 int bitPos,
21 int prevBit,
22 bool isTight) -> int {
23 // Base case: processed all bits successfully
24 if (bitPos < 0) {
25 return 1;
26 }
27
28 // Return memoized result if available (only when not tight)
29 if (!isTight && dp[bitPos][prevBit] != -1) {
30 return dp[bitPos][prevBit];
31 }
32
33 // Determine the upper bound for current bit
34 // If tight, we can only go up to the corresponding bit in n
35 // Otherwise, we can use 0 or 1
36 int upperBound = isTight ? (n >> bitPos & 1) : 1;
37
38 // Count valid numbers by trying each possible bit value
39 int count = 0;
40 for (int currentBit = 0; currentBit <= upperBound; ++currentBit) {
41 // Skip if we would have consecutive 1s
42 if (currentBit == 1 && prevBit == 1) {
43 continue;
44 }
45
46 // Recursively count for remaining bits
47 // New tight constraint: remains tight only if we chose the maximum allowed bit
48 count += countValidNumbers(bitPos - 1,
49 currentBit,
50 isTight && currentBit == upperBound);
51 }
52
53 // Memoize the result when not under tight constraint
54 // (tight results depend on n and can't be reused)
55 if (!isTight) {
56 dp[bitPos][prevBit] = count;
57 }
58
59 return count;
60 };
61
62 // Start from the most significant bit
63 // Initial previous bit is 0 (no bit before MSB)
64 // Initial state is tight (bounded by n)
65 return countValidNumbers(numBits - 1, 0, true);
66 }
67};
68
1/**
2 * Counts the number of integers from 0 to n that don't have consecutive 1s in binary representation
3 * @param n - The upper bound integer
4 * @returns The count of valid integers without consecutive 1s in binary
5 */
6function findIntegers(n: number): number {
7 // Get the number of bits needed to represent n
8 const bitLength: number = n.toString(2).length;
9
10 // Memoization table: dp[position][previousBit]
11 // Stores the count of valid numbers for a given position and previous bit
12 const memoTable: number[][] = Array.from(
13 { length: bitLength },
14 () => Array(2).fill(-1)
15 );
16
17 /**
18 * Depth-first search to count valid integers using digit DP technique
19 * @param position - Current bit position (from most significant to least)
20 * @param previousBit - The bit value at the previous (higher) position
21 * @param isLimit - Whether we're still bounded by the original number n
22 * @returns Count of valid integers from this state
23 */
24 const countValidIntegers = (
25 position: number,
26 previousBit: number,
27 isLimit: boolean
28 ): number => {
29 // Base case: processed all bits successfully
30 if (position < 0) {
31 return 1;
32 }
33
34 // Return memoized result if available and not limited by n
35 if (!isLimit && memoTable[position][previousBit] !== -1) {
36 return memoTable[position][previousBit];
37 }
38
39 // Determine the maximum bit value we can place at current position
40 const maxBitValue: number = isLimit ? (n >> position) & 1 : 1;
41
42 let totalCount: number = 0;
43
44 // Try placing 0 or 1 at current position
45 for (let currentBit = 0; currentBit <= maxBitValue; currentBit++) {
46 // Skip if we would have consecutive 1s
47 if (previousBit === 1 && currentBit === 1) {
48 continue;
49 }
50
51 // Recursively count valid numbers with current bit placed
52 totalCount += countValidIntegers(
53 position - 1,
54 currentBit,
55 isLimit && currentBit === maxBitValue
56 );
57 }
58
59 // Memoize result when not limited by n
60 if (!isLimit) {
61 memoTable[position][previousBit] = totalCount;
62 }
63
64 return totalCount;
65 };
66
67 // Start DFS from most significant bit with no previous bit and limited by n
68 return countValidIntegers(bitLength - 1, 0, true);
69}
70
Time and Space Complexity
Time Complexity: O(log n * 2 * 2)
= O(log n)
The time complexity is determined by the recursive DFS function with memoization. Let's analyze the state space:
- Parameter
i
ranges fromn.bit_length() - 1
down to-1
, giving usO(log n)
possible values - Parameter
pre
can only be0
or1
, giving us2
possible values - Parameter
limit
can only beTrue
orFalse
, giving us2
possible values
The total number of unique states is O(log n * 2 * 2)
= O(log n)
. Due to memoization with @cache
, each state is computed only once. Within each state, we iterate through at most 2 values (j from 0 to up, where up is at most 1), which is O(1)
work per state.
Therefore, the overall time complexity is O(log n)
.
Space Complexity: O(log n)
The space complexity consists of:
- Recursion stack depth: The maximum recursion depth is
n.bit_length()
which isO(log n)
- Memoization cache: The cache stores all unique states, which as analyzed above is
O(log n * 2 * 2)
=O(log n)
Therefore, the overall space complexity is O(log n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Bit Indexing
A common mistake is using the wrong bit indexing scheme, either starting from 0 instead of n.bit_length() - 1
, or incrementing instead of decrementing the index.
Wrong approach:
def dfs(i, pre, limit):
if i >= n.bit_length(): # Wrong termination
return 1
# ... rest of logic
dfs(i + 1, j, ...) # Wrong direction
Correct approach:
def dfs(i, pre, limit):
if i < 0: # Correct termination
return 1
# ... rest of logic
dfs(i - 1, j, ...) # Correct direction
2. Forgetting to Handle the Limit Flag Properly
The limit flag update is crucial but often misunderstood. A common error is to simply pass limit
unchanged or always set it to False
after the first non-maximum digit.
Wrong approach:
# Incorrectly maintaining the limit flag ans += dfs(i - 1, j, limit) # Wrong: doesn't update based on current choice # OR ans += dfs(i - 1, j, False) # Wrong: always removes limit after first bit
Correct approach:
# Limit remains True only if we were at limit AND chose the maximum allowed bit ans += dfs(i - 1, j, limit and j == up)
3. Not Using Memoization or Using It Incorrectly
Without memoization, the solution becomes exponentially slow. Also, using incorrect cache keys can lead to wrong results.
Wrong approach:
# No memoization - exponential time complexity
def dfs(i, pre, limit):
# ... recursive logic without caching
# Or wrong cache key
memo = {}
def dfs(i, pre, limit):
if (i, pre) in memo: # Missing 'limit' in cache key
return memo[(i, pre)]
Correct approach:
from functools import cache
@cache # Automatically handles all parameters as cache key
def dfs(i, pre, limit):
# ... recursive logic
4. Incorrect Consecutive Ones Check
Some implementations check for consecutive ones incorrectly by looking at the wrong conditions or positions.
Wrong approach:
# Wrong: checking if current and next will be 1 (we don't know next yet) if j == 1 and next_bit == 1: continue # Wrong: only checking when pre is 1, missing when j is 0 if pre == 1: continue
Correct approach:
# Skip only when BOTH previous and current are 1 if pre == 1 and j == 1: continue # Or more concisely: if pre and j: continue
5. Off-by-One Error in Bit Extraction
When extracting the bit at position i
from n
, using the wrong shift amount is a common mistake.
Wrong approach:
# Wrong: using bit_length() directly without -1 up = (n >> n.bit_length() & 1) if limit else 1 # Wrong: shifting in wrong direction up = (n << i & 1) if limit else 1
Correct approach:
# Correct: right shift by i positions and mask with 1 up = (n >> i & 1) if limit else 1
6. Edge Case: n = 0
While the code handles this correctly, it's worth noting that when n = 0
, n.bit_length()
returns 0, so we start with dfs(-1, 0, True)
which immediately returns 1, giving the correct answer.
Prevention Tips:
- Test with small examples: Trace through
n = 5
manually to verify bit positions - Add debugging output: Print
(i, pre, limit)
states to understand the recursion flow - Verify bit operations: Test bit extraction separately with known values
- Check memoization effectiveness: Count cache hits vs. misses to ensure it's working
How does quick sort divide the problem into subproblems?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!