数位DP
一、数位是指把一个数字按照个、十、百、千等等一位一位地拆开,关注它每一位上的数字。
如果拆的是十进制数,那么每一位数字都是 0~9,其他进制可类比十进制
数位 DP:用来解决一类特定问题,这种问题比较好辨认,一般具有这几个特征:
1.要求统计满足一定条件的数的数量(即,最终目的为计数);
2.这些条件经过转化后可以使用「数位」的思想去理解和判断;
3.输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;
4.上界很大(比如 10^{18}),暴力枚举验证会超时。
二、数位 DP 的基本原理:
考虑人类计数的方式,最朴素的计数就是从小到大开始依次加一。但我们发现对于位数比较多的数,这样的过程中有许多重复的部分。
例如,从 7000 数到 7999、从 8000 数到 8999、和从 9000 数到 9999 的过程非常相似,它们都是后三位从 000 变到 999,不一样的地方只有千位这一位,所以我们可以把这些过程归并起来,将这些过程中产生的计数答案也都存在一个通用的数组里。此数组根据题目具体要求设置状态,用递推或 DP 的方式进行状态转移。
数位 DP 中通常会利用常规计数问题技巧,比如把一个区间内的答案拆成两部分相减:
即a(l,r)=a(r,0)-a(l-1,0);
三、那么有了通用答案数组,接下来就是统计答案。
统计答案可以选择记忆化搜索,也可以选择循环迭代递推。
为了不重不漏地统计所有不超过上限的答案,要从高到低枚举每一位,再考虑每一位都可以填哪些数字,最后利用通用答案数组统计答案。
例题1
https://www.luogu.com.cn/problem/P2602
解法
先dp出满i位的数的数位数量(有前导0)
再根据上限分位从高到低算出数量,去除前导0的0;
上图:关于具体求数位的过程
Code
点击查看代码
int a, b; // 待求区间a,b
int cnt_a[10], cnt_b[10], dp[15], mi_10[15]; // mi_10[i]为10的i次幂
// cnt[i]为数i的数量 //dp[i]为满前i位数的每个数的数量
int x_wei[15]; // 记录数x每一十进制位
void swdp(int x, int* cnt) { // 数位dp
int tmp = x;
int len = 0;
while (x) {
x_wei[++len] = x % 10;
x /= 10;
}
// 实质上每一次i计算的是对应位数上限的数量
for (int i = len; i >= 1; i--) {
for (int j = 0; j <= 9; j++) { // 加上 0到a[i]*10^(i-1)-1的 前i-1位的个数
cnt[j] += x_wei[i] * dp[i - 1];
}
for (int j = 0; j < x_wei[i]; j++) { // 加上 第i位的个数
cnt[j] += mi_10[i - 1];
}
tmp -= x_wei[i] * mi_10[i - 1]; // tmp为前i位的上界数量
cnt[x_wei[i]] += tmp + 1; // 对第i位加 1是mi_10[i-1];
cnt[0] -= mi_10[i - 1]; // 减去前导0
}
}
void solve() {
cin >> a >> b;
mi_10[0] = 1;
// dp[i]的每一个数 数量相同
for (int i = 1; i <= 13; i++) { // 对满i位的数量dp统计
dp[i] = 10 * dp[i - 1] + mi_10[i - 1]; // mi_10加上第i位的情况
mi_10[i] = 10 * mi_10[i - 1];
}
swdp(b, cnt_b);
swdp(a - 1, cnt_a);
for (int i = 0; i <= 9; i++) {
cout << cnt_b[i] - cnt_a[i] << ' ';
}
}