数位 DP 严格来说其实并不是 DP……它只是个单纯的计数问题
但是怎么说呢……现在大家似乎都把数位 DP 叫这个名字,所以……我们还是……叫它 DP
额什么是数位 DP 呢?
一句话概括——一类求在 K 进制下m满足条件的数的数量有多少个的算法
常见的问题形式:
求 [l..r] 之间的在 K 进制下满足条件 F 的数有多少个?
什么?你说for (int i = l ; i <= r; i++) ans+=check(i);?
别学竞赛了,回家养鲲吧
我们很快就会发现,数位 DP 的复杂度和位数相关,所以一般数据规模奇大
(以上摘自lyd课件)
数位DP
(以下基于k进制讨论)
数位DP一般会让你求\([l,r]\)满足条件的个数,这里我们设函数\(c(x)\)表示小于x的正整数中满足条件的个数,则最终答案就是 \(c(r+1)-c(l)\) 。但k进制时加一操作不一定好实现,所以更常用的是 \(c(r)-c(l)+\text{对r的特判结果}\)。
所以问题转化为求\(c(x)\)。
正如它名字中的“数位”,DP状态的第一维通常是数字的位数。所以只要写出转移方程,就可以得到小于\(k^x\)的满足条件的个数(其中x是任意/给定正整数)即\(\sum dp[x][...]\)
现在要求\(c(x)\)。设\(x\)有\(l\)位,且现在已经求得了\(dp[l-1][...]\)。现在考虑最高位,分为两种不同的情况:
满足条件的y的最高位小于x的最高位。这时能保证\(y<x\),所以后面任意选。
满足条件的y的最高位等于x的最高位。这时可以递归解决后面的位构成的子问题。但我们发现递归完全是浪费,因为子问题求出的dp数组是一样的。所以在子问题中真正有用的是求情况1和递归下一层。
因为递归的层数是递减的,于是我们可以把递归改为循环。
上面这些抽象的描述说实话我都看不懂QAQ
考虑这样的一道题目:求\([l,r]\)中不包含4和62的数的个数。
那么就可以设\(c(x)\)为小于\(x\)的数中不包含4和62的数的个数,最终答案即为\(c(r+1)-c(l)\)。
这里可以设\(dp[i][j]\)表示长度为i位的,最高位是j的满足条件的数的个数。然后我们可以写出这样的转移方程:\(dp[i][j](j\neq4)=sum\{dp[i-1][k]|\text{当}j=6\text{时}k\neq2\}\)
下面假设x
有l
位,最高位是x[1]
,最低位是x[l]
,其他类似。这一段代码求出了dp数组。
for(int j=0;j<=9;j++){
if(j==4)continue;
dp[1][j]=1;
}
for(int i=2;i<=l;i++){
for(int j=0;j<=9;j++){
if(j==4)continue;
for(int k=0;k<=9;k++){
if(j==6&&k==2)continue;
dp[i][j]+=dp[i-1][k];
}
}
}
然后考虑情况一。
int ans=0;
for(int j=0;j<x[1];j++){
if(j==4)continue;
ans+=dp[l][j];
}
然后是情况二。先写递归版。(toint表示将数组表示的数字转换成int,toarr则相反)
ans+=c(从前面去掉1位的x);
这时我们可以发现,在第一次递归里有用的代码是如下的部分:
for(int j=0;j<x[2];j++){
if(j==4||(j==2&&x[1]==6)continue;
ans+=dp[2][j];
}
ans+=c(从前面去掉2位的x);
所以最终我们应该把代码修改为这样子:
int ans=0;
for(int i=l;i>=1;i--){
for(int j=0;j<x[l-i+1];j++){
if(j==4||(j==2&&x[l-i]==6))continue;
ans+=dp[i][j];
}
}