2719. Count of Integers
Problem Description
You are given two numeric strings num1
and num2
representing integers, and two integers max_sum
and min_sum
. Your task is to count how many integers x
are "good" based on the following criteria:
x
must be within the range[num1, num2]
(inclusive), meaningnum1 <= x <= num2
- The sum of digits of
x
must be within the range[min_sum, max_sum]
(inclusive), meaningmin_sum <= digit_sum(x) <= max_sum
For example, if x = 123
, then digit_sum(x) = 1 + 2 + 3 = 6
.
Since the count of good integers can be very large, you need to return the result modulo 10^9 + 7
.
The challenge involves efficiently counting all integers in a potentially huge range that satisfy the digit sum constraint, without iterating through each number individually. The solution uses digit dynamic programming to handle this efficiently by processing the numbers digit by digit from left to right, keeping track of the current digit sum and whether we've reached the upper bound of our range.
Intuition
When we need to count integers in a range [num1, num2]
that satisfy certain digit-based conditions, directly iterating through all numbers would be inefficient, especially for large ranges. Instead, we can transform this into a classic digit DP problem.
The key insight is to convert the range problem into a difference of two simpler problems. Rather than counting good integers in [num1, num2]
, we can:
- Count good integers in
[0, num2]
- Count good integers in
[0, num1-1]
- Subtract the second from the first to get our answer
For counting integers up to a given number with digit sum constraints, we can build the number digit by digit from left to right. At each position, we need to track:
- Current position (
pos
): Which digit we're currently placing - Current digit sum (
s
): Sum of all digits we've placed so far - Limit flag (
limit
): Whether we're still bounded by the upper limit number
The limit
flag is crucial - if we've been placing digits that match the upper bound so far (e.g., if the upper bound is 523
and we've placed 5
and 2
), then at the current position we can only place digits up to the corresponding digit in the upper bound (in this case, 3
). However, if we've already placed a smaller digit earlier (like placing 4
instead of 5
at the first position), then we're free to place any digit 0-9
at subsequent positions.
This recursive approach with memoization allows us to efficiently explore all valid numbers without explicitly generating them. When we reach the end of the number (processed all positions), we simply check if our accumulated digit sum falls within [min_sum, max_sum]
.
The modulo operation 10^9 + 7
is applied during the computation to prevent integer overflow while maintaining the correct result.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
The solution implements digit dynamic programming with memoization. Here's how the implementation works:
Main Function Structure:
The count
function handles the range calculation by computing the answer for [0, num2]
and subtracting the answer for [0, num1-1]
.
Core DP Function - dfs(pos, s, limit)
:
pos
: Current digit position being processed (from left to right)s
: Current sum of digits accumulated so farlimit
: Boolean flag indicating if we're still bounded by the upper limit
Base Case:
When pos >= len(num)
, we've formed a complete number. We return 1
if the digit sum s
is within [min_sum, max_sum]
, otherwise return 0
.
Recursive Case:
-
Determine the upper bound for the current digit:
- If
limit
isTrue
: upper bound isint(num[pos])
(the digit at current position in the limit number) - If
limit
isFalse
: upper bound is9
(we can use any digit)
- If
-
Try all possible digits from
0
toup
:- For each digit
i
, recursively calldfs(pos + 1, s + i, limit and i == up)
- The new limit is
True
only if both currentlimit
isTrue
AND we chose the maximum possible digiti == up
- For each digit
-
Sum all possibilities and apply modulo
10^9 + 7
Memoization with @cache
:
The @cache
decorator stores results of dfs
calls to avoid redundant calculations. This is crucial for efficiency as many subproblems repeat.
Computing the Final Answer:
num = num2
a = dfs(0, 0, True) # Count good integers in [0, num2]
dfs.cache_clear() # Clear cache before next computation
num = str(int(num1) - 1) # Convert to num1-1
b = dfs(0, 0, True) # Count good integers in [0, num1-1]
return (a - b) % mod # Return the difference
The cache is cleared between computations because the num
variable changes, and cached results from the first computation would be invalid for the second.
Time Complexity: O(10 × n × max_sum)
where n
is the length of the number string. For each position (n), each possible digit sum (up to max_sum), we might try up to 10 digits.
Space Complexity: O(n × max_sum)
for the memoization cache storing unique states.
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 a small example to illustrate the solution approach.
Example: Count good integers where num1 = "10"
, num2 = "15"
, min_sum = 3
, max_sum = 5
We need to find integers in range [10, 15] whose digit sums are in [3, 5].
Step 1: Transform the problem Instead of directly counting in [10, 15], we compute:
- Count of good integers in [0, 15]
- Count of good integers in [0, 9] (which is num1-1)
- Answer = first count - second count
Step 2: Count good integers in [0, 15] using dfs(pos, s, limit)
Let's trace through building numbers digit by digit up to "15":
Starting with dfs(0, 0, True)
:
- Position 0 (tens place), sum=0, limit=True
- Upper bound is '1' (first digit of "15")
- Try digit 0: leads to numbers 00-09
dfs(1, 0, False)
- limit becomes False since 0 < 1- At position 1, can use any digit 0-9
- Results: 03(sum=3✓), 04(sum=4✓), 05(sum=5✓) → 3 good numbers
- Try digit 1: leads to numbers 10-15
dfs(1, 1, True)
- limit stays True since 1 == 1- At position 1, upper bound is '5' (second digit of "15")
- Try digits 0-5:
- 10: sum=1+0=1 ✗
- 11: sum=1+1=2 ✗
- 12: sum=1+2=3 ✓
- 13: sum=1+3=4 ✓
- 14: sum=1+4=5 ✓
- 15: sum=1+5=6 ✗
- Results: 3 good numbers
Total for [0, 15]: 3 + 3 = 6 good numbers
Step 3: Count good integers in [0, 9]
Starting with dfs(0, 0, True)
:
- Position 0 (ones place), sum=0, limit=True
- Upper bound is '9' (the only digit of "9")
- Try digits 0-9:
- 0: sum=0 ✗
- 1: sum=1 ✗
- 2: sum=2 ✗
- 3: sum=3 ✓
- 4: sum=4 ✓
- 5: sum=5 ✓
- 6: sum=6 ✗
- 7: sum=7 ✗
- 8: sum=8 ✗
- 9: sum=9 ✗
Total for [0, 9]: 3 good numbers
Step 4: Calculate final answer Answer = 6 - 3 = 3
The good integers in [10, 15] are: 12 (sum=3), 13 (sum=4), 14 (sum=5)
Key Observations:
- The
limit
flag controls whether we can use any digit (0-9) or are restricted by the upper bound - Once we place a digit smaller than the bound, all subsequent positions become unrestricted
- Memoization prevents recalculating the same (pos, s, limit) states multiple times
- The digit sum constraint is only checked when we've built a complete number (base case)
Solution Implementation
1class Solution:
2 def count(self, num1: str, num2: str, min_sum: int, max_sum: int) -> int:
3 """
4 Count integers between num1 and num2 (inclusive) whose digit sum is between min_sum and max_sum.
5 Uses digit DP technique to efficiently count valid numbers.
6
7 Args:
8 num1: Lower bound as string
9 num2: Upper bound as string
10 min_sum: Minimum allowed digit sum
11 max_sum: Maximum allowed digit sum
12
13 Returns:
14 Count of valid integers modulo 10^9 + 7
15 """
16 from functools import cache
17
18 MOD = 10**9 + 7
19
20 @cache
21 def digit_dp(position: int, digit_sum: int, is_limit: bool, current_number: str) -> int:
22 """
23 Dynamic programming function to count valid numbers.
24
25 Args:
26 position: Current position in the number being formed
27 digit_sum: Sum of digits chosen so far
28 is_limit: Whether we're still bounded by the original number's digits
29 current_number: The upper bound number we're comparing against
30
31 Returns:
32 Count of valid numbers from this state
33 """
34 # Base case: reached end of number
35 if position >= len(current_number):
36 # Check if digit sum is within valid range
37 return 1 if min_sum <= digit_sum <= max_sum else 0
38
39 # Determine the maximum digit we can place at current position
40 # If limited by original number, use its digit; otherwise use 9
41 max_digit = int(current_number[position]) if is_limit else 9
42
43 # Try all possible digits from 0 to max_digit
44 total_count = 0
45 for digit in range(max_digit + 1):
46 # Recurse to next position
47 # Update digit_sum and check if we're still at limit
48 total_count += digit_dp(
49 position + 1,
50 digit_sum + digit,
51 is_limit and (digit == max_digit),
52 current_number
53 )
54 total_count %= MOD
55
56 return total_count
57
58 # Count valid numbers from 0 to num2
59 count_up_to_num2 = digit_dp(0, 0, True, num2)
60
61 # Clear cache before computing for num1
62 digit_dp.cache_clear()
63
64 # Count valid numbers from 0 to (num1 - 1)
65 # This excludes num1 from the count
66 num1_minus_one = str(int(num1) - 1)
67 count_up_to_num1_minus_one = digit_dp(0, 0, True, num1_minus_one)
68
69 # Return difference to get count in range [num1, num2]
70 return (count_up_to_num2 - count_up_to_num1_minus_one) % MOD
71
1import java.math.BigInteger;
2
3class Solution {
4 // Modulo value for result
5 private final int MOD = (int) 1e9 + 7;
6
7 // Memoization table: dp[position][digitSum]
8 // Stores the count of valid numbers for a given position and digit sum
9 private Integer[][] dp;
10
11 // Current number string being processed
12 private String currentNumber;
13
14 // Minimum and maximum sum constraints
15 private int minSum;
16 private int maxSum;
17
18 /**
19 * Counts integers between num1 and num2 (inclusive) whose digit sum is between min_sum and max_sum
20 * @param num1 Lower bound of the range (inclusive)
21 * @param num2 Upper bound of the range (inclusive)
22 * @param min_sum Minimum allowed digit sum
23 * @param max_sum Maximum allowed digit sum
24 * @return Count of valid integers modulo 10^9 + 7
25 */
26 public int count(String num1, String num2, int min_sum, int max_sum) {
27 // Initialize constraints
28 minSum = min_sum;
29 maxSum = max_sum;
30
31 // Calculate count of valid numbers from 0 to num2
32 currentNumber = num2;
33 dp = new Integer[23][220]; // Max 23 digits, max sum 9*23 ≈ 220
34 int countUpToNum2 = digitDP(0, 0, true);
35
36 // Calculate count of valid numbers from 0 to (num1 - 1)
37 // This gives us the range [num1, num2] when subtracted from countUpToNum2
38 currentNumber = new BigInteger(num1).subtract(BigInteger.ONE).toString();
39 dp = new Integer[23][220]; // Reset memoization table
40 int countUpToNum1Minus1 = digitDP(0, 0, true);
41
42 // Return the difference (with proper modulo handling)
43 return (countUpToNum2 - countUpToNum1Minus1 + MOD) % MOD;
44 }
45
46 /**
47 * Digit DP recursive function to count valid numbers
48 * @param position Current digit position being processed
49 * @param digitSum Current sum of digits selected so far
50 * @param isLimit Whether we're still bounded by the original number's digits
51 * @return Count of valid numbers from this state
52 */
53 private int digitDP(int position, int digitSum, boolean isLimit) {
54 // Base case: processed all digits
55 if (position >= currentNumber.length()) {
56 // Check if the digit sum falls within the required range
57 return (digitSum >= minSum && digitSum <= maxSum) ? 1 : 0;
58 }
59
60 // Use memoization only when not limited by the original number
61 // (limit states are unique to specific paths and shouldn't be cached)
62 if (!isLimit && dp[position][digitSum] != null) {
63 return dp[position][digitSum];
64 }
65
66 int result = 0;
67
68 // Determine the upper bound for the current digit
69 // If limited, can't exceed the corresponding digit in the original number
70 int upperBound = isLimit ? (currentNumber.charAt(position) - '0') : 9;
71
72 // Try all possible digits from 0 to upperBound
73 for (int digit = 0; digit <= upperBound; ++digit) {
74 // Recursively count valid numbers
75 // Remain limited only if we chose the maximum possible digit
76 result = (result + digitDP(position + 1,
77 digitSum + digit,
78 isLimit && (digit == upperBound))) % MOD;
79 }
80
81 // Cache the result only for non-limit states
82 if (!isLimit) {
83 dp[position][digitSum] = result;
84 }
85
86 return result;
87 }
88}
89
1class Solution {
2public:
3 int count(string num1, string num2, int min_sum, int max_sum) {
4 const int MOD = 1e9 + 7;
5
6 // dp[position][sum] = count of valid numbers
7 // -1 indicates uncomputed state
8 int dp[23][220];
9 memset(dp, -1, sizeof(dp));
10
11 string currentNum = num2;
12
13 // Digit DP function to count numbers with digit sum in [min_sum, max_sum]
14 // pos: current position in the number string
15 // digitSum: sum of digits so far
16 // isLimit: whether we're still bounded by the original number's digits
17 function<int(int, int, bool)> digitDP = [&](int pos, int digitSum, bool isLimit) -> int {
18 // Base case: reached end of number
19 if (pos >= currentNum.size()) {
20 return (digitSum >= min_sum && digitSum <= max_sum) ? 1 : 0;
21 }
22
23 // Use memoization when not limited by original number
24 if (!isLimit && dp[pos][digitSum] != -1) {
25 return dp[pos][digitSum];
26 }
27
28 // Determine upper bound for current digit
29 int upperBound = isLimit ? (currentNum[pos] - '0') : 9;
30
31 // Try all possible digits at current position
32 int result = 0;
33 for (int digit = 0; digit <= upperBound; ++digit) {
34 // Recursively count valid numbers
35 result += digitDP(pos + 1, digitSum + digit, isLimit && (digit == upperBound));
36 result %= MOD;
37 }
38
39 // Store result in dp table if not limited
40 if (!isLimit) {
41 dp[pos][digitSum] = result;
42 }
43
44 return result;
45 };
46
47 // Count valid numbers from 0 to num2
48 int countUpToNum2 = digitDP(0, 0, true);
49
50 // Decrement num1 by 1 to get num1 - 1
51 for (int i = num1.size() - 1; i >= 0; --i) {
52 if (num1[i] == '0') {
53 num1[i] = '9';
54 } else {
55 num1[i] -= 1;
56 break;
57 }
58 }
59
60 // Reset for new calculation
61 currentNum = num1;
62 memset(dp, -1, sizeof(dp));
63
64 // Count valid numbers from 0 to (num1 - 1)
65 int countUpToNum1Minus1 = digitDP(0, 0, true);
66
67 // Return count of valid numbers in range [num1, num2]
68 return (countUpToNum2 - countUpToNum1Minus1 + MOD) % MOD;
69 }
70};
71
1/**
2 * Counts integers between num1 and num2 (inclusive) whose digit sum is between min_sum and max_sum
3 * Uses digit dynamic programming technique
4 * @param num1 - Lower bound as string
5 * @param num2 - Upper bound as string
6 * @param min_sum - Minimum allowed digit sum
7 * @param max_sum - Maximum allowed digit sum
8 * @returns Count of valid numbers modulo 10^9 + 7
9 */
10function count(num1: string, num2: string, min_sum: number, max_sum: number): number {
11 const MOD = 1e9 + 7;
12
13 // Memoization table: dp[position][digitSum]
14 // Maximum 23 digits (for very large numbers) and maximum digit sum of 220 (safety margin)
15 const memoTable: number[][] = Array.from({ length: 23 }, () => Array(220).fill(-1));
16
17 // Current number being processed
18 let currentNumber = num2;
19
20 /**
21 * Recursive function to count valid numbers using digit DP
22 * @param position - Current digit position being processed
23 * @param digitSum - Sum of digits chosen so far
24 * @param isLimit - Whether we're still bounded by the original number's digits
25 * @returns Count of valid numbers from this state
26 */
27 const countValidNumbers = (position: number, digitSum: number, isLimit: boolean): number => {
28 // Base case: processed all digits
29 if (position >= currentNumber.length) {
30 // Check if digit sum is within required range
31 return digitSum >= min_sum && digitSum <= max_sum ? 1 : 0;
32 }
33
34 // Use memoization when not limited by upper bound
35 if (!isLimit && memoTable[position][digitSum] !== -1) {
36 return memoTable[position][digitSum];
37 }
38
39 let result = 0;
40 // Determine upper bound for current digit
41 const upperBound = isLimit ? Number(currentNumber[position]) : 9;
42
43 // Try all possible digits at current position
44 for (let digit = 0; digit <= upperBound; digit++) {
45 result = (result + countValidNumbers(
46 position + 1,
47 digitSum + digit,
48 isLimit && digit === upperBound
49 )) % MOD;
50 }
51
52 // Store result in memoization table if not limited
53 if (!isLimit) {
54 memoTable[position][digitSum] = result;
55 }
56
57 return result;
58 };
59
60 // Count valid numbers from 0 to num2
61 const countUpToNum2 = countValidNumbers(0, 0, true);
62
63 // Calculate num1 - 1 to get count from 0 to num1-1
64 currentNumber = (BigInt(num1) - 1n).toString();
65
66 // Reset memoization table for new calculation
67 memoTable.forEach(row => row.fill(-1));
68
69 // Count valid numbers from 0 to num1-1
70 const countUpToNum1Minus1 = countValidNumbers(0, 0, true);
71
72 // Return difference to get count in range [num1, num2]
73 return (countUpToNum2 - countUpToNum1Minus1 + MOD) % MOD;
74}
75
Time and Space Complexity
Time Complexity: O(n * S * d)
where n
is the length of the longer number between num1
and num2
, S
is the maximum possible sum of digits (which is 9 * n
), and d
is the number of possible digits (10).
The DFS function explores states defined by:
pos
: position in the number (0 to n-1)s
: current sum of digits (0 to 9*n)limit
: boolean flag
For each state, we iterate through at most 10 digits (0-9). Due to memoization with @cache
, each unique state is computed only once. The total number of unique states is n * (9n + 1) * 2 = O(n²)
. Since we iterate through up to 10 digits for each state, the overall time complexity is O(10 * n²) = O(n²)
.
However, more precisely, the time complexity is O(n * S)
where S
represents the range of possible sums, which is bounded by 9n
. Since we call the function twice (once for num2
and once for num1 - 1
), the complexity remains O(n * S)
.
Space Complexity: O(n * S)
where n
is the length of the number and S
is the maximum possible sum.
The space is used for:
- The memoization cache storing results for each unique
(pos, s, limit)
combination - The recursion stack depth which is at most
n
The cache stores at most n * (9n + 1) * 2
states, which simplifies to O(n²)
or more precisely O(n * S)
space. The recursion stack adds O(n)
space, but this is dominated by the cache size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of num1 - 1 Leading to Edge Cases
The Pitfall:
When computing str(int(num1) - 1)
, there's a critical edge case when num1 = "0"
. This results in int("0") - 1 = -1
, which becomes "-1"
as a string. The digit DP function isn't designed to handle negative numbers, leading to incorrect results or unexpected behavior.
Example:
num1 = "0"
num1_minus_one = str(int(num1) - 1) # Results in "-1"
# The DP function will process "-1" incorrectly
Solution:
Add a special check for when num1 = "0"
:
if num1 == "0":
# Special case: when num1 is 0, we don't need to subtract anything
count_up_to_num1_minus_one = 0
else:
num1_minus_one = str(int(num1) - 1)
count_up_to_num1_minus_one = digit_dp(0, 0, True, num1_minus_one)
2. Cache Pollution Between Different Problem Instances
The Pitfall:
The @cache
decorator maintains state between different calls to the count
method. If the Solution class instance is reused for multiple test cases, the cache from previous computations can interfere with new ones, especially since the parameters min_sum
and max_sum
are used inside the cached function but aren't part of the cache key.
Example:
sol = Solution() # First call with min_sum=5, max_sum=10 result1 = sol.count("1", "100", 5, 10) # Second call with different min_sum/max_sum result2 = sol.count("1", "100", 3, 8) # May use cached values with wrong sum bounds!
Solution:
Either include all relevant parameters in the function signature or clear the cache at the beginning of each count
call:
def count(self, num1: str, num2: str, min_sum: int, max_sum: int) -> int:
from functools import cache
MOD = 10**9 + 7
@cache
def digit_dp(position: int, digit_sum: int, is_limit: bool,
current_number: str, min_s: int, max_s: int) -> int:
# Include min_s and max_s as parameters
if position >= len(current_number):
return 1 if min_s <= digit_sum <= max_s else 0
# ... rest of the function
# Pass min_sum and max_sum explicitly
count_up_to_num2 = digit_dp(0, 0, True, num2, min_sum, max_sum)
3. Integer Overflow in Modulo Operation
The Pitfall:
When computing (count_up_to_num2 - count_up_to_num1_minus_one) % MOD
, if count_up_to_num1_minus_one > count_up_to_num2
, the subtraction results in a negative number. In Python, this isn't typically an issue as the modulo operation handles negatives correctly, but in other languages or when porting this solution, it can cause problems.
Example:
# If count_up_to_num2 = 5 and count_up_to_num1_minus_one = 8 result = (5 - 8) % MOD # In Python: gives correct positive result # In some other languages: might give negative result
Solution: Ensure the result is always positive by adding MOD before taking modulo:
return ((count_up_to_num2 - count_up_to_num1_minus_one) % MOD + MOD) % MOD
Or more elegantly:
return (count_up_to_num2 - count_up_to_num1_minus_one + MOD) % MOD
Which data structure is used to implement recursion?
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!