Given an array of digits
which is sorted in non-decreasing order. You can write numbers using each digits[i]
as many times as we want. For example, if digits = ['1','3','5']
, we may write numbers such as '13'
, '551'
, and '1351315'
.
Return the number of positive integers that can be generated that are less than or equal to a given integer n
.
Example 1:
Input: digits = ["1","3","5","7"], n = 100 Output: 20 Explanation: The 20 numbers that can be written are: 1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77.
Example 2:
Input: digits = ["1","4","9"], n = 1000000000 Output: 29523 Explanation: We can write 3 one digit numbers, 9 two digit numbers, 27 three digit numbers, 81 four digit numbers, 243 five digit numbers, 729 six digit numbers, 2187 seven digit numbers, 6561 eight digit numbers, and 19683 nine digit numbers. In total, this is 29523 integers that can be written using the digits array.
Example 3:
Input: digits = ["7"], n = 8 Output: 1
Constraints:
1 <= digits.length <= 9
digits[i].length == 1
digits[i]
is a digit from '1'
to '9'
.digits
are unique.digits
is sorted in non-decreasing order.1 <= n <= 109
This problem asks to count positive integers less than or equal to n
that can be formed using digits from a given set. A brute-force approach would be computationally expensive for large n
. The optimal solution utilizes Digit Dynamic Programming (DP).
Core Idea:
The Digit DP approach breaks down the problem into smaller subproblems based on the digits of n
. Instead of generating all possible numbers, it recursively counts the possibilities digit by digit. Memoization (caching results of subproblems) significantly improves efficiency.
Recursive Function (dfs
)
The heart of the solution is a recursive function dfs(i, lead, limit)
:
i
: The current digit position (starting from the most significant digit).lead
: A boolean flag indicating whether we're still processing leading zeros. Numbers with leading zeros are not counted.limit
: A boolean flag indicating whether the current digit is constrained by the corresponding digit in n
.Base Case:
If i
exceeds the number of digits in n
, it means we've processed all digits. If lead
is true (we had leading zeros), we return 0; otherwise, we return 1 (a valid number was formed).
Recursive Step:
Determine Upper Bound (up
): If limit
is true, the maximum value for the current digit is the corresponding digit in n
; otherwise, it's 9.
Iterate Through Digits: The code iterates through possible digits (j
) from 0 to up
.
Handle Leading Zeros: If j
is 0 and lead
is true, we recursively call dfs
with lead
remaining true to continue processing potential leading zeros.
Check Digit Validity: If j
is in the allowed digits
set, we recursively call dfs
with lead
set to false (since we've passed the leading zeros stage). limit
is updated to reflect whether the next digit is constrained by n
.
Accumulate Results: The results of recursive calls are accumulated in ans
.
Memoization: The @cache
decorator (Python) or equivalent mechanisms (Java, C++, Go, TypeScript) store the results of dfs
calls, avoiding redundant computations.
Main Function:
n
to a string s
.nums
containing the allowed digits (for efficient lookup).dfs(0, True, True)
to start the recursive process from the most significant digit, with lead
and limit
initially set to True
.Time and Space Complexity:
Time Complexity: O(log₁₀ n * D), where D is the size of the digits
set. The logarithm reflects the number of digits in n
, and the process iterates through allowed digits at each position. Memoization drastically reduces the time complexity, avoiding exponential growth.
Space Complexity: O(log₁₀ n) due to the recursive call stack and the memoization table (which is proportional to the number of digits in n
).
Example Code (Python):
class Solution:
def atMostNGivenDigitSet(self, digits: List[str], n: int) -> int:
@cache
def dfs(i: int, lead: int, limit: bool) -> int:
# ... (rest of the code as described above) ...
The code in other languages follows the same logic, with variations in syntax and memoization implementation. The key is the efficient recursive approach guided by the lead
and limit
flags, enhanced by memoization to handle overlapping subproblems.