Facebook Pixel

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:
    • 00 (valid)
    • 11 (valid)
    • 210 (valid)
    • 311 (invalid - has consecutive ones)
    • 4100 (valid)
    • 5101 (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 1s.

The dfs function has three parameters:

  • i: Current bit position (starting from the most significant bit)
  • pre: The previous bit value (0 or 1)
  • limit: Whether we're constrained by the upper bound n

The algorithm works as follows:

  1. Start from the most significant bit of n and work downward
  2. At each position, try placing either 0 or 1
  3. If the previous bit was 1 and we want to place 1, skip this case (would create "11")
  4. The limit flag ensures we don't exceed n:
    • If limit is True, we can only place bits up to the corresponding bit in n
    • If limit is False, we can freely choose 0 or 1
  5. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. What digit we placed previously (to avoid consecutive 1s)
  2. 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 either 0 or 1
  • If the previous position has 1, the current position must be 0 (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 be 0 or 1 (following n's pattern)
  • But if we've built 00__, we've already gone below n, so we can freely choose any valid combination for remaining bits

This naturally leads to a recursive solution where we:

  1. Process one bit at a time from left to right
  2. Make decisions based on the previous bit and whether we're still at the upper bound
  3. 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 or 1)
  • limit: Boolean flag indicating if we're still bounded by n

Implementation Details

  1. 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 is 3, so we start at index 2.

  2. Base case:

    if i < 0:
        return 1

    When we've processed all bits (index becomes negative), we've successfully formed one valid number.

  3. Determining the upper bound for current bit:

    up = (n >> i & 1) if limit else 1
    • If limit is True: We can only place bits up to what n has at position i
    • If limit is False: We can place either 0 or 1
  4. 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 from 0 to up
    • Skip if both pre and j are 1 (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
  5. 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 or 1 (since n has 1 at position 2)
    • If we place 1: Continue with dfs(1, 1, True)
    • If we place 0: Continue with dfs(1, 0, False) (no longer at limit)
  • At position 1: Depends on previous choices
    • If previous was 1 and limit is True: Can only place 0 (since n has 0 here)
    • If previous was 0 and no limit: Can place 0 or 1
  • 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 Evaluator

Example 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 (101100)
            ├─ 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 from n.bit_length() - 1 down to -1, giving us O(log n) possible values
  • Parameter pre can only be 0 or 1, giving us 2 possible values
  • Parameter limit can only be True or False, giving us 2 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 is O(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:

  1. Test with small examples: Trace through n = 5 manually to verify bit positions
  2. Add debugging output: Print (i, pre, limit) states to understand the recursion flow
  3. Verify bit operations: Test bit extraction separately with known values
  4. Check memoization effectiveness: Count cache hits vs. misses to ensure it's working
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does quick sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More