AlgoMaster Logo

Introduction to Digit DP

Low Priority11 min readUpdated May 30, 2026
Listen to this chapter
Unlock Audio

Digit DP counts or optimizes over numbers with digit-level constraints by constructing them digit by digit, from the most significant to the least significant, while tracking a small piece of state. The runtime is proportional to the number of digits times the state size, replacing brute-force iteration over the range.

What Is Digit DP?

Digit DP counts how many numbers in a range [0, N] (or [L, R]) satisfy a given digit-level property. The algorithm builds numbers one digit at a time, left to right, and uses memoization to avoid redundant work.

At each position, the allowed range for the next digit depends on whether the prefix placed so far still matches N exactly. If it does, the next digit is bounded by the corresponding digit of N. As soon as a digit is placed strictly below that limit, every subsequent digit can be anything from 0 to 9. This constraint is captured by the tight flag, which we will visualize with a concrete example shortly.

How to Identify Digit DP Problems

The signal is a count of numbers in a range [L, R] whose validity depends on individual digits, with a bound (often 10^9 or larger) too big to iterate through. Digit DP handles up to 18 or 19 digits efficiently. Typical shapes:

Problem TypeExample
Digit uniquenessCount numbers with all unique digits
Digit sum constraintsCount numbers where digit sum equals K
Digit occurrenceCount numbers containing at least one 3
Divisibility + digitsCount numbers divisible by K (built digit by digit)
Monotonic digitsCount numbers with non-decreasing digits

The Core Idea

Every digit DP solution is built around a recursive function that processes one digit position at a time, from the most significant digit to the least significant. The state typically includes:

  1. Position (pos): Which digit position is currently being filled (0-indexed from the left).
  2. Tight flag (tight): Whether the digits placed so far exactly match the prefix of N. If tight is true, the current digit can only go up to the corresponding digit of N. If tight is false, the current digit can be 0 through 9.
  3. Problem-specific state: Whatever extra information the property requires. For unique digits, this is a bitmask of which digits have been used. For digit sum problems, this is the running sum.

The Tight Constraint Rule

If tight is true and the placed digit equals the corresponding digit of N, tight stays true. If the placed digit is smaller, tight becomes false and stays false for all remaining positions. The decision tree later in this section makes this concrete.

The Leading Zeros Issue

Counting up to N usually includes 0. But leading zeros distort some properties. Is 007 a 3-digit number with unique digits? Leading zeros should not count toward placed-digit invariants.

The fix is a started flag (or equivalently, a marker for whether any non-zero digit has been placed). When started is false, placing a 0 keeps the path in the leading-zeros phase and 0 is not added to the used-digit set.

Range Queries via Subtraction

Most problems ask for the count in [L, R], not [0, N]. The standard approach:

count(L, R) = count(0, R) - count(0, L - 1)

A single function counts valid numbers from 0 to an upper bound; calling it twice yields the range count. This is the same prefix-sum decomposition, applied to digit DP.

The Decision Tree

The recursion for a 2-digit number with N = 25:

Placing 0 or 1 at position 0 forces the final number below 25, so tight becomes false and position 1 has the full range 0-9. Placing 2 keeps tight as true, restricting position 1 to 0-5.

Example Walkthrough: Count Numbers with Unique Digits (LeetCode 357)

Problem Statement

Given an integer n, return the count of all numbers x where 0 <= x < 10^n such that all digits in x are unique.

Examples:

  • n = 0: Output = 1 (only the number 0)
  • n = 2: Output = 91 (numbers 0 to 99 with all unique digits. The 9 invalid ones are 11, 22, 33, 44, 55, 66, 77, 88, 99)

Intuition

The brute-force approach checks every number from 0 to 10^n - 1 and verifies that no digit repeats. For n = 8, that is 100 million numbers. It works but is slow.

Digit DP tracks which digits have already been used via a 10-bit bitmask (one bit per digit 0-9) and skips any digit already in the mask. The bitmask is the problem-specific state added on top of pos, tight, and started.

Step-by-Step Trace

For LC 357 with n = 2, the upper bound is 99 and digits = [9, 9]. Because every digit of the bound is 9, the tight branch only fires along the path 9 -> 9, and tight never has to constrain a non-9 limit. To exercise the tight flag, the trace below uses a different bound: count unique-digit numbers in [0, 234]. Same algorithm, same code, with digits = [2, 3, 4].

Let f(pos, tight, mask, started) denote the recursive call. The root call is f(0, 1, 0, 0) with digits[0] = 2, so limit = 2.

This trace shows the two situations the tight flag handles:

  • At d=2, pos=0: placing the digit equal to the bound (d == limit) keeps tight = true. The next position is still constrained by digits[1].
  • At d=0 or d=1 at pos=0, or d=0/d=1 at pos=1: placing a digit strictly below the bound flips tight to false permanently for the rest of the recursion. Subsequent positions get the full range 0-9.

The started flag plays a separate role. At d=0, pos=0, no real digit has been placed yet, so the recursion descends with started=0 and the leading zero is not added to the digit-uniqueness mask. That same subtree counts both single-digit numbers (1-9, where started becomes true mid-recursion) and the number 0 itself (when started remains false through to the end).

For LC 357 with n = 2, the same algorithm runs but every limit is 9, so the tight branch never restricts choice meaningfully. The recursion reduces to: 10 numbers from the leading-zero subtree (0 through 9) plus 9 first-digit choices times 9 second-digit choices = 91.

Implementation

The digit DP function takes four parameters: the current position, whether the prefix is still tight against the upper bound, a bitmask of used digits, and whether a non-zero digit has been placed (started). Memoization keys on all four.

Complexity Analysis

  • Time Complexity: O(n 2 2^10 2 10) = O(n * 40960). For LC 357, n is at most 8 (the problem caps the answer at 10^8 - 1). For problems where N has up to 18 digits, the state space remains small.
  • Space Complexity: O(n 2 1024 * 2) for the memoization table. With n = 18, that is about 75K entries.

A Second Example: Numbers Divisible By K (LC 600 / 902 Family)

Count Numbers with Unique Digits used a bitmask of seen digits as its state. Another common digit DP state is the running remainder modulo K, which appears whenever the property of interest is divisibility. The mechanics are identical; only the state changes.

Problem Statement

Count integers in [0, N] whose digit sum is divisible by K. (A close cousin: count integers in [0, N] that are themselves divisible by K. The state for that variant tracks the current number modulo K, not the digit-sum modulo K. The shape of the DP is the same.)

The State

Track the remainder of the running digit sum modulo K. The full state for the recursion:

  • pos: current digit position, 0-indexed from the most significant digit.
  • tight: whether the current prefix equals the corresponding prefix of N.
  • remainder: digit sum so far, modulo K.
  • (started is not needed for digit-sum problems because the digit sum of "no digits placed" is 0, and 0 mod K is naturally the starting value.)

At a terminal state (pos == len), return 1 if remainder == 0, else 0.

Recurrence

At each position, try every allowed digit d (constrained by tight):

solve(pos, tight, rem) = sum over d in [0, limit] of solve(pos + 1, newTight, (rem + d) mod K)

where newTight = tight && (d == limit) as before.

Implementation Sketch

The recurrence body never references the value of N directly; it only consults digits[pos] when tight is true. The complexity is O(n 2 K * 10): polynomial in the number of digits and in K, independent of the numeric value of N.

Variant: Numbers Themselves Divisible By K

To count integers x in [0, N] with x mod K == 0, change the remainder update to track the value modulo K, not the digit sum. The new state transition:

newRem = (rem * 10 + d) mod K

That is, building the number digit by digit, the running value is (prefix * 10 + d) mod K. Everything else (tight, terminal check) is identical.

This is the standard reduction that lets digit DP handle "value mod K" properties even when the value itself is too large to store directly. Only the remainder is kept in state, not the value.

What Properties Admit Digit DP

A property is "digit-DP-shaped" when it can be expressed as a running invariant that updates digit-by-digit and depends only on a small piece of state:

  • Digit sum modulo K → state is the running sum mod K.
  • Numerical value modulo K → state is the running value mod K (via the * 10 + d trick).
  • Set of digits used → state is a bitmask of digits.
  • Last digit placed (for "no two adjacent digits are equal") → state is the previous digit.
  • Has the digit pattern been seen (e.g., contains "13") → state is the position in a DFA matching the pattern.

Properties that depend on the full value in non-decomposable ways (e.g., "is the number prime") do not admit digit DP unless reformulated. Primality has no small running invariant; verification would only be possible at the end, defeating the purpose.

End-To-End Range Query: count(L, R) = count(0, R) - count(0, L - 1)

The DP above counts numbers in [0, N]. Most interview questions ask for [L, R], which is the standard inclusion-exclusion split:

count(L, R) = count(0, R) - count(0, L - 1)

For example, "how many integers in [100, 234] have a digit sum divisible by 3?"

  • count(0, 234) = the digit DP applied to upper bound 234.
  • count(0, 99) = the digit DP applied to upper bound 99.
  • count(100, 234) = count(0, 234) - count(0, 99).

Working it out (digit-sum-divisible-by-3, including 0 in the count of [0, 0]):

  • count(0, 234): integers 0..234 whose digit sum is divisible by 3. By inclusion of 0, 3, 6, 9, 12, ..., 234, this is floor(234 / 3) + 1 = 79. (Closed-form check: digit sum mod 3 equals value mod 3 because 10 ≡ 1 mod 3, so the question reduces to value mod 3.)
  • count(0, 99): integers 0..99 with the same property. By the same closed-form, this is floor(99 / 3) + 1 = 34.
  • count(100, 234) = 79 - 34 = 45.

The closed-form check works here because of the special structure of mod 3. For most properties (digit uniqueness, "contains the digit 7", "no two adjacent equal digits"), there is no closed form, and the digit DP is the only tractable approach.

Edge Cases For The Subtraction

  • L = 0: count(L, R) = count(0, R). No subtraction.
  • L < 0: the problem is normally defined on non-negative integers; clamp L to 0.
  • L = 1: count(L - 1, ...) = count(0, ...). Pay attention to whether the problem includes 0 in valid answers. If 0 is excluded (some problems define "positive integer"), subtract whatever count(0) contributes.

The subtraction reduces every range query to two single-bound queries, which is what the digit DP solves. There is no separate algorithm for [L, R] queries; the inclusion-exclusion handles it.

Quiz

Digit DP Quiz

10 quizzes