Numbers At Most N Given Digit Set: Digit DP Pattern
Given a sorted array of digit strings digits (each element is a string like "1", "3", etc.) and a positive integer n, return the count of positive integers that can be formed using only those digits and are <= n. You may reuse digits as many times as you want, and numbers must not have leading zeros.
This is LeetCode 902: Numbers At Most N Given Digit Set. It is a hard problem that looks daunting at first, but once you split it into two clean subproblems, the logic becomes surprisingly compact. The core technique is digit DP, which counts numbers by building them one digit at a time while respecting an upper bound.
For digits = ["1","3","5","7"] and n = 100, the answer is 20. There are 4 one-digit numbers (1, 3, 5, 7), 16 two-digit numbers (11, 13, 15, ..., 77), and 0 three-digit numbers that fit under 100.
Why this problem matters
Digit DP is one of those patterns that shows up rarely enough that people forget it, but frequently enough that it can decide an interview. Any problem that asks "how many numbers in some range satisfy a digit-level constraint?" likely uses this technique. Examples include counting numbers with specific digit sums, numbers that avoid certain digits, and numbers that match a pattern.
The mental model you build here transfers directly to those problems. The key idea is always the same: split the count into numbers with fewer digits (pure combinatorics) and numbers with the same number of digits (digit-by-digit comparison against the bound).
The key insight
Every valid number falls into one of two categories:
-
Shorter than N: if N has
kdigits, then any number with fewer thankdigits is automatically<= n. For lengthi, there areD^ivalid numbers (whereDis the size of the digit set). You just sum these up forifrom 1 tok - 1. -
Same length as N: you must build the number digit by digit, comparing each position to the corresponding digit of N. At each position, digits strictly less than the corresponding digit of N are "free", meaning all remaining positions can be anything. Digits equal to N's digit keep you on a "tight" path. Digits greater than N's digit are invalid.
This split is the backbone of every digit DP problem. The shorter-length count is pure math, and the same-length count is a single left-to-right scan.
Think of it as a tree. At each level you pick a digit. Branches where you pick a digit smaller than N's digit lead to entire free subtrees you can count with D^remaining. Branches where you match exactly keep you on the tight path. Branches where you exceed N's digit get pruned.
The solution
def atMostNGivenDigitSet(digits, n):
s = str(n)
k = len(s)
D = len(digits)
result = sum(D ** i for i in range(1, k))
for i in range(k):
found_equal = False
for d in digits:
if d < s[i]:
result += D ** (k - i - 1)
elif d == s[i]:
found_equal = True
if not found_equal:
return result
return result + 1
Here is what each piece does:
- Convert N to its string representation so you can compare digit by digit.
- Count shorter numbers: for lengths 1 through
k - 1, each position hasDchoices, givingD^1 + D^2 + ... + D^(k-1)total numbers. - Count same-length numbers: scan the digits of N from left to right. At each position, count how many digits in the set are strictly less than N's digit (each contributes
D^remainingnumbers). If a digit matches N's digit exactly, continue to the next position. If no digit matches, stop, because you cannot build N itself or any number with this prefix. - Handle N itself: if you made it through all positions with a match at every step, N itself is a valid number, so add 1.
Visual walkthrough
Step 1: Count all 1-digit numbers
Any single digit from the set is a valid number less than 100. That gives us 4.
Step 2: Count all 2-digit numbers
Each of the 2 digit positions has 4 choices. Total = 4 x 4 = 16 numbers.
Step 3: Same-length numbers (3 digits), check first digit of N
N[0] = 1. Count digits strictly less than 1: none. The only option is to match d = 1 exactly (tight).
Step 4: Same-length (tight), check second digit of N
N[1] = 0. No digit in the set is <= 0, so no valid 3-digit numbers exist. The tight path dies here.
Step 5: Sum all lengths
Total valid numbers = 4 + 16 + 0 = 20.
The walkthrough confirms the count: 4 one-digit numbers, 16 two-digit numbers, and 0 three-digit numbers. The tight path for three-digit numbers dies at the second position because no digit in the set is <= 0.
Complexity analysis
| Approach | Time | Space |
|---|---|---|
| Digit DP | O(log N * D) | O(log N) |
Time: you convert N to a string of length k = O(log N), then for each of the k positions you scan through D digits. Total work is O(k * D). Computing D^i values also takes O(k) time.
Space: the string representation of N takes O(log N) space. No DP table is needed since you only track a running count and whether you are on the tight path.
The building blocks
1. Counting shorter numbers with combinatorics
For any number with fewer digits than N, every position is a free choice from D options. This is the same counting principle you use when computing how many strings of length i you can form from an alphabet of size D:
total_shorter = sum(D ** i for i in range(1, k))
This pattern applies whenever you need to count all strings of a given length from a fixed alphabet. It shows up in combinatorics-heavy problems like counting binary strings, generating permutations, and base conversion.
2. Digit-by-digit comparison (tight bound)
The left-to-right scan compares your number to N one digit at a time. At each position, you decide: go lower (freedom for all remaining positions), match exactly (stay tight), or exceed (impossible, stop). This is the heart of digit DP:
for i in range(k):
for d in digits:
if d < s[i]:
result += D ** (k - i - 1)
elif d == s[i]:
found_equal = True
if not found_equal:
return result
This scan pattern is reusable for any problem where you count numbers up to a bound by processing digits left to right. The "tight" flag controls whether remaining digits are free or constrained.
The digit set is sorted, so you could use binary search to find the count of digits less than s[i] in O(log D) instead of O(D). For small digit sets (at most 9 elements), this does not matter in practice, but it is a good optimization to mention in an interview.
Edge cases
- All digits greater than N's first digit: for example
digits = ["3","5"]andn = 2. No same-length number is valid. The answer is purely from shorter lengths, which in this case is 0 since N has only 1 digit. Answer: 0. - N itself is constructible:
digits = ["1","2"]andn = 21. The tight path succeeds through all positions, so you add 1 for N itself. - Single-digit N:
digits = ["1","3","5","7"]andn = 5. No shorter-length numbers exist. Just count digits<= 5: that is 1, 3, 5, giving 3. - All digits the same:
digits = ["1"]andn = 111. Shorter:1^1 + 1^2 = 2. Same length: tight path succeeds (1 matches at every position), add 1. Total: 3 (the numbers 1, 11, 111). - Digit "1" with large N:
digits = ["1"]andn = 1000000000. The answer is just the number of digits in N, since the only numbers are 1, 11, 111, 1111, etc. That gives 9.
From understanding to recall
You have read the solution and it makes sense. Split into shorter numbers (easy combinatorics) and same-length numbers (digit-by-digit scan). But can you write it from scratch without looking?
The details matter: using D ** (k - i - 1) for the free subtree size, remembering to check found_equal at each position, and adding 1 at the end if the tight path completes. These are small things that are easy to mix up under pressure if you have not practiced writing them from memory.
Digit DP problems are rare enough that you might not encounter one for months, then suddenly face one in an interview. Spaced repetition keeps the pattern fresh. You practice writing the combinatorics sum and the digit scan from scratch at increasing intervals until the code flows without hesitation.
Related posts
- Counting Bits - Another problem that uses bit-level structure for counting, similar mindset to digit-level analysis
- Number of Digit One - Classic digit DP that counts occurrences of a specific digit across a range
- Permutations - Backtracking and counting, the brute-force alternative to the combinatorial approach used here
CodeBricks breaks the digit DP pattern into its combinatorial counting and digit-by-digit comparison building blocks, then drills them independently with spaced repetition. You type each piece from scratch until the pattern is automatic. When a digit DP question shows up in your interview, you do not think about it. You just write it.