NeFut Logo NeFut
Admin Login

Digit Dynamic Programming: Efficiently Counting Integers with Specific Digit Features in a Range

Published at: 2026-05-27 09:17 Last updated: 2026-06-06 13:04
#C++ #Tutorial

Core Logic and Mathematical Principles

The essence of Digit Dynamic Programming (Digit DP) is optimizing topological counting using a Trie-like structure for large integer sets expanded digit by digit. It is mainly used to solve problems such as "counting the number of integers in the range $[L, R]$ that satisfy certain specific digit features (such as not containing specific digits, or the sum of the digits being prime, etc.)".

  1. Dimensional Reduction via Interval Difference: Maintaining complex digit constraints directly on the closed interval $[L, R]$ is extremely difficult. Since the counting function $f(X)$ (which represents the count of integers satisfying the condition within $[0, X]$) has explicit additivity and independence, the problem can be losslessly reduced to two independent single-boundary counts via prefix differences:

$$\text{Ans} = f(R) - f(L - 1)$$

  1. Trie Tree Structure of State Space and Memoization Pruning: For a given upper bound $X$, we can view the selection process from the most significant to the least significant digit as a topological traversal on a high-dimensional digital trie (Trie tree).

State Design and Algorithm Derivation

The most elegant and least prone to failure implementation of Digit DP is a highly cohesive memoization search template.

1. Core Search Function Parameter Design

Define int dfs(int pos, int state, bool limit, bool lead):

2. State Transition and Memoization Control

Let the upper limit of the current digit corresponding to $X$ be $up$ (if limit is true, then $up = \text{digits}[pos]$, otherwise $up = 9$). By looping through the digits that can be filled in the current position $i \in [0, up]$:

3. Mathematical Criteria for Memoization Backtracking

The return value of dfs(pos, state) can only be written into the memoization array dp[pos][state] when limit == false and lead == false. Because once there is an edge constraint or leading zero contamination, the value calculated at that moment is just an incomplete subtree and cannot be reused as a general state.


C++ Standard Source Code (NOIP Style)

The following code solves the classic Digit DP model: "Count the number of integers in the range $[L, R]$ that do not contain consecutive identical digits (i.e., adjacent digits are different)", strictly compatible with the Linux g++ -O2 compilation standard.

#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>

using namespace std;

long long dp[20][11]; // dp[pos][pre_digit] Memoization array
int digits[20];       // Stores each digit of the upper bound X after decomposition

// Core memoization search function
long long dfs(int pos, int pre, bool limit, bool lead) {
    // Base condition: all digits processed, indicating a valid number has been successfully constructed
    if (pos == 0) {
        return 1; 
    }

    // Critical pitfall: only when there are no constraints and no leading zero contamination can we directly read the memoization cache
    if (!limit && !lead && dp[pos][pre] != -1) {
        return dp[pos][pre];
    }

    // Dynamically lock the upper limit of the current position based on the edge state
    int up = limit ? digits[pos] : 9;
    long long res = 0;

    for (int i = 0; i <= up; ++i) {
        // Condition filtering: if not in leading zero state, the current digit cannot be the same as the previous digit
        if (!lead && i == pre) {
            continue;
        }

        // Recursively enter the next digit
        // 1. The new limit state is determined by the current limit and whether it fills the upper limit digit
        // 2. The new lead state is determined by the current lead and whether the current digit continues to fill 0
        res += dfs(pos - 1, i, limit && (i == up), lead && (i == 0));
    }

    // Critical pitfall: only when there are no constraints and no leading zero contamination can we solidify the current value into the global cache
    if (!limit && !lead) {
        dp[pos][pre] = res;
    }

    return res;
}

// Solve the number of valid quantities in the range [0, n]
long long solve(long long n) {
    if (n < 0) return 0;
    if (n == 0) return 1;

    int len = 0;
    while (n > 0) {
        digits[++len] = n % 10; // Decomposing the number, digits[1] is the least significant, digits[len] is the most significant
        n /= 10;
    }

    // Before each solution, the memoization array must be reset to -1
    memset(dp, -1, sizeof(dp));

    // Start from the most significant digit downwards, initial state: constrained by edge (true), leading zero (true), previous digit passed an irrelevant value 10
    return dfs(len, 10, true, true);
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    long long l, r;
    if (cin >> l >> r) {
        // Use prefix differences to achieve lossless mapping of the closed interval [L, R]
        cout << solve(r) - solve(l - 1) << "\n";
    }

    return 0;
}

NOIP Practical Pitfall Guide

1. Incorrectly reading and writing memoization for states with edge constraints (limit == true)

2. Ignoring the difference boundary $L-1$ leading to integer underflow


Classic NOIP/Luogu Real Questions

1. Luogu P2602 [ZJOI2010] Digit Counting

2. Luogu P4999 Annoying Math Homework

Original Source: local://15.3

[h] Back to Home