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 digit1
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]
.
Intuition
When counting digit occurrences across a range of numbers, a brute force approach would iterate through every number and count the 1
s, 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 1
s 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
, or2
- If we chose
2
, at the second position we can choose0
,1
,2
, or3
- If we chose
2
then3
, at the third position we can only choose0
,1
,2
,3
, or4
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 1
s 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 1
s 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 digit1
seen so far in the current number being constructedlimit
: Boolean flag indicating if we're still bounded by the upper limitn
Implementation Steps:
-
Base Case: When
i >= len(s)
, we've processed all digit positions and return the accumulated countcnt
. -
Determine Upper Bound:
- If
limit
isTrue
, we can only place digits from0
tos[i]
(the i-th digit of n) - If
limit
isFalse
, we can place any digit from0
to9
up = int(s[i]) if limit else 9
- If
-
Try All Valid Digits: For each valid digit
j
from0
toup
:- Recursively call
dfs
for the next position - If
j == 1
, increment the count by 1 - Update the
limit
flag: it remainsTrue
only if both currentlimit
isTrue
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)
- Recursively call
-
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
- If we choose 0:
- 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)
wherem
is the number of digits inn
andD = 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 EvaluatorExample 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 equalsO(log(n))
since the string representation ofn
haslog₁₀(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 tolen(s) - 1
:O(log(n))
possibilitiescnt
represents the count of digit 1s encountered so far, which can be at mostlog(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
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
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
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
Want a Structured Path to Master System Design Too? Don’t Miss This!