Digit DP — Counting Numbers with Properties
A comprehensive walkthrough of the Digit DP technique and LeetCode problems that use it, from the core template to its variants.
Pattern Overview
Trigger: “Count numbers in range [low, high] satisfying some digit property”
All problems share:
- Range reduction:
count(low, high) = count(0, high) - count(0, low - 1) - Build digit by digit from left to right
- Four core parameters:
pos,state,tight,started - Memoization with
lru_cache
Quick Navigation: I need to…
| Symptom in the problem | The state you add |
Go to |
|---|---|---|
“count numbers in [L, R] with digit sum = S / in [a,b]” |
running sum / remaining | 2719 |
| “count numbers with all distinct digits” | 10-bit mask |
2376 |
| “count numbers with at least one repeated digit” | 10-bit mask + complement |
1012 |
“how many times does digit X appear across 1..n” |
occurrence count (return it, not 0/1) |
233 |
| “only these digits are allowed” | none — iterate the allowed set | 902 |
| “sum of odd-position digits = sum of even-position” | diff + length |
3791 |
| “divisible by k AND some digit property” | rem (+ second state) |
2827 |
| “which digit families exist / what state do I pick” | — | §3 Common State Choices, §12 identify router |
“range endpoints are strings too big for int” |
string-subtraction trick | 2719 note |
Table of Contents
- Why This Pattern Exists
- Core Template
- Understanding Each Parameter
- Common State Choices
- Problem 3791 — Balanced Integers in a Range
- Problem 2376 — Count Special Integers
- Problem 233 — Number of Digit One
- Problem 902 — Numbers At Most N Given Digit Set
- Problem 1012 — Numbers With Repeated Digits
- Problem 2719 — Count of Integers (Digit Sum Bounds)
- Problem 2827 — Number of Beautiful Integers
- Master Comparison Table
- How to Identify This Pattern
0. Why This Pattern Exists
The problem: “How many integers in [1, 10^18] have digit sum exactly 30?”
Your first instinct is a loop:
count = 0
for x in range(1, 10**18 + 1): # 10^18 iterations — never finishes
if digit_sum(x) == 30:
count += 1
At a billion iterations/second this takes ~30 years. The range is too big to enumerate. Yet the answer isn’t astronomically large, and most numbers are boring — we’re wasting time re-examining digits we’ve already reasoned about.
The key realization: we don’t care about each number individually. We care about choices, digit by digit. Build the number left to right, one digit at a time. There are only 18 positions, and at each position only 10 possible digits — that’s a tiny decision tree, not 10^18 leaves.
Counting numbers ≤ 316 with some property:
first digit d0
/ | \
0..2 3 (d0 can't exceed 3, the leading digit of 316)
(free) (still "tight" — bounded by 316)
Two numbers that reach position pos with the same running digit-sum and the same freedom (are we still hugging the upper bound 316, or free to use 0–9?) have the identical count of completions. That’s the overlapping-subproblem signal — memoize on (pos, digit_sum_so_far, tight) and the 10^18 problem collapses to a few thousand states.
★ Insight ─────────────────────────────────────
- Digit DP trades “iterate over numbers” for “iterate over prefixes of digits”. 10^18 numbers, but only
18 positions × ~small state × 2 tight-flagsdistinct subproblems. - The whole pattern is: fix the digits left-to-right, remember just enough about the prefix to finish counting, and cache. Everything below is variations on what “just enough” means (a sum, a mask, a remainder…).
─────────────────────────────────────────────────
1. Core Template
from functools import lru_cache
def count(num):
"""Count numbers from 1 to num satisfying the property."""
if num <= 0:
return 0
digits = [int(c) for c in str(num)]
n = len(digits)
@lru_cache(maxsize=None)
def dp(pos, state, tight, started):
"""
pos — current digit position (0 to n-1)
state — problem-specific tracking
tight — are we still bounded by num's digits?
started — have we placed a non-zero digit yet?
"""
if pos == n:
if not started:
return 0 # number is 0, usually excluded
return check(state) # does this number satisfy the property?
limit = digits[pos] if tight else 9
result = 0
for d in range(0, limit + 1):
new_tight = tight and (d == limit)
new_started = started or (d != 0)
new_state = transition(state, d, new_started)
result += dp(pos + 1, new_state, new_tight, new_started)
return result
ans = dp(0, initial_state, True, False)
# dp is a fresh closure per call, so its cache can't leak into other
# bounds. This clear only frees memory early (optional) — it is NOT
# required for correctness with this nested-function layout.
dp.cache_clear()
return ans
# Final answer
answer = count(high) - count(low - 1)
The only thing that changes between problems:
state: what you track (sum, diff, mask, remainder…)transition: how state updates when placing digitdcheck: what condition to verify at the end
★ Insight ─────────────────────────────────────
- The
count(high) - count(low - 1)split is the whole reason Digit DP only ever solves “count from 1 to N”. You never write range logic — you write one upper-bound counter and subtract two calls. A property that holds on[L,R]becomesf(R) - f(L-1), exactly like a prefix sum on the number line. count()iscount(1..num), so the caller must passlow - 1, NOTlow. Off-by-one here is the #1 Digit DP bug — thecount(low-1)call is what excludes everything belowlow.- The four parameters split cleanly:
pos+tight+startedare the skeleton (identical in every problem); onlystatecarries the problem. Learn the skeleton once and each new problem is just “what’s the state?”.─────────────────────────────────────────────────
A Tiny Recursion Trace
Count integers in [0, 13] with no digit 4. Digits of 13 = [1, 3], so n = 2. Nodes are (pos, tight):
(pos0, tight) d ∈ 0..1 (limit = digits[0] = 1)
├─ d=0 → (pos1, free) d ∈ 0..9, skip 4 → 9 completions (0..9 minus 4)
├─ d=1 → (pos1, tight) d ∈ 0..3, skip nothing → 4 completions (10,11,12,13)
│
└─ but d=0 branches into (pos1, free); ANY later prefix that reaches (pos1, free)
reuses the SAME memoized node — no digit 4 free-count is computed once.
(pos1, free) = 9 and (pos1, tight) = 4, so the root returns 9 + 4 = 13 (every value 0..13 except 4). The free node is where memo hits pay off: on larger bounds many distinct tight-prefixes drop into the identical (pos, free, state) subtree and collapse to one cached value.
2. Understanding Each Parameter
pos — Current Position
digits of N = [3, 5, 2, 8, 1]
0 1 2 3 4 ← pos moves left to right
We build the number one digit at a time, from the most significant.
tight — Upper Bound Constraint
Controls which digits we can place at the current position.
N = 352
pos=0, tight=True, limit=3:
d=0: tight=False (0 < 3, now free forever)
d=1: tight=False (1 < 3)
d=2: tight=False (2 < 3)
d=3: tight=True (3 == 3, still bounded)
pos=1 after placing 3, tight=True, limit=5:
d=0..4: tight=False
d=5: tight=True → next digit limited to 2
pos=2 after placing 3,5, tight=True, limit=2:
d=0..2: valid (number ≤ 352)
d=3..9: FORBIDDEN (would exceed 352)
Rule: tight stays True only if every digit placed so far equals N’s corresponding digit.
★ Insight ─────────────────────────────────────
tightis a one-way door: once you place a digit below the bound,new_tight = tight and (d == limit)goes False and stays False for the rest of the number. There’s exactly one tight path (the prefix that equals N so far) and everything that falls off it lands in the same free subtree.- That is precisely why memoization works. A
tight=Truestate has a unique prefix, so it’s visited once and barely benefits from caching — the payoff is all thetight=Falsestates, where thousands of different prefixes collapse onto the same(pos, state, False)node. Never memoize ontight=Trueexpecting reuse; do memoize the free states. - Most bugs come from computing
limitwrong: it’sdigits[pos]only when tight, else9. Flip that and you either overcount (freely using 9s under a tight bound) or undercount.─────────────────────────────────────────────────
started — Leading Zeros
Handles the fact that 007 is actually 7 (a 1-digit number, not 3-digit).
started=False → still in leading zeros (haven't placed a real digit)
started=True → placed at least one non-zero digit
Transition: new_started = started or (d != 0)
Without this, we’d incorrectly treat leading zeros as real digits, breaking problems that depend on digit count or position.
★ Insight ─────────────────────────────────────
startedexists so a leading0is a non-choice, not a placed digit. Beforestarted, the0you “place” must skip the state transition (see 2376/3791:new_mask/new_diffonly updateif new_started). Forget that guard and007looks like it “used digit 0” or “has length 3” — wrong for distinct-digit or position-parity problems.- Not every problem needs it. Digit-sum problems (2719) drop
startedentirely — leading zeros add 0 to the sum, so they’re harmless. Addstartedonly when leading zeros would corrupt the state: digit count, position parity, first-digit rules, or “no repeated digit” (where a phantom leading 0 falsely marks 0 as used).─────────────────────────────────────────────────
state — Problem-Specific
This is what makes each problem unique. See next section.
3. Common State Choices
| Problem Type | State | Transition | Check |
|---|---|---|---|
| Digit sum = S | remaining |
remaining - d |
remaining == 0 |
| Digit sum in [a, b] | sum |
sum + d |
a <= sum <= b |
| No repeated digits | mask (bitmask) |
mask \| (1 << d) |
always 1 (just count) |
| Divisible by K | remainder |
(rem * 10 + d) % K |
rem == 0 |
| Balanced (odd=even sum) | diff |
diff ± d |
diff == 0 |
| Count of specific digit | count |
count + (d == target) |
count == k |
| No adjacent same digits | last_digit |
d |
always 1 |
| Even/odd digit counts | (even_cnt, odd_cnt) |
increment based on d | even_cnt == odd_cnt |
4. Problem 3791 — Balanced Integers in a Range
Difficulty: Hard
Count numbers in [low, high] with ≥ 2 digits where sum of odd-position digits = sum of even-position digits.
Key State
Track diff = odd_sum - even_sum and length (digit count).
Number: 1 3 3 1
Pos: 1 2 3 4
Odd: 1 3 = 4 → diff += d at odd positions
Even: 3 1 = 4 → diff -= d at even positions
diff = 0 → balanced ✓
Solution
class Solution:
def countBalancedIntegers(self, low: int, high: int) -> int:
def count(num):
if num < 10:
return 0
digits = [int(c) for c in str(num)]
n = len(digits)
OFFSET = 82 # shift diff to non-negative range
@lru_cache(maxsize=None)
def dp(pos, diff, tight, started, length):
if pos == n:
if not started or length < 2:
return 0
return 1 if diff == OFFSET else 0
limit = digits[pos] if tight else 9
result = 0
for d in range(0, limit + 1):
new_tight = tight and (d == limit)
new_started = started or (d != 0)
new_length = length + (1 if new_started else 0)
if new_started:
actual_pos = new_length
if actual_pos % 2 == 1: # odd position
new_diff = diff + d
else: # even position
new_diff = diff - d
else:
new_diff = diff
result += dp(pos + 1, new_diff, new_tight, new_started, new_length)
return result
ans = dp(0, OFFSET, True, False, 0)
dp.cache_clear()
return ans
return count(high) - count(low - 1)
State: (pos, diff, tight, started, length) — diff tracks odd_sum - even_sum, length tracks digit count.
Complexity: O(n × 163 × 2 × 2 × n × 10) ≈ O(n² × 6520)
5. Problem 2376 — Count Special Integers
Difficulty: Hard
Count numbers in [1, n] with all distinct digits.
Key State
Use a bitmask to track which digits have been used.
Number: 3 1 5
mask after '3': 0b1000 (bit 3 set)
mask after '1': 0b1010 (bits 1,3 set)
mask after '5': 0b101010 (bits 1,3,5 set)
Try placing '3' again → bit 3 already set → skip
Solution
class Solution:
def countSpecialNumbers(self, n: int) -> int:
digits = [int(c) for c in str(n)]
L = len(digits)
@lru_cache(maxsize=None)
def dp(pos, mask, tight, started):
if pos == L:
return 1 if started else 0
limit = digits[pos] if tight else 9
result = 0
for d in range(0, limit + 1):
if started and (mask & (1 << d)):
continue # digit already used
new_tight = tight and (d == limit)
new_started = started or (d != 0)
new_mask = mask | (1 << d) if new_started else 0
result += dp(pos + 1, new_mask, new_tight, new_started)
return result
return dp(0, 0, True, False)
State: (pos, mask, tight, started) — mask is 10-bit bitmask of used digits.
Complexity: O(n × 1024 × 2 × 2 × 10) where n = digit count
6. Problem 233 — Number of Digit One
Difficulty: Hard
Count total number of digit 1 appearing in all numbers from 1 to n.
Key Difference
Instead of counting numbers, count total occurrences of digit 1.
Solution
class Solution:
def countDigitOne(self, n: int) -> int:
digits = [int(c) for c in str(n)]
L = len(digits)
@lru_cache(maxsize=None)
def dp(pos, count, tight, started):
if pos == L:
return count if started else 0
limit = digits[pos] if tight else 9
result = 0
for d in range(0, limit + 1):
new_tight = tight and (d == limit)
new_started = started or (d != 0)
new_count = count + (1 if d == 1 else 0)
result += dp(pos + 1, new_count, new_tight, new_started)
return result
return dp(0, 0, True, False)
State: (pos, count, tight, started) — count tracks how many 1’s placed so far.
Note: The base case returns count (not 0 or 1) because we’re summing occurrences, not counting numbers.
★ Insight ─────────────────────────────────────
- This is the one problem that breaks the “return 0/1” reflex. Every other guide problem asks how many numbers qualify (base case returns 1); 233 asks how many 1’s total, so the base case returns the accumulated
count. Same skeleton, one-word change in the payload. - Equivalent reframing:
dphere computes a sum over numbers of (their digit-1 count), which by linearity equals summing 1 each time a 1 is placed. Both views give the same answer — the accumulate-in-state version shown is the one that generalizes to “count digit X” or “count pairs” with no new machinery.─────────────────────────────────────────────────
7. Problem 902 — Numbers At Most N Given Digit Set
Difficulty: Hard
Given a sorted array of digits (strings), count numbers ≤ n using only those digits.
Key Difference
Instead of d in range(0, limit+1), only iterate over the allowed digits.
Solution
class Solution:
def atMostNGivenDigitSet(self, digits_set: list[str], n: int) -> int:
allowed = [int(d) for d in digits_set]
digits = [int(c) for c in str(n)]
L = len(digits)
@lru_cache(maxsize=None)
def dp(pos, tight, started):
if pos == L:
return 1 if started else 0
limit = digits[pos] if tight else 9
result = 0
if not started:
# Option: place nothing (stay in leading zeros)
result += dp(pos + 1, False, False)
for d in allowed:
if d > limit:
break
new_tight = tight and (d == limit)
result += dp(pos + 1, new_tight, True)
return result
return dp(0, True, False)
State: (pos, tight, started) — minimal state since any combination of allowed digits is valid.
★ Insight ─────────────────────────────────────
- The loop changes from
for d in range(0, limit+1)tofor d in allowed— you iterate the permitted digits, not all ten. Becauseallowedis sorted,if d > limit: breakis a clean early cutoff under the tight bound. - The separate
if not started: dp(pos+1, False, False)branch is how you count shorter numbers. Staying in leading-zeros one more position means “this number has fewer digits than N” — that single line is what lets a 3-digit bound also count all the valid 1- and 2-digit numbers. - No
stateat all here: once digits are drawn only fromallowed, every completion is valid, so there’s nothing to check but “did we start”. Minimal-state problems are the best place to first internalize the barepos/tight/startedskeleton.─────────────────────────────────────────────────
8. Problem 1012 — Numbers With Repeated Digits
Difficulty: Hard
Count numbers in [1, n] with at least one repeated digit.
Key Idea
Use complement: answer = n - count_of_special_numbers(n) where special = all digits distinct (Problem 2376).
class Solution:
def numDupDigitsAtMostN(self, n: int) -> int:
# Count numbers with ALL distinct digits, subtract from n
digits = [int(c) for c in str(n)]
L = len(digits)
@lru_cache(maxsize=None)
def dp(pos, mask, tight, started):
if pos == L:
return 1 if started else 0
limit = digits[pos] if tight else 9
result = 0
for d in range(0, limit + 1):
if started and (mask & (1 << d)):
continue
new_tight = tight and (d == limit)
new_started = started or (d != 0)
new_mask = mask | (1 << d) if new_started else 0
result += dp(pos + 1, new_mask, new_tight, new_started)
return result
all_distinct = dp(0, 0, True, False)
return n - all_distinct
Technique: Complement counting — sometimes it’s easier to count what you don’t want.
9. Problem 2719 — Count of Integers (Digit Sum Bounds)
Difficulty: Hard
Count numbers in [num1, num2] whose digit sum is in [min_sum, max_sum].
Solution
class Solution:
def count(self, num1: str, num2: str, min_sum: int, max_sum: int) -> int:
MOD = 10**9 + 7
def solve(num_str):
digits = [int(c) for c in num_str]
n = len(digits)
@lru_cache(maxsize=None)
def dp(pos, dsum, tight):
if dsum > max_sum:
return 0 # pruning
if pos == n:
return 1 if min_sum <= dsum <= max_sum else 0
limit = digits[pos] if tight else 9
result = 0
for d in range(0, limit + 1):
result += dp(pos + 1, dsum + d, tight and (d == limit))
return result % MOD
ans = dp(0, 0, True)
dp.cache_clear()
return ans
# count(num2) - count(num1) + check(num1)
ans2 = solve(num2)
ans1 = solve(num1)
# Check if num1 itself is valid
s = sum(int(c) for c in num1)
num1_valid = 1 if min_sum <= s <= max_sum else 0
return (ans2 - ans1 + num1_valid) % MOD
State: (pos, dsum, tight) — no started needed since leading zeros don’t affect digit sum.
Note: Inputs are strings (can be very large), so we use string-based subtraction: count(num2) - count(num1) + check(num1) instead of count(num2) - count(num1 - 1).
10. Problem 2827 — Number of Beautiful Integers
Difficulty: Hard
Count numbers in [low, high] where count of even digits = count of odd digits AND number is divisible by k.
Key State
Track two things simultaneously: digit parity balance and remainder.
class Solution:
def numberOfBeautifulIntegers(self, low: int, high: int, k: int) -> int:
def count(num):
if num <= 0:
return 0
digits = [int(c) for c in str(num)]
n = len(digits)
@lru_cache(maxsize=None)
def dp(pos, diff, rem, tight, started):
"""
diff = count_even_digits - count_odd_digits
rem = number mod k so far
"""
if pos == n:
if not started:
return 0
return 1 if diff == 0 and rem == 0 else 0
limit = digits[pos] if tight else 9
result = 0
for d in range(0, limit + 1):
new_tight = tight and (d == limit)
new_started = started or (d != 0)
if new_started:
new_diff = diff + (1 if d % 2 == 0 else -1)
new_rem = (rem * 10 + d) % k
else:
new_diff = diff
new_rem = 0
result += dp(pos + 1, new_diff, new_rem, new_tight, new_started)
return result
ans = dp(0, 0, 0, True, False)
dp.cache_clear()
return ans
return count(high) - count(low - 1)
State: (pos, diff, rem, tight, started) — combines two properties in one DP.
Complexity: O(n × (2n+1) × k × 2 × 2 × 10) where n = digit count
★ Insight ─────────────────────────────────────
- Two independent conditions (“even-count = odd-count” AND “divisible by k”) become two independent state dimensions carried side by side —
diffandrem— and the check is justdiff == 0 and rem == 0. You never multiply the problems; you widen the state tuple. This is the general recipe for “property A AND property B”. - The
remtransition(rem * 10 + d) % kis Horner’s method: it’s the running number mod k, updated one digit at a time so you never build the full (possibly huge) integer. This is the reason divisibility fits Digit DP — the remainder is a tiny bounded state even when the number isn’t. - Cost multiplies too: state count is
pos × diff-range × k × .... Ifkis large this DP gets heavy — divisibility problems are only cheap becauserem < kandkis small.─────────────────────────────────────────────────
11. Master Comparison Table
| Problem | State | Transition | Check | Time |
|---|---|---|---|---|
| 3791 Balanced | diff, length |
diff ± d based on position parity |
diff == 0, length ≥ 2 |
O(n² × 163) |
| 2376 Special | mask (10-bit) |
mask \| (1 << d) |
started |
O(n × 1024) |
| 233 Digit One | count |
count + (d == 1) |
return count |
O(n × n) |
| 902 Digit Set | — | only place allowed digits | started |
O(n × |set|) |
| 1012 Repeated | mask |
mask \| (1 << d) |
complement: n - distinct |
O(n × 1024) |
| 2719 Sum Bounds | dsum |
dsum + d |
min ≤ sum ≤ max |
O(n × S) |
| 2827 Beautiful | diff, rem |
diff ± 1, (rem*10+d)%k |
diff == 0, rem == 0 |
O(n² × k) |
12. How to Identify This Pattern
Trigger: “Count numbers in a range with digit property”
See "count numbers in [L, R]"?
└── Yes → Digit DP likely
│
├── Property depends on digit sum?
│ └── State: sum or remaining_sum
│
├── Property depends on which digits appear?
│ └── State: bitmask of used digits
│
├── Property depends on divisibility?
│ └── State: remainder mod k
│
├── Property depends on digit positions?
│ └── State: diff (odd vs even position sums)
│
├── Property depends on adjacent digits?
│ └── State: last_digit
│
└── Multiple properties combined?
└── Combine states (diff + rem, mask + sum, etc.)
The Universal Steps
1. Range reduction
count(low, high) = count(high) - count(low - 1)
2. Convert number to digit array
3. dp(pos, state, tight, started)
│ │ │ │
│ │ │ └── leading zeros handled
│ │ └── still bounded by limit?
│ └── problem-specific (THE KEY VARIABLE)
│
└── which digit position (left to right)
4. Base case: pos == n → check state
5. Loop: d from 0 to (digits[pos] if tight else 9)
6. Transition: update state based on d
Common Pitfalls
| Pitfall | Fix |
|---|---|
| Forgetting leading zeros | Use started parameter |
| Off-by-one in range | count(high) - count(low - 1) |
| State too large | Use diff instead of two sums, remainder instead of full value |
| Sharing one cache across bounds | Define @lru_cache on a nested dp inside count() so each call gets a fresh cache (as in the template). A module/class-level cache reused across different digits gives wrong answers — clear it or key on the digits. |
| String inputs (2719) | Use count(num2) - count(num1) + valid(num1) |
Practice Order
Start on an easier on-ramp before the all-Hard core. The first two need little or no state (or none at all), so you learn the pos/tight/started skeleton in isolation, then layer state on.
Start here
│
▼
357 (Medium) ──── Count Numbers with Unique Digits: warm-up, closed-form or trivial
│ mask DP — meet the pos/tight skeleton with the lightest state
▼
902 (Hard) ──── Simplest full template: restricted digit set, minimal state
│
▼
233 (Hard) ──── Count occurrences instead of numbers
│
▼
2376 (Hard) ──── Bitmask state for unique digits
│
▼
1012 (Hard) ──── Complement counting + bitmask
│
▼
2719 (Hard) ──── Digit sum with bounds, string inputs
│
▼
3791 (Hard) ──── Position-dependent state + length tracking
│
▼
2827 (Hard) ──── Combined state: parity balance + divisibility
When This Fails
Digit DP counts numbers by building them digit by digit, so it needs the property to be decidable from a small running summary:
- The property is not digit-local. “Is
xprime?” or “isxa perfect square?” cannot be tracked in a compact state as you place digits. Digit DP does not apply. - The state explodes. A state that must remember the entire prefix (say, all digits seen in
order) is
10^dstates — no better than enumeration. Digit DP pays off only when the summary is small: a count, a remainder, a mask, a parity. - You forgot
startedwhere leading zeros matter. Counting numbers with distinct digits must not treat the leading zeros of a short number as digits. If a leading zero would be counted, you need the flag. - The bound is exclusive or the range is on the wrong side.
count(R) − count(L−1)assumescountis inclusive. Off by one here shifts every answer by one number.
Self-Test
Answer these from memory, out loud or on paper, before looking. Recognition is not recall: rereading an explanation feels like knowing, and it is not. A question you cannot answer cold names the exact section to revisit — you do not need to reread the guide.
1. What is the standard range trick, and what does it assume?
Answer
answer([L, R]) = count(R) − count(L − 1), where count(X) counts qualifying values in [0, X]. It assumes count is inclusive of its bound. For huge L given as a string, subtract by decrementing the string rather than converting.
2. What do the four standard parameters mean?
Answer
pos: which digit index you are placing. tight: whether the prefix so far exactly matches the bound, which caps the current digit. started: whether a non-zero digit has appeared yet, distinguishing real digits from leading padding. Plus the problem-specific state (a digit sum, a remainder, a mask).
3. Why must tight be in the memo key but the bound itself need not be?
Answer
The bound is fixed for the whole call, so it is context, not state. tight varies between paths reaching the same pos and genuinely changes how many completions exist — cache them together and you get wrong counts.
4. When can you omit started, and when is omitting it a bug?
Answer
Omit it when leading zeros are harmless — counting occurrences of the digit 1 from 0 upward, for instance. It is required whenever a leading zero would be miscounted as a real digit: distinct-digit problems, digit-set membership, first-digit constraints.
5. What is the upper limit for the current digit when tight is true?
Answer
The corresponding digit of the bound. Placing exactly that digit keeps tight for the next position; placing anything smaller frees the remaining positions to range over all ten digits.
6. What kinds of running state actually work here?
Answer
Small summaries: a digit sum (bounded by 9d), a remainder mod m, a bitmask of digits used (2^10), a count, a parity, the previous digit. Anything requiring the full prefix defeats the purpose.
7. How would you count the sum of qualifying numbers rather than how many there are?
Answer
Return a pair (count, sum) from the recursion. When you place digit d at a position with place value p, the contribution is sum_child + d · p · count_child — the child’s own sum plus this digit repeated once for each completion beneath it.
See Also
- Dynamic Programming §12 — digit DP’s place among the other DP families, if you arrived from the general DP guide.
- Bitmask Techniques — when your digit state is a set of seen digits (LC 1012, 2376), the mask machinery lives there.
- Binary Search — the other technique that survives
n <= 10^18; if the property is not digit-local, search the answer instead of building it digit by digit. - Pattern Decision Map — the router: which technique does a cold problem call for?
- Pattern Mastery Program — the spaced-repetition schedule, mastery checklist, and drill formats that turn reading into recall.
Pattern mastered — same template, different pluggable state and transitions.