2999. Count the Number of Powerful Integers
Problem Description
You need to find how many "powerful" integers exist within a given range [start, finish]
.
A positive integer is considered powerful if it meets two conditions:
- It ends with the string
s
(meanings
is a suffix of the number when viewed as a string) - Every digit in the number is at most
limit
For example, if s = "25"
and limit = 5
, then:
525
would be powerful (ends with "25" and all digits ≤ 5)625
would NOT be powerful (ends with "25" but has digit 6 > 5)523
would NOT be powerful (doesn't end with "25")
The solution uses Digit Dynamic Programming (Digit DP) to efficiently count valid numbers. The key insight is to transform the problem into finding the difference between two counts:
answer = count([1, finish]) - count([1, start-1])
The recursive function dfs(pos, lim)
builds numbers digit by digit from left to right:
pos
: current position being processedlim
: whether we're still bounded by the upper limit number
The algorithm works as follows:
- If the number being built has fewer digits than
s
, it can't possibly end withs
, so return 0 - When we've built enough digits to potentially match
s
, check if the remaining positions can forms
as a suffix - Otherwise, try placing each valid digit (0 to min of current limit and
limit
parameter) at the current position and recursively count valid numbers
The @cache
decorator memorizes results to avoid recalculating the same states, making the solution efficient even for large ranges.
Intuition
When we need to count numbers in a range with specific digit constraints, a brute force approach of checking every number would be too slow for large ranges. This is where we recognize a pattern that leads us to Digit DP.
The key observation is that we're dealing with constraints on individual digits and a suffix requirement. Instead of generating all numbers, we can build valid numbers digit by digit from left to right, keeping track of whether we're still constrained by the upper bound.
Think of it like filling in blanks: _ _ _ _ 2 5
where the last two positions must be "25" (our suffix s
), and each blank can only be filled with digits from 0 to limit
.
The insight to use count([1, finish]) - count([1, start-1])
comes from the principle of complementary counting - it's easier to count from 1 to a number than to count within an arbitrary range directly.
As we build numbers digit by digit, we face decisions at each position:
- If we haven't placed enough digits yet to accommodate the suffix, some numbers will be too short
- Once we reach the point where the remaining positions exactly match the suffix length, we must check if placing the suffix would create a valid number
- The
lim
flag is crucial - it tells us whether we're still bounded by the original number's digits or if we can freely choose any digit up tolimit
For example, if we're counting up to 5234 and we've placed "52" so far, the next digit can only be 0-3 to stay within bounds. But if we had placed "51", then for the next position we could use any digit from 0 to our limit
.
Memoization naturally fits here because the same sub-problems (remaining positions with same constraints) appear multiple times. Once we know how many valid numbers can be formed from a certain position with certain constraints, we can reuse that result.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
The solution implements Digit DP with memoization to efficiently count powerful integers. Here's how the implementation works:
Core Function Design:
The main recursive function dfs(pos, lim)
represents:
pos
: Current digit position we're filling (0-indexed from left)lim
: Boolean flag indicating if we're still bounded by the upper limit numbert
Step-by-step Implementation:
-
Base Case - Insufficient Digits:
if len(t) < n: return 0
If the target number
t
has fewer digits than the suffixs
, it's impossible to form a valid powerful integer. -
Base Case - Suffix Position Reached:
if len(t) - pos == n: return int(s <= t[pos:]) if lim else 1
When we've filled enough positions and only the suffix length remains:
- If
lim
is True (still bounded), check if placing suffixs
keeps us within bounds - If
lim
is False (already smaller than upper bound), we can always place the suffix
- If
-
Recursive Case - Fill Current Position:
up = min(int(t[pos]) if lim else 9, limit) ans = 0 for i in range(up + 1): ans += dfs(pos + 1, lim and i == int(t[pos]))
- Calculate upper bound
up
for current digit: minimum of the digit constraint and the limit parameter - Try each valid digit from 0 to
up
- Update
lim
for next position: remains True only if we chose the exact digit fromt[pos]
- Calculate upper bound
-
Main Algorithm - Range Calculation:
n = len(s) t = str(start - 1) a = dfs(0, True) dfs.cache_clear() t = str(finish) b = dfs(0, True) return b - a
- Convert the range problem to:
count([1, finish]) - count([1, start-1])
- Clear cache between calculations to avoid interference
- Return the difference as the final answer
- Convert the range problem to:
Key Optimizations:
- Memoization (
@cache
): Stores results ofdfs(pos, lim)
to avoid recalculating identical subproblems - Early Termination: Returns 0 immediately when the number can't possibly satisfy the suffix requirement
- Tight Bounds: The
lim
flag ensures we only explore valid numbers within the range
Time Complexity: O(D × 10 × 2)
where D is the number of digits in finish
. Each position has at most 10 digit choices and 2 states for lim
.
Space Complexity: O(D × 2)
for the memoization cache.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through finding powerful integers in the range [50, 120]
where s = "5"
and limit = 6
.
Step 1: Transform the problem
- We need:
count([1, 120]) - count([1, 49])
- First calculate
count([1, 49])
, thencount([1, 120])
Calculating count([1, 49]):
We build numbers digit by digit, with t = "49"
:
Starting at dfs(0, True)
(position 0, still bounded):
- Since
len("49") - 0 = 2
and suffix length is 1, we continue - Upper bound:
min(4, 6) = 4
(digit at position 0 is '4', limit is 6) - Try digits 0-4:
- Digit 0:
dfs(1, False)
- now unbounded since 0 < 4 - Digit 1:
dfs(1, False)
- unbounded since 1 < 4 - Digit 2:
dfs(1, False)
- unbounded since 2 < 4 - Digit 3:
dfs(1, False)
- unbounded since 3 < 4 - Digit 4:
dfs(1, True)
- still bounded since 4 = 4
- Digit 0:
For dfs(1, False)
(position 1, unbounded):
- Now
len("49") - 1 = 1
equals suffix length - Since unbounded, we can place suffix "5" → returns 1
For dfs(1, True)
(position 1, bounded):
- Now
len("49") - 1 = 1
equals suffix length - Check if "5" ≤ "9" (remaining part of "49") → True → returns 1
Total for count([1, 49])
: 0 + 1 + 1 + 1 + 1 + 1 = 5
(Numbers: 5, 15, 25, 35, 45)
Calculating count([1, 120]):
With t = "120"
:
Starting at dfs(0, True)
:
- Since
len("120") - 0 = 3
and suffix length is 1, we continue - Upper bound:
min(1, 6) = 1
- Try digits 0-1:
- Digit 0:
dfs(1, False)
- Digit 1:
dfs(1, True)
- Digit 0:
For dfs(1, False)
(position 1, unbounded):
len("120") - 1 = 2
> suffix length 1, continue- Upper bound:
min(9, 6) = 6
(unbounded, so use limit) - Try digits 0-6, each leads to
dfs(2, False)
- Each
dfs(2, False)
returns 1 (can place suffix "5") - Subtotal: 7
For dfs(1, True)
(position 1, bounded by "20"):
- Upper bound:
min(2, 6) = 2
- Try digits 0-2:
- Digit 0:
dfs(2, False)
→ returns 1 - Digit 1:
dfs(2, False)
→ returns 1 - Digit 2:
dfs(2, True)
→ checks "5" ≤ "0" → False → returns 0
- Digit 0:
- Subtotal: 2
Total for count([1, 120])
: 7 + 2 = 9
(Numbers: 5, 15, 25, 35, 45, 55, 65 from first branch + 105, 115 from second branch)
Final Answer: 9 - 5 = 4 powerful integers in range [50, 120] (These are: 55, 65, 105, 115)
Solution Implementation
1class Solution:
2 def numberOfPowerfulInt(self, start: int, finish: int, limit: int, s: str) -> int:
3 """
4 Count powerful integers in range [start, finish] where:
5 - Each digit doesn't exceed 'limit'
6 - The number ends with suffix 's'
7 """
8 from functools import cache
9
10 @cache
11 def count_valid_numbers(position: int, is_tight: int) -> int:
12 """
13 Digit DP to count valid numbers.
14
15 Args:
16 position: Current position in the number being built
17 is_tight: Whether we're still bounded by the upper limit number
18
19 Returns:
20 Count of valid numbers from this state
21 """
22 # If current number has fewer digits than suffix, it's invalid
23 if len(current_bound) < suffix_length:
24 return 0
25
26 # Check if we've reached the position where suffix should start
27 if len(current_bound) - position == suffix_length:
28 # If tight, check if remaining part is >= suffix
29 # If not tight, any number with suffix is valid
30 return int(s <= current_bound[position:]) if is_tight else 1
31
32 # Determine the maximum digit we can place at current position
33 if is_tight:
34 max_digit = min(int(current_bound[position]), limit)
35 else:
36 max_digit = limit
37
38 # Try all possible digits from 0 to max_digit
39 total_count = 0
40 for digit in range(max_digit + 1):
41 # Recurse to next position
42 # Remain tight only if we're currently tight and chose the maximum possible digit
43 total_count += count_valid_numbers(
44 position + 1,
45 is_tight and digit == int(current_bound[position])
46 )
47
48 return total_count
49
50 # Length of the required suffix
51 suffix_length = len(s)
52
53 # Count numbers <= (start - 1)
54 current_bound = str(start - 1)
55 count_below_start = count_valid_numbers(0, True)
56
57 # Clear cache before computing for the upper bound
58 count_valid_numbers.cache_clear()
59
60 # Count numbers <= finish
61 current_bound = str(finish)
62 count_up_to_finish = count_valid_numbers(0, True)
63
64 # Return count of numbers in range [start, finish]
65 return count_up_to_finish - count_below_start
66
1class Solution {
2 private String suffix;
3 private String currentUpperBound;
4 private Long[] memoization;
5 private int maxDigitValue;
6
7 public long numberOfPowerfulInt(long start, long finish, int limit, String s) {
8 this.suffix = s;
9 this.maxDigitValue = limit;
10
11 // Count powerful integers from 0 to (start - 1)
12 currentUpperBound = String.valueOf(start - 1);
13 memoization = new Long[20];
14 long countBelowStart = dfs(0, true);
15
16 // Count powerful integers from 0 to finish
17 currentUpperBound = String.valueOf(finish);
18 memoization = new Long[20];
19 long countUpToFinish = dfs(0, true);
20
21 // Return count in range [start, finish]
22 return countUpToFinish - countBelowStart;
23 }
24
25 /**
26 * Dynamic programming function to count valid numbers
27 * @param position Current position in the number being formed
28 * @param isTight Whether we're still bounded by the upper limit (currentUpperBound)
29 * @return Count of valid numbers from this state
30 */
31 private long dfs(int position, boolean isTight) {
32 // If upper bound has fewer digits than required suffix, no valid numbers exist
33 if (currentUpperBound.length() < suffix.length()) {
34 return 0;
35 }
36
37 // Use memoization when not in tight constraint
38 if (!isTight && memoization[position] != null) {
39 return memoization[position];
40 }
41
42 // Base case: when remaining positions equal suffix length
43 if (currentUpperBound.length() - position == suffix.length()) {
44 if (isTight) {
45 // Check if suffix fits within the remaining bound
46 String remainingBound = currentUpperBound.substring(position);
47 return suffix.compareTo(remainingBound) <= 0 ? 1 : 0;
48 } else {
49 // No tight constraint, suffix always fits
50 return 1;
51 }
52 }
53
54 // Determine the maximum digit we can place at current position
55 int upperLimit = isTight ? currentUpperBound.charAt(position) - '0' : 9;
56 upperLimit = Math.min(upperLimit, maxDigitValue);
57
58 // Try all possible digits from 0 to upperLimit
59 long totalCount = 0;
60 for (int digit = 0; digit <= upperLimit; ++digit) {
61 boolean nextIsTight = isTight && (digit == currentUpperBound.charAt(position) - '0');
62 totalCount += dfs(position + 1, nextIsTight);
63 }
64
65 // Store result in memoization array if not under tight constraint
66 if (!isTight) {
67 memoization[position] = totalCount;
68 }
69
70 return totalCount;
71 }
72}
73
1class Solution {
2public:
3 long long numberOfPowerfulInt(long long start, long long finish, int limit, string s) {
4 // Convert start-1 to string for calculating count of powerful integers < start
5 string targetNumber = to_string(start - 1);
6
7 // Memoization table for digit DP (-1 means not computed yet)
8 long long memo[20];
9 memset(memo, -1, sizeof(memo));
10
11 // Digit DP function to count powerful integers <= targetNumber
12 // pos: current position in targetNumber being processed
13 // isTight: whether we're still bounded by targetNumber's digits
14 auto countPowerfulIntegers = [&](this auto&& countPowerfulIntegers, int pos, bool isTight) -> long long {
15 // If targetNumber is shorter than suffix s, no valid numbers exist
16 if (targetNumber.size() < s.size()) {
17 return 0;
18 }
19
20 // Use memoization only when not in tight constraint
21 if (!isTight && memo[pos] != -1) {
22 return memo[pos];
23 }
24
25 // Base case: when remaining digits equal suffix length
26 // Check if we can place suffix s at this position
27 if (targetNumber.size() - pos == s.size()) {
28 if (isTight) {
29 // If tight, suffix must be <= remaining part of targetNumber
30 return s <= targetNumber.substr(pos) ? 1 : 0;
31 } else {
32 // If not tight, suffix can always be placed
33 return 1;
34 }
35 }
36
37 long long result = 0;
38
39 // Determine upper bound for current digit
40 // If tight: bounded by current digit of targetNumber
41 // If not tight: can go up to min(9, limit)
42 int upperBound = isTight ? (targetNumber[pos] - '0') : 9;
43 upperBound = min(upperBound, limit);
44
45 // Try all possible digits at current position
46 for (int digit = 0; digit <= upperBound; ++digit) {
47 // Recursively count for next position
48 // Remain tight only if we chose the maximum allowed digit
49 bool nextTight = isTight && (digit == (targetNumber[pos] - '0'));
50 result += countPowerfulIntegers(pos + 1, nextTight);
51 }
52
53 // Store result in memoization table if not in tight constraint
54 if (!isTight) {
55 memo[pos] = result;
56 }
57
58 return result;
59 };
60
61 // Count powerful integers from 0 to (start-1)
62 long long countBelowStart = countPowerfulIntegers(0, true);
63
64 // Reset for counting up to finish
65 targetNumber = to_string(finish);
66 memset(memo, -1, sizeof(memo));
67
68 // Count powerful integers from 0 to finish
69 long long countUpToFinish = countPowerfulIntegers(0, true);
70
71 // Return count in range [start, finish]
72 return countUpToFinish - countBelowStart;
73 }
74};
75
1/**
2 * Counts the number of powerful integers in the range [start, finish]
3 * A powerful integer is one that ends with suffix 's' and all digits are <= limit
4 * @param start - The starting number of the range (inclusive)
5 * @param finish - The ending number of the range (inclusive)
6 * @param limit - The maximum allowed digit value (0-9)
7 * @param s - The required suffix string
8 * @returns The count of powerful integers in the given range
9 */
10function numberOfPowerfulInt(start: number, finish: number, limit: number, s: string): number {
11 // Calculate count for numbers from 0 to (start-1)
12 let targetNumber: string = (start - 1).toString();
13 let memoization: number[] = Array(20).fill(-1);
14
15 /**
16 * Dynamic programming function to count valid numbers
17 * @param position - Current position in the number being formed
18 * @param hasLimit - Whether we're still bounded by the target number's digits
19 * @returns Count of valid numbers from current state
20 */
21 const countValidNumbers = (position: number, hasLimit: boolean): number => {
22 // If target number is shorter than required suffix, no valid numbers exist
23 if (targetNumber.length < s.length) {
24 return 0;
25 }
26
27 // Return memoized result if available (only when not limited)
28 if (!hasLimit && memoization[position] !== -1) {
29 return memoization[position];
30 }
31
32 // Base case: reached the position where suffix should start
33 if (targetNumber.length - position === s.length) {
34 if (hasLimit) {
35 // Check if suffix fits within the remaining digits of target
36 return s <= targetNumber.substring(position) ? 1 : 0;
37 }
38 // No limit means suffix always fits
39 return 1;
40 }
41
42 let totalCount: number = 0;
43 // Determine upper bound for current digit
44 const upperBound: number = Math.min(
45 hasLimit ? Number(targetNumber[position]) : 9,
46 limit
47 );
48
49 // Try all possible digits from 0 to upperBound
50 for (let digit = 0; digit <= upperBound; digit++) {
51 // Recursively count, maintaining limit flag if we chose the max digit
52 totalCount += countValidNumbers(
53 position + 1,
54 hasLimit && digit === Number(targetNumber[position])
55 );
56 }
57
58 // Memoize result when not limited by target number
59 if (!hasLimit) {
60 memoization[position] = totalCount;
61 }
62
63 return totalCount;
64 };
65
66 // Count powerful integers from 0 to (start-1)
67 const countUpToStartMinus1: number = countValidNumbers(0, true);
68
69 // Reset for counting from 0 to finish
70 targetNumber = finish.toString();
71 memoization = Array(20).fill(-1);
72
73 // Count powerful integers from 0 to finish
74 const countUpToFinish: number = countValidNumbers(0, true);
75
76 // Return the difference to get count in range [start, finish]
77 return countUpToFinish - countUpToStartMinus1;
78}
79
Time and Space Complexity
The time complexity is O(log M × D)
, where M
represents the maximum value between start
and finish
, and D = 10
(the number of possible digits 0-9).
Time Complexity Analysis:
- The recursive function
dfs
explores digit positions from left to right in the number representation - The maximum depth of recursion is
O(log M)
since we process each digit position of the number (the number of digits in a numberM
is proportional tolog M
) - At each position, we iterate through at most
min(limit + 1, 10)
choices, which is bounded byD = 10
- The function is called twice (once for
start - 1
and once forfinish
) - With memoization via
@cache
, each unique state(pos, lim)
is computed only once - There are at most
O(log M)
positions and 2 possible values forlim
(True/False), giving usO(log M)
unique states - Therefore, the total time complexity is
O(log M × D)
Space Complexity Analysis:
- The recursion depth is at most
O(log M)
(the number of digits) - The memoization cache stores at most
O(log M)
unique states (consideringpos
ranges from 0 to number of digits, andlim
has 2 values) - The string
t
takesO(log M)
space to store the number representation - Therefore, the total space complexity is
O(log M)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Suffix Comparison with Leading Zeros
Problem: When comparing if the suffix s
can be placed at the end, the code directly compares strings: s <= current_bound[position:]
. This can fail when the suffix has leading zeros.
For example, if s = "007"
and we're checking if it fits in the remaining part "123", the string comparison "007" <= "123"
returns True
, but numerically placing "007" at those positions would create an invalid number.
Solution: Convert to integers for proper numerical comparison:
if len(current_bound) - position == suffix_length:
if is_tight:
# Compare as integers to handle leading zeros correctly
return 1 if int(s) <= int(current_bound[position:]) else 0
else:
return 1
2. Not Validating Suffix Digits Against Limit
Problem: The code assumes the suffix s
itself is valid (all digits ≤ limit), but doesn't validate this. If s = "789"
and limit = 5
, the algorithm would still try to count numbers ending with "789", which is impossible.
Solution: Add validation before starting the computation:
def numberOfPowerfulInt(self, start: int, finish: int, limit: int, s: str) -> int:
# Validate that suffix itself satisfies the digit limit
for digit_char in s:
if int(digit_char) > limit:
return 0 # No valid powerful integers possible
# Rest of the implementation...
3. Cache Pollution Between Test Cases
Problem: Using @cache
without proper cleanup between different test cases can cause incorrect results when the same Solution
instance is reused with different parameters.
Solution: Use local caching or ensure proper cleanup:
def numberOfPowerfulInt(self, start: int, finish: int, limit: int, s: str) -> int:
from functools import lru_cache
# Create a new cache for each function call
@lru_cache(maxsize=None)
def count_valid_numbers(position: int, is_tight: int) -> int:
# Implementation...
# Compute results
current_bound = str(start - 1)
count_below_start = count_valid_numbers(0, True)
# Clear cache between bounds
count_valid_numbers.cache_clear()
current_bound = str(finish)
count_up_to_finish = count_valid_numbers(0, True)
# Clear cache after computation for next test case
count_valid_numbers.cache_clear()
return count_up_to_finish - count_below_start
4. Edge Case: When start = 1
Problem: Computing start - 1
when start = 1
gives 0, and str(0) = "0"
. This edge case might not be handled correctly if the suffix length equals 1 and suffix is "0".
Solution: Handle the zero case explicitly:
if start == 1:
# Special handling for lower bound
current_bound = "0"
count_below_start = 0 if suffix_length > 1 or s != "0" else 1
else:
current_bound = str(start - 1)
count_below_start = count_valid_numbers(0, True)
5. Integer Overflow for Large Ranges
Problem: Although Python handles arbitrary precision integers, converting between strings and integers repeatedly for very large numbers can impact performance.
Solution: Keep computations in string format when possible and only convert when necessary for comparisons:
# Instead of converting entire numbers, work with digit comparisons
if len(current_bound) - position == suffix_length:
if is_tight:
# Compare digit by digit instead of full integer conversion
for i in range(suffix_length):
if s[i] < current_bound[position + i]:
return 1
elif s[i] > current_bound[position + i]:
return 0
return 1 # Equal case
else:
return 1
How does quick sort divide the problem into subproblems?
Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!