Facebook Pixel

233. Number of Digit One

Problem Description

Given an integer n, you need to count how many times the digit 1 appears in all non-negative integers from 0 to n (inclusive).

For example:

  • If n = 13, you would count the digit 1 in: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13
  • The digit 1 appears in: 1 (once), 10 (once), 11 (twice), 12 (once), 13 (once)
  • Total count = 1 + 1 + 2 + 1 + 1 = 6

The problem asks you to return the total count of how many times the digit 1 appears across all integers in the range [0, n].

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

Intuition

When counting digit occurrences across a range of numbers, a brute force approach would iterate through every number and count the 1s, but this becomes inefficient for large values of n.

The key insight is that we can solve this problem digit by digit, considering the patterns of how 1s appear in each position. This naturally leads us to think about Digit Dynamic Programming (Digit DP), where we build numbers digit by digit and keep track of our count along the way.

Think of it like this: when we're constructing numbers from 0 to n, we start from the leftmost digit and work our way right. At each position, we have choices for what digit to place (0-9), but we need to be careful not to exceed n.

For example, if n = 234:

  • At the first position, we can choose digits 0, 1, or 2
  • If we chose 2, at the second position we can choose 0, 1, 2, or 3
  • If we chose 2 then 3, at the third position we can only choose 0, 1, 2, 3, or 4

The crucial observation is that once we place a digit that's smaller than the corresponding digit in n, all subsequent positions become "free" - we can choose any digit from 0 to 9 without worrying about exceeding n.

This is where the limit flag comes in - it tracks whether we're still bounded by n or if we've already gone below it and have freedom in our choices.

As we build these numbers digit by digit, we maintain a count (cnt) of how many 1s we've encountered so far. When we place a 1 at any position, we increment this count. By exploring all valid number constructions this way and accumulating the counts, we get our final answer.

The memoization (@cache) helps us avoid recalculating the same subproblems - if we've already computed how many 1s appear when starting from a certain digit position with a certain count and limit state, we can reuse that result.

Solution Approach

The solution uses Digit DP with memoized recursion. We convert the number n to a string s to easily access individual digits, then use a depth-first search function to explore all valid numbers.

Core Function Design:

The function dfs(i, cnt, limit) has three parameters:

  • i: Current digit position being processed (0 is the leftmost/highest digit)
  • cnt: Count of digit 1 seen so far in the current number being constructed
  • limit: Boolean flag indicating if we're still bounded by the upper limit n

Implementation Steps:

  1. Base Case: When i >= len(s), we've processed all digit positions and return the accumulated count cnt.

  2. Determine Upper Bound:

    • If limit is True, we can only place digits from 0 to s[i] (the i-th digit of n)
    • If limit is False, we can place any digit from 0 to 9
    up = int(s[i]) if limit else 9
  3. Try All Valid Digits: For each valid digit j from 0 to up:

    • Recursively call dfs for the next position
    • If j == 1, increment the count by 1
    • Update the limit flag: it remains True only if both current limit is True AND we chose the maximum possible digit (j == up)
    for j in range(up + 1):
        ans += dfs(i + 1, cnt + (j == 1), limit and j == up)
  4. Memoization: The @cache decorator automatically memoizes the results, preventing redundant calculations for the same (i, cnt, limit) state.

Example Walkthrough:

For n = 13:

  • Start with dfs(0, 0, True)
  • At position 0: can choose 0 or 1 (since first digit of 13 is 1)
    • If we choose 0: dfs(1, 0, False) - no longer limited
    • If we choose 1: dfs(1, 1, True) - still limited, count increases by 1
  • At position 1 (when still limited after choosing 1): can choose 0, 1, 2, or 3
    • Each choice leads to different recursive calls
  • The recursion aggregates all counts from valid number constructions

Time and Space Complexity:

  • Time: O(m² × D) where m is the number of digits in n and D = 10 (possible digit values)
  • Space: O(m²) for the memoization cache

The solution efficiently counts all occurrences of digit 1 by exploring the number space systematically while avoiding redundant computations through memoization.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through the solution with n = 23 to understand how the Digit DP approach works.

Setup:

  • Convert n = 23 to string: s = "23"
  • Start with dfs(0, 0, True) - position 0, count 0, limited by n

Step 1: First digit (tens place)

  • Current state: dfs(0, 0, True)
  • Since limit = True, we can choose digits 0, 1, or 2 (up to s[0] = '2')

Choice 1a: Place '0' at tens place

  • Call dfs(1, 0, False) - move to position 1, count stays 0, no longer limited
  • At position 1, since limit = False, we can choose any digit 0-9
  • This represents numbers 00-09 (effectively 0-9)
  • For digit '1' at ones place: dfs(2, 1, False) returns 1
  • Total from this branch: 1 (only the number 01)

Choice 1b: Place '1' at tens place

  • Call dfs(1, 1, False) - move to position 1, count becomes 1, no longer limited
  • At position 1, since limit = False, we can choose any digit 0-9
  • This represents numbers 10-19
  • Each number already has one '1' from tens place
  • For digit '1' at ones place (number 11): dfs(2, 2, False) returns 2
  • For other digits at ones place: dfs(2, 1, False) returns 1 each
  • Total from this branch: 1×9 + 2×1 = 11 (ten '1's from tens place, plus one more from 11)

Choice 1c: Place '2' at tens place

  • Call dfs(1, 0, True) - move to position 1, count stays 0, still limited
  • At position 1, since limit = True, we can only choose digits 0-3 (up to s[1] = '3')
  • This represents numbers 20-23
  • For digit '1' at ones place (number 21): dfs(2, 1, True/False) returns 1
  • For other digits: dfs(2, 0, True/False) returns 0
  • Total from this branch: 1 (only from number 21)

Step 2: Aggregation

  • Total count = 1 + 11 + 1 = 13

Verification: Numbers from 0 to 23 containing '1':

  • 1 (one '1')
  • 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 (ten numbers, with 11 having two '1's)
  • 21 (one '1')

Total: 1 + 10 + 1 + 1 = 13 ✓

The memoization ensures that identical subproblems (like counting from certain positions with the same count and limit values) are only computed once, making the algorithm efficient even for large values of n.

Solution Implementation

1class Solution:
2    def countDigitOne(self, n: int) -> int:
3        """
4        Count the total number of digit 1 appearing in all non-negative integers less than or equal to n.
5        Uses digit DP with memoization to solve the problem.
6        """
7        from functools import cache
8      
9        @cache
10        def count_ones_dp(position: int, ones_count: int, is_limited: bool) -> int:
11            """
12            Dynamic programming function to count digit 1s.
13          
14            Args:
15                position: Current position in the number string (0-indexed from left)
16                ones_count: Count of 1s accumulated so far in the current number being formed
17                is_limited: Whether the current number is still bounded by the original number n
18          
19            Returns:
20                Total count of digit 1s in all valid numbers from current state
21            """
22            # Base case: reached the end of the number
23            if position >= len(number_str):
24                return ones_count
25          
26            # Determine the upper bound for current digit
27            # If limited by original number, use the digit at current position
28            # Otherwise, we can use any digit from 0-9
29            upper_bound = int(number_str[position]) if is_limited else 9
30          
31            total_count = 0
32          
33            # Try all possible digits from 0 to upper_bound
34            for digit in range(upper_bound + 1):
35                # Recursively calculate for next position
36                # Update ones_count if current digit is 1
37                # Update is_limited flag: remains True only if we're still at the limit and chose the max digit
38                total_count += count_ones_dp(
39                    position + 1, 
40                    ones_count + (1 if digit == 1 else 0),
41                    is_limited and (digit == upper_bound)
42                )
43          
44            return total_count
45      
46        # Convert number to string for digit-by-digit processing
47        number_str = str(n)
48      
49        # Start DFS from position 0, with 0 ones counted, and limited by n
50        return count_ones_dp(0, 0, True)
51
1class Solution {
2    private int digitCount;                    // Total number of digits in n
3    private char[] digitsArray;                // Array of digits representing n
4    private Integer[][] memoization;           // DP memoization table [position][count of ones]
5
6    /**
7     * Counts the total number of digit '1' appearing in all numbers from 0 to n
8     * @param n The upper bound number
9     * @return Total count of digit '1' in range [0, n]
10     */
11    public int countDigitOne(int n) {
12        // Convert number to char array for digit-by-digit processing
13        digitsArray = String.valueOf(n).toCharArray();
14        digitCount = digitsArray.length;
15      
16        // Initialize memoization table
17        // First dimension: current position in the number
18        // Second dimension: count of ones accumulated so far
19        memoization = new Integer[digitCount][digitCount];
20      
21        // Start DFS from position 0, with 0 ones counted, and limit flag true
22        return dfs(0, 0, true);
23    }
24
25    /**
26     * Recursive function to count digit '1' using digit DP approach
27     * @param position Current position/index in the number being constructed
28     * @param onesCount Count of '1's accumulated in the current number being formed
29     * @param isLimit Whether we're still bounded by the original number n
30     * @return Total count of '1's in all valid numbers from current state
31     */
32    private int dfs(int position, int onesCount, boolean isLimit) {
33        // Base case: reached the end of digits
34        if (position >= digitCount) {
35            return onesCount;
36        }
37      
38        // Check memoization: only use cached result when not limited by upper bound
39        if (!isLimit && memoization[position][onesCount] != null) {
40            return memoization[position][onesCount];
41        }
42      
43        // Determine the upper bound for current digit
44        // If limited, can only go up to current digit of n; otherwise can use 0-9
45        int upperBound = isLimit ? digitsArray[position] - '0' : 9;
46      
47        int totalCount = 0;
48      
49        // Try all possible digits from 0 to upperBound at current position
50        for (int digit = 0; digit <= upperBound; digit++) {
51            // Recursively count for next position
52            // Update onesCount if current digit is 1
53            // Update isLimit: remains true only if we're at limit AND chose the upperBound digit
54            totalCount += dfs(
55                position + 1, 
56                onesCount + (digit == 1 ? 1 : 0), 
57                isLimit && (digit == upperBound)
58            );
59        }
60      
61        // Store result in memoization table only when not limited
62        // (limited states are specific to the input and shouldn't be cached)
63        if (!isLimit) {
64            memoization[position][onesCount] = totalCount;
65        }
66      
67        return totalCount;
68    }
69}
70
1class Solution {
2public:
3    int countDigitOne(int n) {
4        // Convert number to string for digit-by-digit processing
5        string numStr = to_string(n);
6        int numDigits = numStr.size();
7      
8        // Memoization table: dp[position][count of ones so far]
9        // Stores the result for a given position and count when not limited by upper bound
10        int dp[numDigits][numDigits];
11        memset(dp, -1, sizeof(dp));
12      
13        // Recursive function using digit DP technique
14        // Parameters:
15        // - pos: current position in the number (0-indexed from left)
16        // - onesCount: count of digit '1' seen so far
17        // - isLimit: whether we're still bounded by the original number n
18        auto digitDP = [&](this auto&& digitDP, int pos, int onesCount, bool isLimit) -> int {
19            // Base case: processed all digits
20            if (pos >= numDigits) {
21                return onesCount;
22            }
23          
24            // Use memoization only when not limited by upper bound
25            // (when limited, the state depends on previous digits chosen)
26            if (!isLimit && dp[pos][onesCount] != -1) {
27                return dp[pos][onesCount];
28            }
29          
30            // Determine the maximum digit we can place at current position
31            int maxDigit = isLimit ? (numStr[pos] - '0') : 9;
32          
33            // Try all possible digits from 0 to maxDigit
34            int totalCount = 0;
35            for (int digit = 0; digit <= maxDigit; ++digit) {
36                // Recursively process next position
37                // - Increment onesCount if current digit is 1
38                // - Update isLimit: remains true only if we chose the maximum possible digit
39                totalCount += digitDP(pos + 1, 
40                                     onesCount + (digit == 1), 
41                                     isLimit && (digit == maxDigit));
42            }
43          
44            // Store result in memoization table if not limited
45            if (!isLimit) {
46                dp[pos][onesCount] = totalCount;
47            }
48          
49            return totalCount;
50        };
51      
52        // Start from position 0, with 0 ones counted, and limited by n
53        return digitDP(0, 0, true);
54    }
55};
56
1/**
2 * Counts the total number of digit 1 appearing in all non-negative integers from 0 to n
3 * Uses digit dynamic programming approach
4 * @param n - The upper bound integer
5 * @returns The count of digit 1 in all numbers from 0 to n
6 */
7function countDigitOne(n: number): number {
8    // Convert number to string for digit-by-digit processing
9    const numberString: string = n.toString();
10    const digitCount: number = numberString.length;
11  
12    // Memoization table: memo[position][count of ones so far]
13    // -1 indicates uncomputed state
14    const memo: number[][] = Array.from(
15        { length: digitCount }, 
16        () => Array(digitCount).fill(-1)
17    );
18  
19    /**
20     * Depth-first search to count digit 1 occurrences
21     * @param position - Current digit position being processed (0-indexed from left)
22     * @param onesCount - Count of digit 1 encountered so far
23     * @param isLimit - Whether we're still bounded by the original number's digits
24     * @returns Total count of digit 1 for all valid numbers from current state
25     */
26    const dfs = (position: number, onesCount: number, isLimit: boolean): number => {
27        // Base case: reached the end of all digits
28        if (position >= digitCount) {
29            return onesCount;
30        }
31      
32        // Check memoization table if not constrained by limit
33        if (!isLimit && memo[position][onesCount] !== -1) {
34            return memo[position][onesCount];
35        }
36      
37        // Determine the maximum digit we can place at current position
38        const maxDigit: number = isLimit ? parseInt(numberString[position]) : 9;
39      
40        // Try all possible digits from 0 to maxDigit
41        let totalCount: number = 0;
42        for (let digit = 0; digit <= maxDigit; digit++) {
43            // Recursively process next position
44            // Update onesCount if current digit is 1
45            // Update isLimit: remains true only if we're at limit AND chose maxDigit
46            totalCount += dfs(
47                position + 1, 
48                onesCount + (digit === 1 ? 1 : 0), 
49                isLimit && digit === maxDigit
50            );
51        }
52      
53        // Store result in memoization table if not limited
54        if (!isLimit) {
55            memo[position][onesCount] = totalCount;
56        }
57      
58        return totalCount;
59    };
60  
61    // Start DFS from position 0, with 0 ones counted, and limit flag true
62    return dfs(0, 0, true);
63}
64

Time and Space Complexity

Time Complexity: O(log(n) * 10 * log(n)) which simplifies to O(log²(n))

The recursive function explores a tree-like structure where:

  • The depth of recursion is len(s), which equals O(log(n)) since the string representation of n has log₁₀(n) digits
  • At each recursive level i, we iterate through at most 10 possible digits (0-9)
  • Each state (i, cnt, limit) is computed only once due to memoization with @cache
  • The total number of unique states is O(log(n) * log(n) * 2) because:
    • i ranges from 0 to len(s) - 1: O(log(n)) possibilities
    • cnt represents the count of digit 1s encountered so far, which can be at most log(n)
    • limit is a boolean with 2 possible values
  • Each state performs O(10) work in the loop

Therefore, the overall time complexity is O(log(n) * log(n) * 2 * 10) = O(log²(n))

Space Complexity: O(log²(n))

The space is consumed by:

  • The recursion call stack depth: O(log(n))
  • The memoization cache storing all unique states: O(log(n) * log(n) * 2) = O(log²(n))
  • The string s storing the digits: O(log(n))

The dominant factor is the cache storage, giving us O(log²(n)) space complexity.

Common Pitfalls

1. Incorrect Limit Flag Update

Pitfall: A common mistake is incorrectly updating the limit flag when transitioning to the next digit position. Developers often write:

# WRONG: This doesn't properly track the boundary
total_count += count_ones_dp(position + 1, ones_count + (1 if digit == 1 else 0), is_limited)

Why it's wrong: Once we choose a digit less than the maximum allowed digit at any position, all subsequent positions are no longer limited by the original number. The limit should only remain True if we're currently limited AND we chose the maximum possible digit.

Solution:

# CORRECT: Properly update the limit flag
total_count += count_ones_dp(
    position + 1, 
    ones_count + (1 if digit == 1 else 0),
    is_limited and (digit == upper_bound)  # Only stay limited if we chose the max digit
)

2. Accumulating Count vs. Returning Count

Pitfall: Confusing whether to accumulate the count of 1s during recursion or return it at the base case. Some implementations try to add 1 to the result every time they encounter a digit 1:

# WRONG: Trying to accumulate in the wrong way
if digit == 1:
    total_count += 1  # This counts individual digits, not complete numbers
total_count += count_ones_dp(position + 1, ones_count, is_limited and digit == upper_bound)

Why it's wrong: The function should track how many 1s are in the current number being formed and return that count only when a complete number is formed (at the base case).

Solution:

# CORRECT: Track count in parameter, return at base case
if position >= len(number_str):
    return ones_count  # Return accumulated count for this complete number

# Pass updated count to next recursion level
total_count += count_ones_dp(
    position + 1, 
    ones_count + (1 if digit == 1 else 0),  # Update count in parameter
    is_limited and (digit == upper_bound)
)

3. Missing or Incorrect Memoization State

Pitfall: Not including all necessary state variables in the memoization, particularly forgetting the ones_count parameter:

# WRONG: Missing ones_count in memoization
@cache
def count_ones_dp(position: int, is_limited: bool) -> int:
    # This won't correctly memoize different paths with different 1 counts

Why it's wrong: Different paths through the recursion tree can reach the same position with different counts of 1s. Without including ones_count in the memoization key, we'll incorrectly reuse results from paths with different 1 counts.

Solution: Include all state variables that affect the result:

# CORRECT: Include all necessary state
@cache
def count_ones_dp(position: int, ones_count: int, is_limited: bool) -> int:
    # Properly memoizes based on position, count, and limit

4. Off-by-One Error with Range

Pitfall: Using incorrect range bounds when iterating through possible digits:

# WRONG: Missing the upper_bound digit itself
for digit in range(upper_bound):  # This excludes upper_bound

Why it's wrong: When is_limited is True, we need to include the digit at the current position of n as a valid choice. Using range(upper_bound) would exclude it.

Solution:

# CORRECT: Include upper_bound in the range
for digit in range(upper_bound + 1):  # Includes 0 through upper_bound
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

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

Load More