📘 数位DP

2026-08-07
⭐⭐⭐ GESP 8级

📖 概念讲解

【考点 · 8级】数位DP,属于动态规划 + 位运算思想的交叉考点。

【说人话】

数位DP的核心思路是:把一个数拆成从高位到低位的每一位,逐位讨论。每一位可以「填小于上限的数(后面随便填)」或「填等于上限的数(继续卡上限)」。

关键变量:

模板思路:f(pos, limit, state) 表示从第 pos 位开始,在 limit 约束下,当前状态为 state 的合法方案数。

💡 核心技巧:limit 为 true 时,当前位只能填 0 到 digit[pos];limit 为 false 时,可以填 0 到 9。每次递归时把 limit 传递下去。

💻 代码示例

经典题目:统计 [1, N] 中不含前导零、且相邻两位数字不同的正整数个数

1#include <bits/stdc++.h>
2using namespace std;
3int digits[20], dp[20][10][2]; // dp[pos][上一位][limit] 记忆化数组
4int len;
5
6// 返回从第pos位开始,上一位填了pre,是否卡上限的方案数
7int dfs(int pos, int pre, bool limit, bool lead) {
8 if (pos < 0) return 1; // 所有位都填完了,方案+1
9 if (!limit && !lead && dp[pos][pre][0] != -1) return dp[pos][pre][0]; // 已算过直接返回
10 int up = limit ? digits[pos] : 9; // limit时上限=当前位数字,否则可填0~9
11 int ans = 0;
12 for (int i = 0; i <= up; i++) { // 逐位枚举当前位能填的数字
13 if (lead && i == 0) // 前导零:跳过0,继续lead
14 ans += dfs(pos - 1, pre, limit && i == up, true);
15 else if (i != pre) // 核心约束:相邻两位不能相同
16 ans += dfs(pos - 1, i, limit && i == up, false);
17 }
18 if (!limit && !lead) dp[pos][pre][0] = ans; // 只缓存无limit的情况
19 return ans;
20}
21
22int solve(int n) {
23 len = 0;
24 while (n) { digits[len++] = n % 10; n /= 10; } // 拆数:低位存digits[0]
25 return dfs(len - 1, 0, true, true); // 从最高位开始,初始卡上限
26}
27
28int main() {
29 int L, R; cin >> L >> R;
30 memset(dp, -1, sizeof(dp)); // 初始化记忆化数组
31 cout << solve(R) - solve(L - 1) << endl; // 容斥:[1,R] - [1,L-1] = [L,R]
32}
33// 输入:1 20
34// 输出:19(仅11有相邻相同数字,被排除)

🧩 互动小测

题目 1

数位DP中,limit 变量的作用是什么?

题目 2

为什么 limit=true 时不能直接用记忆化结果?

题目 3

统计 [L, R] 范围内的答案,公式是什么?

🏋️ 动手练一练

📝 编程练习

题目:统计 [1, N] 中所有数位上数字之和恰好等于 S 的正整数个数。
输入:两个正整数 N 和 S(N ≤ 10^18,S ≤ 162)
输出:满足条件的数的个数
提示:在数位DFS中加一个 sum 状态表示当前已用的数字之和。当 sum > S 时提前剪枝。最终当 pos < 0 时,判断 sum == S。
参考答案:
#include <bits/stdc++.h>
using namespace std;
long long N;
int S, digits[20], dp[20][163];
int len;

long long dfs(int pos, int sum, bool limit) {
    if (sum > S) return 0;           // 剪枝:超出目标和
    if (pos < 0) return sum == S;    // 到末位,判断是否恰好等于S
    if (!limit && dp[pos][sum] != -1)
        return dp[pos][sum];         // 记忆化(无limit才缓存)
    int up = limit ? digits[pos] : 9;
    long long ans = 0;
    for (int i = 0; i <= up; i++)
        ans += dfs(pos - 1, sum + i, limit && i == up);
    if (!limit) dp[pos][sum] = ans;
    return ans;
}

long long solve(long long n) {
    len = 0;
    while (n) { digits[len++] = n % 10; n /= 10; }
    memset(dp, -1, sizeof(dp));
    return dfs(len - 1, 0, true);
}

int main() {
    cin >> N >> S;
    cout << solve(N) << endl;
}

要点:把 pre 状态换成 sum(数字之和),其余框架完全一样。注意 sum > S 提前返回0是关键剪枝,不然会超时。

📝 易错点提醒

🏠 返回主页
学完这个知识点后点一下