Digit DP
Key Points
- •Digit DP is a dynamic programming technique for counting or aggregating values over all integers in a range that satisfy a digit-based property.
- •It builds numbers from the most significant digit to the least, carrying a 'tight' flag to respect the upper bound and a 'leading zeros' flag to avoid false contributions.
- •The core state is typically dp[pos][tight][leading][state], where 'state' encodes your property (e.g., sum so far, remainder modulo m, previous digit, or a digit-used mask).
- •To answer a range query [L, R], compute F(R) − F(L − 1), where F(x) is the DP result over [0, x].
- •Transitions iterate over the next digit d from 0 to (tight ? boun : 9) and update the state accordingly.
- •Time complexity is roughly O(numbe_digits × stat × 10), which is practical for up to 10^18 with careful state design.
- •Handle leading zeros carefully: they should not trigger constraints like 'digit used' or 'adjacent equal' until the first nonzero digit appears.
- •Digit DP is versatile: it can count valid numbers, compute sums like sum of digits over a range, or enforce constraints like divisibility, uniqueness, or adjacency conditions.
Prerequisites
- →Dynamic Programming (DP) fundamentals — Digit DP builds on the idea of overlapping subproblems and memoization.
- →Base representation of integers — Understanding how numbers decompose into digits is core to designing states and transitions.
- →Recursion and memoization — Typical implementations are top-down recursive with memoized states.
- →Modular arithmetic — Many tasks involve remainders (divisibility) or require modulo to prevent overflow.
- →Time and space complexity analysis — To design feasible states and predict performance for constraints like 10^18.
- →Bitmasking — Digit uniqueness or subset constraints are naturally represented as bitmasks.
- →Handling edge cases — Correctly accounting for leading zeros and whether to include the number 0 is crucial.
Detailed Explanation
Tap terms for definitions01Overview
Digit DP is an algorithmic pattern designed to answer questions about numbers by looking at their digits one by one. Instead of checking every number in a range, which would be too slow, we build numbers digit-by-digit under constraints (like 'no digit 7', 'sum of digits ≤ S', 'no adjacent equal digits'). The trick is to add a 'tight' flag that tells us whether we have already matched the bound's prefix exactly; if so, the next digit is limited to the bound's corresponding digit, otherwise it can be anything from 0 to 9. Another important flag is 'leading zeros', which indicates whether we have started the number yet; constraints should usually ignore leading zeros. You can think of Digit DP as a layered decision process: at each position pos, we choose a digit and move to pos + 1, updating any necessary 'state', like the current remainder modulo m or which digits we've used. We cache (memoize) results for each state so we do not recompute the same subproblem, giving excellent performance even for 19-digit numbers (up to 10^18). The final result for a range [L, R] comes from computing F(R) − F(L − 1), where F(x) summarizes valid numbers from 0 to x. This approach is extremely flexible and turns many 'count numbers with property P in a range' problems into manageable dynamic programs.
02Intuition & Analogies
Imagine you are filling out a form with 19 boxes for digits (enough for numbers up to 10^18). You start at the leftmost box and must obey two rules: (1) at every step, if you have matched the upper bound so far, the next digit cannot exceed the corresponding digit of the bound; (2) you have not 'started' placing a real number until you place the first nonzero digit—leading zeros don't count as real digits for most constraints. Now suppose you're told 'never write the digit 7.' As you go box by box, you just avoid 7. If you’re also told 'the number must be divisible by 13', you carry along the current remainder as you write each digit (like a running checksum). Or if you’re told 'no two adjacent digits can be the same', you remember the last digit you wrote and avoid writing it again next. Because many different paths through these boxes share the same situation—same position, same tightness to the bound, same started/leading-zeros status, and the same extra info (like remainder or previous digit)—you can reuse (memoize) results. This is like remembering that from a certain partially filled form, there are X valid completions no matter how you got there. That reuse is what makes Digit DP fast. Finally, to handle a range [L, R] rather than only an upper bound R, you compute the number of valid forms up to R and subtract those up to L − 1. This cancels out everything below L, leaving exactly the count in [L, R].
03Formal Definition
04When to Use
Use Digit DP when the constraint depends on the decimal (or base-b) digits rather than on the whole integer arithmetically in a nonlocal way. Typical uses include: forbidding certain digits; bounding the sum of digits; controlling adjacency (e.g., no equal neighbors); divisibility by tracking remainder modulo m; and uniqueness via a bitmask of used digits. It is especially effective when the upper bound is large (like up to 10^18), making brute force impossible. Digit DP shines when you need to evaluate many valid completions of a prefix under a tight upper bound, and when the property can be updated locally with each digit placed. It also works well for aggregation beyond counting, such as summing digit sums or weighted values across the valid numbers, by returning structured values (e.g., pairs: count and sum) from the DP. Avoid Digit DP when the property is not digit-local or requires global factors not captured by a compact state—e.g., primality of the whole number without additional optimizations, or constraints that require looking ahead arbitrarily far beyond what a finite state can track. Similarly, it is suboptimal if the base is extremely large or the state space explodes beyond manageable size.
⚠️Common Mistakes
- Mishandling leading zeros: Constraints like 'digit used' or 'adjacent equal' should ignore leading zeros. Keep a 'leading' flag and only apply rules once you place the first nonzero digit. 2) Forgetting the range trick: Always compute Answer([L, R]) = F(R) − F(L − 1). Off-by-one errors here are common; explicitly handle L = 0 (then L − 1 = −1 yields F(−1) = 0).
- Wrong tight update: The next state's tight flag is not simply whether tight was 1; it becomes tight' = (tight == 1 and d == bound_digit_at_pos). 4) Not resetting memoization between different bounds or parameters: The DP cache depends on the bound’s digits and on parameters like m; rebuild or clear memo whenever those change.
- Overflow: Counts up to 10^18 fit in 64-bit signed integers, but aggregated sums (e.g., sum of digits over all numbers up to 10^18) can exceed 64-bit. Use modular arithmetic or wider types appropriately. 6) Counting zero incorrectly: Decide whether '0' should be counted for your property. If your base case excludes 'started = false', you might miss counting 0 when it should be included (e.g., divisible by m). Handle 0 explicitly when needed.
- State explosion: Adding too many dimensions can make the DP slow or memory-heavy. Strive for minimal sufficient state (e.g., use prev digit index 0..9 plus a sentinel 10, or a compact 10-bit mask). 8) Mixing properties without adjusting transitions: When combining properties (e.g., remainder and uniqueness), ensure each transition correctly updates every component of the composite state.
Key Formulas
Range Decomposition
Explanation: To count/aggregate over a range [L, R], compute the cumulative result up to R and subtract the cumulative result up to L − 1. Handle L = 0 by defining F(-1) = 0.
Digit DP Recurrence
Explanation: The value at a state equals the sum over all allowed next digits up to hi (either 9 or the bound's digit, depending on tight). Each digit updates the flags and the property state.
Digit Bound
Explanation: When we have matched the bound so far (tight = 1), the next digit cannot exceed the bound's digit at that position; otherwise any digit 0..9 is allowed.
Remainder Transition
Explanation: When still in leading zeros, placing another zero does not change the numeric value and keeps remainder the same; otherwise update the remainder as usual.
Time Complexity
Explanation: We consider n digit positions, about 10 transitions per state, and a property-specific state space of size |S|. This is efficient even for n up to 19.
Space Complexity (Memoized)
Explanation: Storing memo for each pos, tight (2), leading (2), and property state |S| yields linear space in n times the state space size.
Positional Value
Explanation: The value of a number built from digits is the sum of each digit times its positional base power. Digit DP exploits this positional structure.
Branch Factor
Explanation: At each step, we branch over digits 0..hi. The maximum branching per state is 10 (digits 0 through 9).
Complexity Analysis
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Digit DP: count numbers in [0, x] with no digit 7. 5 // We'll use dp[pos][tight][leading] memoization. 6 7 struct No7DP { 8 string s; // digits of bound x 9 long long dp[20][2][2]; // -1 means uncomputed 10 11 long long dfs(int pos, int tight, int leading) { 12 if (pos == (int)s.size()) { 13 // We've formed a number (possibly 0). It's valid because we never allowed digit 7. 14 return 1; // counts this number 15 } 16 long long &res = dp[pos][tight][leading]; 17 if (res != -1) return res; 18 res = 0; 19 int hi = tight ? (s[pos] - '0') : 9; 20 for (int d = 0; d <= hi; ++d) { 21 if (d == 7) continue; // forbid digit 7 anywhere (including first nonzero) 22 int nextTight = tight && (d == hi); 23 int nextLeading = leading && (d == 0); 24 res += dfs(pos + 1, nextTight, nextLeading); 25 } 26 return res; 27 } 28 29 long long solve(long long x) { 30 if (x < 0) return 0; // empty set 31 s = to_string(x); 32 memset(dp, -1, sizeof(dp)); 33 return dfs(0, 1, 1); 34 } 35 }; 36 37 long long count_range(long long L, long long R) { 38 No7DP solver; 39 return solver.solve(R) - solver.solve(L - 1); 40 } 41 42 int main() { 43 ios::sync_with_stdio(false); 44 cin.tie(nullptr); 45 46 long long L, R; 47 // Example input: L R 48 // e.g., 1 1000 49 if (!(cin >> L >> R)) return 0; 50 cout << count_range(L, R) << "\n"; 51 return 0; 52 } 53
We count all numbers up to x that avoid digit 7 by recursively placing digits from most significant to least significant. The tight flag ensures we never exceed x, and the leading flag allows leading zeros. Since we always skip d = 7, the base case returns 1 for any completed number (including 0). The final range answer is solve(R) − solve(L − 1).
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Digit DP: count numbers in [0, x] that are divisible by m. 5 // We do not count the number 0 inside the DP (started==false) to avoid accidental inclusion; 6 // we'll add it manually if needed (i.e., if 0 in range and 0 % m == 0 which is always true). 7 8 struct DivisibleDP { 9 string s; 10 int m; 11 // dp[pos][tight][leading][rem] = count 12 vector<vector<vector<vector<long long>>>> dp; 13 14 long long dfs(int pos, int tight, int leading, int rem) { 15 if (pos == (int)s.size()) { 16 // Valid if we've actually formed a number (not all leading zeros) and remainder is 0. 17 return (!leading && rem == 0) ? 1LL : 0LL; 18 } 19 long long &res = dp[pos][tight][leading][rem]; 20 if (res != -1) return res; 21 res = 0; 22 int hi = tight ? (s[pos] - '0') : 9; 23 for (int d = 0; d <= hi; ++d) { 24 int nextTight = tight && (d == hi); 25 int nextLeading = leading && (d == 0); 26 int nextRem = rem; 27 if (!nextLeading) { 28 // If we've started (i.e., not all zeros), update remainder with this digit 29 nextRem = ( (leading ? 0 : rem) * 10 + d ) % m; // works either way 30 } 31 res += dfs(pos + 1, nextTight, nextLeading, nextRem); 32 } 33 return res; 34 } 35 36 long long solve(long long x, int mod) { 37 if (x < 0) return 0; 38 m = mod; 39 s = to_string(x); 40 dp.assign(s.size(), vector<vector<vector<long long>>>(2, vector<vector<long long>>(2, vector<long long>(m, -1)))); 41 return dfs(0, 1, 1, 0); 42 } 43 }; 44 45 long long count_divisible_in_range(long long L, long long R, int m) { 46 DivisibleDP solver; 47 long long res = solver.solve(R, m) - solver.solve(L - 1, m); 48 // Include 0 explicitly if it's in [L, R] 49 if (L <= 0 && 0 <= R) res += 1; // 0 is divisible by m 50 return res; 51 } 52 53 int main() { 54 ios::sync_with_stdio(false); 55 cin.tie(nullptr); 56 57 long long L, R; int m; 58 // Example input: L R m 59 // e.g., 1 1000000 13 60 if (!(cin >> L >> R >> m)) return 0; 61 cout << count_divisible_in_range(L, R, m) << "\n"; 62 return 0; 63 } 64
We track the remainder modulo m as part of the state. Leading zeros are treated specially: while still leading, placing 0 does not update the remainder. The base case counts only numbers that have started (not all zeros) and have remainder 0. We then add 0 separately if the interval includes it.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Compute Sum_{x in [L,R]} sum_of_digits(x) modulo MOD via Digit DP aggregator. 5 // The DP returns a pair {count, sum} for [0, bound]. We include 0 naturally (count=1, sum=0 at base). 6 7 static const long long MOD = 1000000007LL; 8 9 struct Node { long long cnt, sum; }; // both modulo MOD 10 11 struct SumDigitsDP { 12 string s; 13 Node dp[20][2][2]; 14 bool vis[20][2][2]; 15 16 Node add(Node a, Node b) { 17 return Node{ (a.cnt + b.cnt) % MOD, (a.sum + b.sum) % MOD }; 18 } 19 20 Node dfs(int pos, int tight, int leading) { 21 if (pos == (int)s.size()) { 22 // One completed number (including 0); sum of digits contributes 0 here 23 return Node{1, 0}; 24 } 25 if (vis[pos][tight][leading]) return dp[pos][tight][leading]; 26 vis[pos][tight][leading] = true; 27 Node res{0, 0}; 28 int hi = tight ? (s[pos] - '0') : 9; 29 for (int d = 0; d <= hi; ++d) { 30 int nextTight = tight && (d == hi); 31 int nextLeading = leading && (d == 0); 32 Node child = dfs(pos + 1, nextTight, nextLeading); 33 // If not leading (we have started) or d != 0 starts the number, the placed digit contributes d * (#numbers formed by the rest) 34 if (!nextLeading) { 35 // We placed a real digit at this position; its contribution repeats for all completions of the suffix 36 child.sum = (child.sum + (child.cnt * d) % MOD) % MOD; 37 } 38 res = add(res, child); 39 } 40 return dp[pos][tight][leading] = res; 41 } 42 43 long long solve(long long x) { 44 if (x < 0) return 0; 45 s = to_string(x); 46 memset(vis, 0, sizeof(vis)); 47 Node ans = dfs(0, 1, 1); 48 return ans.sum % MOD; 49 } 50 }; 51 52 long long sum_digits_range(long long L, long long R) { 53 SumDigitsDP solver; 54 long long res = (solver.solve(R) - solver.solve(L - 1)) % MOD; 55 if (res < 0) res += MOD; 56 return res; 57 } 58 59 int main() { 60 ios::sync_with_stdio(false); 61 cin.tie(nullptr); 62 63 long long L, R; 64 // Example input: L R 65 // e.g., 0 1000000000000000000 66 if (!(cin >> L >> R)) return 0; 67 cout << sum_digits_range(L, R) << "\n"; 68 return 0; 69 } 70
This aggregator DP returns both the count of completions and the sum of digits over all completions. When adding a digit d at position pos, that digit contributes d times the number of ways to fill the suffix. We use modulo arithmetic to avoid overflow and include 0 by returning {1, 0} at the base case.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Digit DP: count numbers in [0, x] with no two adjacent equal digits. 5 // State adds 'prev' digit (0..9) and a sentinel 10 for 'no previous yet'. 6 7 struct NoAdjacentEqualDP { 8 string s; 9 long long dp[20][2][2][11]; // pos, tight, leading, prev(0..10) 10 11 long long dfs(int pos, int tight, int leading, int prev) { 12 if (pos == (int)s.size()) { 13 // We count 1 for every formed number, including 0. A single-digit 0 trivially has no adjacent equals. 14 return 1; 15 } 16 long long &res = dp[pos][tight][leading][prev]; 17 if (res != -1) return res; 18 res = 0; 19 int hi = tight ? (s[pos] - '0') : 9; 20 for (int d = 0; d <= hi; ++d) { 21 int nextTight = tight && (d == hi); 22 int nextLeading = leading && (d == 0); 23 int nextPrev = prev; 24 if (!nextLeading) { 25 // We're placing a real digit 26 if (leading) { 27 // First real digit; no adjacency check yet 28 nextPrev = d; 29 } else { 30 if (d == prev) continue; // violates 'no adjacent equal' 31 nextPrev = d; 32 } 33 } else { 34 // Still leading zeros; keep prev as sentinel 10 35 nextPrev = 10; 36 } 37 res += dfs(pos + 1, nextTight, nextLeading, nextPrev); 38 } 39 return res; 40 } 41 42 long long solve(long long x) { 43 if (x < 0) return 0; 44 s = to_string(x); 45 for (int i = 0; i < 20; ++i) 46 for (int a = 0; a < 2; ++a) 47 for (int b = 0; b < 2; ++b) 48 for (int c = 0; c < 11; ++c) 49 dp[i][a][b][c] = -1; 50 return dfs(0, 1, 1, 10); // 10 = sentinel for 'no previous' 51 } 52 }; 53 54 long long count_no_adjacent_equal_in_range(long long L, long long R) { 55 NoAdjacentEqualDP solver; 56 return solver.solve(R) - solver.solve(L - 1); 57 } 58 59 int main() { 60 ios::sync_with_stdio(false); 61 cin.tie(nullptr); 62 63 long long L, R; 64 // Example input: L R 65 // e.g., 1 1000 66 if (!(cin >> L >> R)) return 0; 67 cout << count_no_adjacent_equal_in_range(L, R) << "\n"; 68 return 0; 69 } 70
We keep track of the previous placed digit (prev) with a sentinel 10 meaning 'none yet'. While still in leading zeros, we do not apply the adjacency constraint. Once started, we reject any transition where the new digit equals prev. The DP counts 0 as valid; the range difference handles [L, R].