A clear explanation of counting numbers less than or equal to N using digit-by-digit construction and combinatorics.
Problem Restatement
We are given:
- A set of allowed digits as strings.
- An integer
n.
We may use the given digits any number of times to build positive integers.
We need to count how many numbers are less than or equal to n.
For example:
digits = ["1", "3", "5", "7"]
n = 100Valid numbers include:
1, 3, 5, 7,
11, 13, 15, 17,
31, 33, ...
71, 73, 75, 77We count every valid positive integer that does not exceed 100.
Input and Output
| Item | Meaning |
|---|---|
| Input | digits and integer n |
| Output | Number of valid integers |
| Digits | Can be reused multiple times |
| Constraint | Every constructed number must be <= n |
Function shape:
def atMostNGivenDigitSet(digits: list[str], n: int) -> int:
...Examples
Example 1:
digits = ["1", "3", "5", "7"]
n = 100Answer:
20Explanation:
- 1-digit numbers:
4 - 2-digit numbers:
4 * 4 = 16 - No valid 3-digit number is
<= 100
Total:
4 + 16 = 20Example 2:
digits = ["1", "4", "9"]
n = 1000000000Answer:
29523Example 3:
digits = ["7"]
n = 8Valid numbers:
7Answer:
1First Thought: Generate Everything
A direct approach is:
- Generate every possible number using the allowed digits.
- Keep only numbers
<= n.
This quickly becomes impossible.
If we have k digits and length m, the number of possibilities is:
k^mFor large n, this explodes rapidly.
We need to count mathematically instead of generating explicitly.
Key Insight
We can split the problem into two parts.
Part 1: Numbers Shorter Than n
Suppose:
n = 3456Every valid number with:
- 1 digit
- 2 digits
- 3 digits
is automatically smaller than 3456.
So we can count them directly.
If there are k allowed digits:
- Length 1:
k - Length 2:
k^2 - Length 3:
k^3
Total:
k + k^2 + k^3Part 2: Numbers With Same Length As n
Now we count valid numbers digit by digit.
For:
n = 3456we compare from left to right.
At each position:
- Count digits smaller than the current digit of
n. - Add all combinations for remaining positions.
- If the exact digit exists, continue.
- Otherwise stop.
This behaves like lexicographic counting.
Algorithm
Convert n into a string:
s = str(n)Let:
m = len(s)
k = len(digits)Step 1: Count Shorter Numbers
For every length smaller than m:
k^lengthAdd all counts together.
Step 2: Process Equal-Length Numbers
Walk through each digit position.
At position i:
- Count how many allowed digits are smaller than
s[i]. - For each smaller digit, remaining positions can be anything:
smaller_count * (k ^ remaining_positions)- If
s[i]itself is not available, stop immediately. - Otherwise continue to the next digit.
If we finish all positions successfully, include n itself.
Correctness
Every valid number belongs to exactly one of two groups:
- Numbers with fewer digits than
n - Numbers with the same number of digits as
n
The first group is counted completely using combinatorics.
For the second group, we process digits left to right.
At each position, choosing a smaller digit guarantees the constructed number is already smaller than n, so remaining positions may contain any allowed digits.
If the current digit of n is unavailable, no continuation can match or stay below n, so counting stops correctly.
If every digit matches successfully, then n itself is valid and must be counted.
No valid number is missed, and no invalid number is added.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(len(n) * len(digits)) | Each position scans all digits |
| Space | O(1) | Only constant extra space |
Implementation
class Solution:
def atMostNGivenDigitSet(self, digits: list[str], n: int) -> int:
s = str(n)
k = len(digits)
m = len(s)
answer = 0
for length in range(1, m):
answer += k ** length
for i in range(m):
current = s[i]
smaller_count = 0
for d in digits:
if d < current:
smaller_count += 1
remaining = m - i - 1
answer += smaller_count * (k ** remaining)
if current not in digits:
return answer
return answer + 1Code Explanation
Convert n into a string so we can process digits individually:
s = str(n)Count all valid shorter numbers:
for length in range(1, m):
answer += k ** lengthNow process equal-length numbers.
At each position:
for i in range(m):we count digits smaller than the current digit:
if d < current:
smaller_count += 1Every smaller choice allows free selection of remaining positions:
answer += smaller_count * (k ** remaining)If the current digit itself does not exist:
if current not in digits:
return answerthen we cannot continue matching n.
If every digit matches successfully, n itself is valid:
return answer + 1Testing
def run_tests():
s = Solution()
assert s.atMostNGivenDigitSet(["1", "3", "5", "7"], 100) == 20
assert s.atMostNGivenDigitSet(["1", "4", "9"], 1000000000) == 29523
assert s.atMostNGivenDigitSet(["7"], 8) == 1
assert s.atMostNGivenDigitSet(["1", "2", "3"], 3) == 3
assert s.atMostNGivenDigitSet(["1", "2", "3"], 35) == 12
assert s.atMostNGivenDigitSet(["5", "7", "8"], 4) == 0
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
| Sample case | Confirms main logic |
Very large n | Checks scalability |
| Single digit set | Minimal configuration |
| Exact equality | Confirms inclusive counting |
| Mid-range value | Checks digit-by-digit counting |
| No valid numbers | Confirms zero handling |