Digit DP
Digit DP is the umbrella name for a family of dynamic-programming techniques designed to count (or sum, or compute statistics over) integers in a range
[L, R]satisfying some digit-related propertyP. The property typically depends on the number’s individual digits — the count of a particular digit, the sum of digits, divisibility, the presence/absence of a substring of digits, equality to a palindrome, or any other constraint expressible position-by-position. Naively iterating over[L, R]isΘ(R − L), fine forR = 10^6but disastrous forR = 10^{18}. Digit DP decomposes the problem digit-by-digit, building integers one position at a time while tracking three pieces of state: (1) the position in the number being built, (2) a “tight” flag indicating whether the prefix so far is still equal to the corresponding prefix ofR(the upper bound), and (3) any auxiliary state the propertyPrequires (digit-sum-modulo-k, last digit, count of a specific digit, etc.). The complexity is typicallyO(d · S · 10)whered = ⌊log₁₀(R)⌋ + 1is the number of digits andSis the size of the auxiliary state — utterly polynomial inlog Rrather than linear inR. The technique is mostly seen in competitive programming (Codeforces, AtCoder, ICPC) and shows up in advanced LeetCode problems (LC 233, LC 600, LC 902, LC 1067, LC 2376, LC 2719). The conceptual hook is the tight flag: once the prefix has gone “below” the upper bound at any position, all subsequent digits are unconstrained and can be 0–9 freely; until then, the current digit is constrained to 0..R’s-digit-at-this-position, and choosing exactly that digit keeps the tight flag set. Mastering this single-bit state-machine is the difference between a solution that runs in milliseconds and a brute force that doesn’t terminate.
1. Intuition — Build the Number Digit-by-Digit, From the Most-Significant End
Suppose R = 4567 and you want to count numbers in [0, R] with some property P (e.g., “digit sum is a multiple of 3”). The naive iteration over 4568 numbers is fine here, but if R = 10^{18} it is not. Digit DP says: instead of generating numbers, generate them digit-by-digit, from the most significant to the least significant, and track enough state to know how many ways the remaining digits can be filled to satisfy P.
The first decision is “what’s the leading digit?” If R = 4567, the first digit can be 0, 1, 2, 3, 4. If we pick 0, 1, 2, or 3, then the remaining three digits can be anything (000 to 999) and we are no longer constrained by R. If we pick 4, then the next digit is constrained: it can only be 0, 1, 2, 3, 4, 5 (because R’s next digit is 5). And so on, recursively.
This dichotomy — “are we still hugging the upper bound, or have we already gone strictly below?” — is captured by a single boolean state called the tight flag. When tight, the current position’s digit ranges over 0 to R_pos (the digit of R at this position). When not tight, it ranges freely over 0..9. Tightness can only break in one direction: once not-tight, always not-tight. This is what makes the state space tractable.
A real-world analogy: imagine you are reading off a budget cap of $4567 for a charity drive. As pledges come in, donors call out their pledges digit-by-digit (“first digit?”, “next digit?”). As long as everyone has pledged exactly the same digits as the cap so far, the auctioneer is still constraining the next digit (you can’t bid $5xxx because that exceeds the cap). The moment someone bids less than the cap’s digit at some position (say $3xxx), all subsequent digits are unrestricted — you’ve already saved enough to bid 999 and still be under cap. The “tight flag” is just whether the auction is still at the cap or has gone safely below.
The third piece — the auxiliary state — is whatever extra information the property P requires. For “digit sum mod 3 == 0”, you track the running sum mod 3 (state of size 3). For “no two consecutive digits are the same”, you track the previous digit (state of size 11, including a “no previous digit” sentinel). For “doesn’t contain the substring 13”, you track the last digit (state of size 11) plus a “have we seen 13 already?” flag (state of size 2). The auxiliary state’s size determines the complexity, and the art of digit DP is finding the minimal state that encodes everything you need to know.
The computation runs as a recursion over positions, with a memo keyed on (position, tight flag, auxiliary state). For typical problems, this gives O(d · 2 · S · 10) time, where d is the digit count, S is the auxiliary state cardinality, and 10 is the per-position branching factor. For R = 10^{18} and S = 100, that’s ~3.6 × 10^4 operations — essentially free.
2. The Tight-Flag Mechanics
The tight flag is the conceptual heart. Walk through it once and it’s permanently understood.
Let R = d_0 d_1 d_2 ... d_{n-1} be R written digit-by-digit, most significant first. We construct numbers x = x_0 x_1 ... x_{n-1} (also n digits, with leading zeros if x < 10^{n-1}) one digit at a time. At position i, we choose x_i.
The constraint: x ≤ R. Equivalently:
- Either there exists some position
j < iwherex_j < d_j(andx_k = d_kfor allk < j), in which case any choice ofx_iis valid. - Or
x_k = d_kfor allk < i(we have been huggingRso far), in which casex_i ≤ d_i. Ifx_i < d_i, we go “below” and all subsequent positions are free. Ifx_i = d_i, we stay tight.
The tight flag at position i summarizes case 2 vs case 1: tight = 1 means we are still in case 2 (no position has yet “gone below”); tight = 0 means we are in case 1 (some prior position went below, all is free).
Transitions:
- If
tight = 0(already free):x_i ∈ {0, 1, ..., 9}, andtightremains 0. - If
tight = 1:- If
x_i < d_i:tightflips to 0 for the next position. - If
x_i = d_i:tightremains 1. - If
x_i > d_i: invalid (would exceed R).
- If
Equivalently: new_tight = tight AND (x_i == d_i). The tight flag can only flip from 1 to 0 across positions; never the other way.
This gives the DP its tractability: the tight dimension is just a single bit, doubling the state size by a factor of 2 — negligible. The position dimension is d ≈ log₁₀ R, and digit choices at each position are at most 10. The total work is dominated by the auxiliary-state size.
3. Worked Example — Count Integers in [0, N] With Digit Sum Equal to k
Concrete formulation: given N (up to 10^{18}) and k (up to ~200), count how many integers in [0, N] have digit sum exactly k.
3.1 State Design
- Position
pos: which digit we are filling (0-indexed from the most significant).pos ∈ [0, d]wheredis the digit count ofN. - Tight flag
tight:1if the prefix equalsN’s prefix,0otherwise. - Auxiliary state
sum_so_far: the sum of digits placed so far,sum ∈ [0, k](we can prune anything >ksince the answer can’t recover).
State count: d × 2 × (k+1). For N = 10^{18}, k = 200: 19 × 2 × 201 ≈ 7600 states. Each transitions to at most 10 child states. Total work: ~76,000 — instant.
3.2 Recurrence
count(pos, tight, sum_so_far) = number of ways to fill positions pos, pos+1, ..., d-1 such that:
- The remaining digits respect the tight constraint relative to
N. - The total digit sum equals
k.
count(pos, tight, sum_so_far):
if pos == d:
return 1 if sum_so_far == k else 0
upper := digit_of_N_at_pos[pos] if tight else 9
total := 0
for digit in 0..upper:
new_tight := tight and (digit == upper)
new_sum := sum_so_far + digit
if new_sum > k: # prune: sum can't decrease
continue
total += count(pos + 1, new_tight, new_sum)
return total
Symbol-by-symbol unpacking:
posis the current digit position (0-indexed from MSB).tightis the tight flag at this position; if 1, we’re still hugging N; if 0, we’re free.sum_so_faris the sum of digits already placed.upperis the maximum allowed digit at this position:N’s digit if tight,9if not.- For each candidate digit,
new_tight = tight AND (digit == upper)(the conjunction: both must be true to remain tight). new_sum = sum_so_far + digitis the running total updated.- Recurse and sum.
3.3 Tiny Numerical Trace
N = 24, k = 6. Numbers in [0, 24] with digit sum = 6: let’s enumerate by hand: 06, 15, 24 → three numbers (treating 06 as 6 → digit sum 6, valid; 15 → 1+5=6; 24 → 2+4=6). So expected count = 3.
DP trace (N = 24, so d = 2, digits [2, 4], k = 6):
Start: count(pos=0, tight=1, sum=0).
At pos 0, tight, upper = 2. Try digit ∈ {0, 1, 2}:
digit = 0: new_tight = (1 AND 0 == 2) = (1 AND 0) = 0. new_sum = 0. Recursecount(1, 0, 0).digit = 1: new_tight = (1 AND 1 == 2) = 0. new_sum = 1. Recursecount(1, 0, 1).digit = 2: new_tight = (1 AND 2 == 2) = 1. new_sum = 2. Recursecount(1, 1, 2).
count(1, 0, 0): pos 1, free, sum 0. Upper = 9. Try digit ∈ {0, …, 9}:
- We need final sum
= 6. So digit must be6. digit = 6: new_sum = 6. Recursecount(2, 0, 6)→ returns 1 (base case: sum == k). All other digits at this level give sum ≠ 6 → return 0.- Total: 1. (This corresponds to the number
06 = 6.)
count(1, 0, 1): pos 1, free, sum 1. Need digit = 5 to reach sum 6.
digit = 5: returns 1. (This is15.)- Total: 1.
count(1, 1, 2): pos 1, tight, sum 2. Upper = 4 (digit of N at position 1). Try digit ∈ {0, …, 4}:
digit = 4: new_sum = 6. Returns 1. (This is24.)- Other digits: sum ∈ {2, 3, 4, 5} ≠ 6, return 0.
- Total: 1.
Sum at root: 1 + 1 + 1 = 3. ✓
The DP found exactly 3, matching the brute-force enumeration.
3.4 Memoization Subtlety
Memoization keys: (pos, tight, sum_so_far). The crucial subtlety: memoize only when tight == 0. When tight == 1, the upper-bound digit at this position depends on N, which is fixed for the run — so technically tight == 1 states are also legitimate to cache. But the canonical idiom is to cache only tight == 0 because (a) those states are reused across the recursion (many paths lead to the same “free” sub-tree), while (b) tight == 1 states are visited at most once per position.
Both forms are correct; the “cache only tight == 0” form is slightly more memory-efficient. Either works for the asymptotic.
4. Pseudocode
DigitDP_DigitSum(N, k):
digits := decimal digits of N (most significant first), length d
memo := map from (pos, tight, sum) to count
function count(pos, tight, sum):
if pos == d:
return 1 if sum == k else 0
if tight == 0 and (pos, tight, sum) in memo:
return memo[(pos, tight, sum)]
upper := digits[pos] if tight else 9
total := 0
for digit := 0 to upper:
new_tight := tight and (digit == upper)
new_sum := sum + digit
if new_sum > k: continue # prune
total += count(pos + 1, new_tight, new_sum)
if tight == 0:
memo[(pos, tight, sum)] = total
return total
return count(0, tight=1, sum=0)
For range queries [L, R], compute f(R) - f(L - 1) where f(x) = count of valid integers in [0, x]. This converts a range query to two prefix queries via inclusion-exclusion.
5. Python Implementation
from functools import lru_cache
def count_in_range_with_digit_sum(N: int, k: int) -> int:
"""Count integers in [0, N] whose digit sum equals k."""
digits = list(map(int, str(N)))
d = len(digits)
@lru_cache(maxsize=None)
def helper(pos: int, tight: int, sum_so_far: int) -> int:
if pos == d:
return 1 if sum_so_far == k else 0
upper = digits[pos] if tight else 9
total = 0
for digit in range(upper + 1):
new_tight = 1 if (tight and digit == upper) else 0
new_sum = sum_so_far + digit
if new_sum > k:
continue
total += helper(pos + 1, new_tight, new_sum)
return total
return helper(0, 1, 0)
# Test
print(count_in_range_with_digit_sum(24, 6)) # 3
print(count_in_range_with_digit_sum(100, 5)) # 6 (5, 14, 23, 32, 41, 50) -> 6
print(count_in_range_with_digit_sum(10**18, 100)) # large, fastA subtle point: lru_cache keys on the full argument tuple including tight, so it caches both tight == 0 and tight == 1 states. This is fine — the total number of cached states is bounded by d · 2 · (k+1), which is tiny.
5.1 Iterative Tabulation Form
Recursive memoization is the most natural way to write digit DP. An iterative form is possible but uglier:
def count_in_range_with_digit_sum_iter(N: int, k: int) -> int:
digits = list(map(int, str(N)))
d = len(digits)
# dp[pos][tight][sum] for pos in [0, d], tight in {0, 1}, sum in [0, k]
dp = [[[0] * (k + 1) for _ in range(2)] for _ in range(d + 1)]
# Base case: pos == d
for tight in range(2):
for s in range(k + 1):
dp[d][tight][s] = 1 if s == k else 0
# Fill backward
for pos in range(d - 1, -1, -1):
for tight in range(2):
for s in range(k + 1):
upper = digits[pos] if tight else 9
total = 0
for digit in range(upper + 1):
new_tight = 1 if (tight and digit == upper) else 0
new_sum = s + digit
if new_sum > k:
continue
total += dp[pos + 1][new_tight][new_sum]
dp[pos][tight][s] = total
return dp[0][1][0]Most digit-DP implementations use the recursive form; tabulation offers no speedup and is messier.
5.2 Range [L, R] via Subtraction
def count_in_range(L: int, R: int, k: int) -> int:
return count_in_range_with_digit_sum(R, k) - count_in_range_with_digit_sum(L - 1, k)This is the standard idiom: compute “count up to R” and “count up to L-1” and subtract. Works because the property is closed under prefix-counting.
6. Worked Example — Count Numbers Without Substring 49 (Codeforces 55D Variant)
Property: count integers in [0, N] whose decimal representation does not contain the substring 49.
6.1 State Design
pos: position (0 to d).tight: 0/1.last_digit: 0..9 plus a “no previous digit” sentinel. To avoid dealing with the sentinel separately, we uselast_digit = -1initially or we reserve10as the sentinel.seen_49: boolean (1 if we’ve already placed49in the number; 0 otherwise).
We could also drop seen_49 and abort early (return 0) the moment we see 49. Both formulations work.
6.2 Recurrence
count(pos, tight, last_digit):
if pos == d: return 1
upper := digits[pos] if tight else 9
total := 0
for digit in 0..upper:
if last_digit == 4 and digit == 9: continue # would form "49"
new_tight := tight and (digit == upper)
total += count(pos + 1, new_tight, digit)
return total
The last_digit == 4 and digit == 9 check enforces the “no 49” constraint. State size: d × 2 × 10. For d = 19, total ~380 states — extremely small.
6.3 Python
def count_no_49_substring(N: int) -> int:
digits = list(map(int, str(N)))
d = len(digits)
@lru_cache(maxsize=None)
def helper(pos: int, tight: int, last_digit: int) -> int:
if pos == d:
return 1
upper = digits[pos] if tight else 9
total = 0
for digit in range(upper + 1):
if last_digit == 4 and digit == 9:
continue
new_tight = 1 if (tight and digit == upper) else 0
total += helper(pos + 1, new_tight, digit)
return total
# Use 10 as the "no previous digit" sentinel
return helper(0, 1, 10)The pattern is identical to §5; only the auxiliary state and the constraint differ.
7. Variants and Extensions
7.1 Sum of Property Values, Not Just Count
If the question is “sum the property P(x) over all x in [L, R],” the recurrence returns a sum instead of a count. Each leaf returns P(x) for the specific x constructed; internal nodes sum the children. Sometimes you need to track an auxiliary “the sum of remaining-digits is currently s” running variable to compute the final value.
7.2 Digit DP With Leading Zeros
Sometimes the problem distinguishes between numbers with leading zeros (e.g., 5-digit number 00123 = 123) and the actual integer. Add a started flag to the state: started == 0 means we have not placed any non-zero digit yet. This is sometimes needed to exclude the number 0 itself or to count digits correctly.
7.3 Modular Constraints
“Count integers in [L, R] divisible by m whose digit sum is k mod m'”: auxiliary state is (value mod m, digit_sum mod m'). Each state transition multiplies the running value by 10 and adds the new digit, then takes mod. Doable for small m, m' (each adds a multiplicative factor of m to the state size).
7.4 Counting via Combinatorial Pre-computation
For specific properties, you can precompute combinatorial quantities (e.g., binomial coefficients, generating-function values) and combine them at each tight-bound position. This is sometimes faster than the full DP but problem-specific.
7.5 Digit DP Over Bases Other Than 10
Replace 10 with base everywhere. The technique works for binary, hexadecimal, etc. In binary, the per-position branching is 2 instead of 10, often making the DP much smaller. In hexadecimal, it’s 16.
7.6 Multi-Number Digit DP
Some problems ask “count pairs (a, b) with 0 ≤ a, b ≤ N satisfying P”. The state can be enriched to track both numbers’ digits in lockstep. State explodes if both are tight; usually feasible for one or two numbers.
8. Diagram — The Tight-Flag State Machine
flowchart LR Start((Start: tight=1)) -- "digit < N[pos]" --> Free((Free: tight=0)) Start -- "digit == N[pos]" --> Start Free -- "any digit 0-9" --> Free Start -- "digit > N[pos]" --> Invalid((INVALID: x > N)) style Invalid fill:#f99
What this diagram shows. A two-state finite automaton describing the tight flag’s evolution as we move position-by-position through the number being constructed. We start in the tight state (the prefix equals N’s prefix). At each position, choosing a digit less than the corresponding digit of N transitions to the free state, where all subsequent digits are unconstrained. Choosing the digit equal to N’s digit keeps us tight. Choosing a greater digit is invalid — it would produce a number exceeding N, so the recursion never explores this branch. The free state is absorbing: once you’re free, you stay free for all remaining positions. This is what lets us safely memoize the DP — once tight = 0, the same (pos, sum) (or whatever auxiliary state) leads to identical sub-problems regardless of which path led here, so caching is valid. Without the tight flag, every path through the recursion would be unique (the bound R couples positions together); with it, the coupling is reduced to a single bit, making the state space polynomial in log R.
9. Common Interview Problems
| LC # / Source | Problem | State Augmentation |
|---|---|---|
| LC 233 | Count Digit One | Count of digit 1 so far; also closed-form possible |
| LC 600 | Non-negative Integers without Consecutive Ones | Last digit (binary digit DP) |
| LC 902 | Numbers At Most N Given Digit Set | Allowed-digit constraint, tight flag |
| LC 1067 | Digit Count in Range | Specific digit count, range subtraction |
| LC 1397 | Find All Good Strings | Aho-Corasick + tight + state |
| LC 2376 | Count Special Integers | ”All digits distinct” → bitmask of used digits + tight |
| LC 2719 | Count of Integers | Digit sum range, two tight flags (one per bound) |
| LC 357 | Count Numbers with Unique Digits | Combinatorial closed-form, DP also possible |
| Codeforces 55D | Beautiful Numbers (digit-divisibility) | LCM mod 2520 + remainder mod 2520 (clever state compression) |
| AtCoder ABC 154 E | Almost Everywhere Zero | Count of non-zero digits |
| Codeforces 401D | Roman and Numbers | Bitmask of multi-set + value mod m |
| Codeforces 628D | Magic Numbers | Position parity + digit constraint |
The recognition signal: count integers (or sum a function over them) in a huge range [L, R] (typically R ≥ 10^9) where the property depends on digits. If R ≤ 10^7, brute force is fine. Beyond that, digit DP.
10. Pitfalls
10.1 Forgetting to Subtract f(L-1) for Range Queries
Range [L, R] requires f(R) - f(L-1), not f(R) - f(L). Off-by-one if you use f(L) (the value L itself is then excluded).
10.2 Memoizing Across tight = 1 States Incorrectly
If you cache by (pos, sum) only — ignoring tight — you will conflate the constrained and unconstrained sub-problems. The constrained one has fewer choices; the unconstrained one has more. Mixing them up gives the wrong count. Always include tight in the memo key (or only cache tight = 0 and re-execute tight = 1 paths, which is also correct).
10.3 Off-by-One in upper Loop Bound
for digit in range(upper + 1) — inclusive of upper. range(upper) is wrong (skips the upper digit when tight). This is the most common bug in fresh implementations.
10.4 Forgetting Leading-Zero Edge Case
If the problem distinguishes “the number 5” from “the digit string 005” (e.g., counting digits), you need a started flag. Forgetting it counts leading zeros as actual digits and inflates the answer.
10.5 Auxiliary State Too Large
The complexity is dominated by the auxiliary state size. A naive “track the entire digit-sum” state is O(81) for a 9-digit max-sum-81 number — fine. A naive “track all digits placed so far” is O(10^d) — exponential in d, useless. Compress to the minimum sufficient statistic for the property P.
10.6 Mixing 0-Indexed and 1-Indexed Positions
digits[0] is the most significant digit; digits[d-1] is the least. Sometimes the recursion recurses from least-to-most or vice versa. Pick one and document.
10.7 Pruning Too Aggressively
The “if new_sum > k: continue” prune in §3.3 works because the digit sum is monotone. For non-monotone auxiliary states (e.g., value mod m), pruning is unsafe — value mod m can go up and down. Only prune on monotone invariants.
10.8 Forgetting the Base Case Returns 1 (Not Always)
The leaf pos == d typically returns 1 if the property is satisfied at the leaf. But for sum-style queries, it returns the value of the property, not 1. And for “skip leading zeros” queries, you return 1 only if started = 1 (else the number was just 0, possibly excluded). Be precise.
10.9 Confusing tight with loose
Some implementations use loose (the negation of tight) as the flag. Mathematically equivalent, but mixing the two halfway through the implementation produces silently-wrong logic. Pick one and stay consistent.
10.10 Double-Counting When Range Is Inclusive vs Exclusive
If f(x) counts [0, x] inclusive and you want [L, R], use f(R) - f(L-1). If f(x) counts [0, x) exclusive, use f(R+1) - f(L). Off-by-one if you mix conventions.
10.11 lru_cache Key Hashing in Python
For very tight loops (digit DP with S = 10^4 auxiliary states and d = 18), Python’s lru_cache overhead dominates. Switch to manual memoization with a dict keyed on tuples, or — for performance — write in C++. The asymptotic is fine; the constant matters.
11. Variants for Sum of Property
Sometimes you want the sum Σ_{x ∈ [L, R]} P(x) rather than the count |{x : P(x)}|. The recurrence becomes:
sum_dp(pos, tight, ...auxiliary...):
if pos == d:
return value at leaf # the actual value of x as a number, or P(x), etc.
total := 0
for digit in 0..upper:
...
total += sum_dp(pos + 1, new_tight, new_aux)
return total
Computing the value of x requires tracking either (a) the running value mod m (if you want Σ x mod m) or (b) the running value as an integer (if you want Σ x, which requires careful handling at each digit). For “sum of digit sums of all x in [L, R]”, the recurrence accumulates digit sums position-by-position in a clever decomposition.
12. Complexity (Recap)
Time: O(d · 2 · S · base) where:
d ≈ log_base(R)is the number of digits in base.2is the size of the tight flag.Sis the auxiliary state size.baseis 10 (decimal) or 2 (binary), the per-position branching.
For typical interview/competitive problems: d ≤ 19, base = 10, S ≤ 10^4. Total: ~10^6 ops — instantaneous.
Space: O(d · 2 · S) for the memo table.
13. Open Questions
- Is there a clean characterization of “digit-DP-friendly” properties? Properties expressible as a regular language over digits clearly work (the auxiliary state is the DFA’s state). Beyond that, the boundary is fuzzy.
- Can digit DP be parallelized across position prefixes? The recursion has independent sub-trees once tight is broken, suggesting parallelism, but the constants are usually too small to matter.
- What’s the relationship between digit DP and automatic sequences in the Cobham-Allouche-Shallit sense? Both involve finite-state recognition of digit-related properties; the connection is real but not commonly leveraged.
A note on the scope of the term. There is no hard line between “digit DP” and “automaton/regular-expression DP” — the latter is simply digit DP whose auxiliary state is the state of a finite automaton over the digit (or character) stream, rather than a numeric quantity like a running digit sum. LeetCode 1397 Find All Good Strings is the canonical example: it counts strings in a lexicographic range [s1, s2] that avoid an “evil” substring, and the standard solution is plainly described as “digit DP” (here over characters) where the auxiliary state is the KMP automaton index tracking how much of evil has been matched (LeetCode 1397 solutions). For multiple forbidden patterns, the automaton is an Aho-Corasick trie and its current node is the auxiliary state. The unifying principle is that any property recognizable by a finite automaton over digits fits the digit-DP frame — the DFA’s state is the minimal sufficient statistic (see §10.5), and the tight flag plus position dimensions are unchanged. The USACO Guide presents digit DP through purely numeric auxiliary states (digit counts, “started” flags) and does not invoke automata explicitly, but this is a narrower exposition, not a different technique. So the earlier hedge here was overcautious: it is entirely standard to call the LC 1397 family digit DP, and this note’s “property on digits” framing already subsumes the automaton case — the automaton state is just one kind of auxiliary state.
14. See Also
- Memoization vs Tabulation — digit DP is typically memoization (top-down), tabulation is awkward
- DP State Identification — recognizing “count integers with digit property” as digit DP
- 01 Knapsack — different DP family, but the “include/exclude” mental model transfers
- Bitmask DP — sometimes the auxiliary state is itself a bitmask (e.g., LC 2376)
- Bit Manipulation Tricks — relevant when the digit base is 2
- Divide and Conquer DP — sibling advanced-DP technique; speeds up layered partition DPs via argmin monotonicity rather than counting by digits
- Combinations — closed-form alternatives for some digit-counting problems
- Modular Arithmetic — for digit-DP problems with divisibility constraints
- Big-O Notation
- SWE Interview Preparation MOC