Algorithm tutorials
Digit DP
Count integers up to 10^18 whose digits satisfy a rule by building them digit by digit with tight and leading-zero flags, memoizing on the position and a small summary of the prefix, and answering ranges with prefix differences.
This is an extension topic beyond most interviews; it pays off in hard OAs that ask you to count the integers in a huge range (up to 10^18) whose digits satisfy some rule.
Intuition
Counting from 0 to 10^18 one number at a time is hopeless, but the work is extremely repetitive: counting 7000-7999, 8000-8999 and 9000-9999 all walk the last three digits through 000-999 in the same way. Digit DP builds numbers digit by digit from the most significant position and caches "how many ways can the remaining positions be filled, given what we know so far".
The standard state:
pos: which digit we are filling.tight: whether every digit so far equals the corresponding digit of the upper boundn. If tight, the current digit can go only up ton's digit; otherwise it can be anything from 0 to 9.started: whether a non-zero digit has been placed yet. Leading zeros usually must not count as real digits (they are not "used" digits and do not make neighbors).- the problem-specific summary of the prefix: previous digit, digit sum modulo k, mask of used digits, number of ones so far...
Ranges are handled with prefix counts: count(lo..hi) = count(0..hi) - count(0..lo-1).
When to use it in interviews / OAs
- "How many integers in [L, R] ..." with R up to 10^9 or 10^18 and a condition on the digits (distinct digits, no forbidden pattern, digit sum, alternating parity, adjacent digit differences).
- "Total number of times digit d appears in all numbers up to n".
- Counting numbers at most n that can be written with a given set of digits, or in a different base (for example binary strings without consecutive ones).
Template
Write a memoized DFS over positions. Python's cache includes tight and started in the key, so no special handling is needed (in C++ one usually memoizes only the non-tight, started states).
Total count of digit 1 in 0..n. The prefix summary is "how many ones so far"; leading zeros contribute nothing, so started is not needed.
from functools import cache
def count_digit_one(n: int) -> int:
# total number of digit 1 written when listing every integer 0..n
digits = list(map(int, str(n)))
@cache
def dfs(pos: int, ones: int, tight: bool) -> int:
if pos == len(digits):
return ones
limit = digits[pos] if tight else 9
return sum(dfs(pos + 1, ones + (d == 1), tight and d == limit) for d in range(limit + 1))
return dfs(0, 0, True)
Integers in 1..n with all digits distinct. The prefix summary is a 10-bit mask of used digits. Leading zeros must not mark digit 0 as used, which is exactly what started is for.
def count_distinct_digit_numbers(n: int) -> int:
# how many integers in [1, n] have pairwise different digits
digits = list(map(int, str(n)))
@cache
def dfs(pos: int, used: int, tight: bool, started: bool) -> int:
if pos == len(digits):
return int(started) # the all-zero "number" is not counted
limit = digits[pos] if tight else 9
total = 0
for d in range(limit + 1):
if not started and d == 0: # still in the leading zeros
total += dfs(pos + 1, used, tight and d == limit, False)
elif not used >> d & 1:
total += dfs(pos + 1, used | 1 << d, tight and d == limit, True)
return total
return dfs(0, 0, True, False)
Adjacent digits differ by at least 2. Here the summary is the previous digit, and only digits after the number has started are compared.
def count_windy(n: int) -> int:
# integers in [1, n] whose adjacent digits differ by at least 2
if n <= 0:
return 0
digits = list(map(int, str(n)))
@cache
def dfs(pos: int, prev: int, tight: bool, started: bool) -> int:
if pos == len(digits):
return int(started)
limit = digits[pos] if tight else 9
total = 0
for d in range(limit + 1):
nt = tight and d == limit
if not started:
total += dfs(pos + 1, d, nt, d != 0)
elif abs(d - prev) >= 2:
total += dfs(pos + 1, d, nt, True)
return total
return dfs(0, 0, True, False)
def count_in_range(lo: int, hi: int, count_upto) -> int:
# count_upto(x) counts valid integers in [0, x]; the answer for [lo, hi] is a difference
return count_upto(hi) - (count_upto(lo - 1) if lo > 0 else 0)
Complexity
Time is (number of positions) x (number of prefix summaries) x 2 (tight) x 2 (started) x 10 (digit choices). For 10^18 there are 19 positions: the digit-one count has about 19 * 20 * 2 summaries, the distinct-digits count about 19 * 1024 * 4. Both run instantly. Space is the number of cached states.
Common pitfalls
- Leading zeros: "0042" is the number 42. Without
started, zeros at the front get treated as digits (marking 0 as used, or being compared to the next digit). - Counting zero: decide whether 0 is a valid answer and keep it consistent between
count(hi)andcount(lo - 1). - The lo - 1 boundary: when
lo = 0, there is nothing to subtract;str(-1)would break the digit list. - Tight in the memo (other languages): if you memoize without
tightin the key, only store results computed withtight == False, because a tight state depends onn's remaining digits. - Stale caches: the cached function depends on
digits, so define it inside the counting function (as above) to get a fresh cache for each bound. - Too large a summary: if the prefix summary can take millions of values (for example the exact digit sum of a 100-digit bound times a large modulus), check the state count before coding.
Interview variations
- Numbers at most n built from a given digit set: summary is nothing at all; only
tightandstartedmatter. - Numbers with at least one repeated digit: complement of the distinct-digits count.
- Non-negative integers without consecutive ones in binary: same template over the binary digits of n, summary is the previous bit.
- Count integers whose digit sum lies in [a, b]: summary is the running digit sum.
- Stepping numbers (adjacent digits differ by exactly 1) in a range given as strings: summary is the previous digit; ranges come as big strings, so compute
count(hi) - count(lo) + check(lo). - Beautiful integers (equal numbers of even and odd digits and divisible by k): summary is (even minus odd, value mod k).