【递推型】数位 DP
本文章同步发表在博客园。
什么是数位 DP?
数位 DP 通常用来统计区间
上界 R 的处理技巧
由数值的比较规则可知,当前数位的取值范围与靠前数位的值有关。
如果靠前的所有数位都与
整体来说就是任意时候当前数均不能大于
因此,状态中需要一个关于上界的标记
转移时,如果靠前位或者当前位与
如果单独考虑 ub=0 的情况
由于当位数为
这样能使代码变得更清晰……吗?
其实上非也。
如果同时有多个数据限制
因此不建议使用该方法。
下界 L 的处理技巧
下界
啊对对对,你说的很对,但是你先别急,你听我说。
如果不用容斥法儿,那就要考虑下界
那么现在问题就来了,到底是加下界的方案好呢,还是常规的容斥法好呢?
答案是前者。
为什么?这么轻易下定论,难道连个理由都没有?要知道后者是最常用的啊喂!
是的是的,但是你自认为后者少去了下界标记
因为后者还需要考虑前导零。考虑前导零,实质上就等同于在考虑一个下界标记,只不过
而且,同样的,如果有多个范围限制,这东西又得上容斥,特麻烦。
故推荐前者写法。(当然并不是阻止你用容斥写法!你想用还是可以用滴!)
经典例题分析
这里选用 P2657 windy 数 作为例题进行讲解。
读入时统一用字符串读入并补充
考虑如何定义
不难想到状态中应存在:
- 当前枚举到第
i 位 - 下界标记
x 以及上界标记y - 当前位的数字
k
综上所述我们可以定义出一个四维的
接下来考虑转移。
首先我们枚举
接下来枚举当前位数字
那么接下来就可以转移了。我的转移是扩散型的。
可以秒算出新一轮的
不过这个条件少了一点。如果现在还处在前导零范围,那我们就不需要保证这个条件了,所以需要再特判一下
那么这个题目就结束了。
代码:
#include<bits/stdc++.h>
#define LL long long
using namespace std;
string a,b;LL ch,n,dp[20][2][2][10],ans;
int main(){
cin>>a>>b;n=b.size(),ch=n-a.size();
while(a.size()<n)a="0"+a;dp[0][0][0][0]=1;
for(int i=0;i<n;i++)for(int x=0;x<2;x++)
for(int y=0;y<2;y++)for(int j=0;j<=9;j++)
for(int k=(x?0:a[i]-'0');k<=(y?9:b[i]-'0');k++){
int u=(x||k>(a[i]-'0')),v=(y||k<(b[i]-'0'));
if((!x&&i<=ch)||abs(j-k)>1)dp[i+1][u][v][k]+=dp[i][x][y][j];
}
for(int x=0;x<2;x++)for(int y=0;y<2;y++)
for(int j=0;j<=9;j++)ans+=dp[n][x][y][j];
cout<<ans<<"\n";
return 0;
}
P.S.这份代码同样也可以通过 加强版 的数据。双倍经验!这可是一黄一蓝啊!
第二道例题详解
这道例题为 P6218 Round Numbers S。
这个例题算是比较简单的了。
首先由于 DP 处理的时候是用二进制处理的,所以一开始先把
接下来考虑 DP 的状态设计。
由于二进制顶多
什么样的
因此这是本题一个坑点之一,你需要特殊判断一下前导零不能算进
其他的话,转移和上一题比较相似,也要枚举
最后累加答案的时候枚举
代码:
#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL l,r,n,ch,dp[40][40][40][2][2],ans;string L,R;
int main(){
cin>>l>>r;dp[0][0][0][0][0]=1;
while(l)L=(char)(l%2+'0')+L,l>>=1;
while(r)R=(char)(r%2+'0')+R,r>>=1;
n=R.size();ch=n-L.size();while(L.size()<n)L="0"+L;
for(int i=0;i<n;i++)for(int x=0;x<2;x++)
for(int y=0;y<2;y++)for(int p=0;p<=i;p++)for(int q=0;p+q<=i;q++)
for(int k=(x?0:L[i]-'0');k<=(y?1:R[i]-'0');k++){
int nx=(x||k>(L[i]-'0')),ny=(y||k<(R[i]-'0'));
dp[i+1][p+(!k&&(!(!x&&i<=ch)))][q+(k)][nx][ny]+=dp[i][p][q][x][y];
}
for(int p=0;p<=n;p++)for(int q=0;p+q<=n&&q<=p;q++)
for(int x=0;x<2;x++)for(int y=0;y<2;y++)ans+=dp[n][p][q][x][y];
cout<<ans<<"\n";
return 0;
}
初试身手——例 3
这道例题为 同类分布。
注意到各位数字之和随着 DP 的推进在不断变换。这样很难处理,不能存取模后的值,但原值过大根本无法记录。
怎么办?凉拌炒鸡蛋,好吃又好看!
其实上很简单。反正数据范围不算很大,那就暴力一点嘛!在外面套一个大的循环,枚举最终的各位数字之和。这样就可以记录了。
重述一遍,定义
主要转移难度在于创造新的
整个来说也不算很难吧。
代码:
#include<bits/stdc++.h>
#define LL long long
using namespace std;
string L,R;
LL n,ch,dp[20][200][200][5],ans;
void init(int N){
for(int i=0;i<=n;i++)for(int j=0;j<=N;j++)
for(int k=0;k<=N;k++)for(int x=0;x<4;x++)dp[i][j][k][x]=0;
dp[0][0][0][0]=1;return;
}
int main(){
cin>>L>>R;n=R.size(),ch=n-L.size();
while(L.size()<n)L="0"+L;
for(int mod=1;mod<=n*9;mod++){
init(mod);
for(int i=0;i<n;i++)for(int f=0;f<=mod;f++)
for(int o=0;o<4;o++)for(int p=0;p<=mod;p++)
for(int k=(o&1?0:L[i]-'0');k<=(o&2?9:R[i]-'0')&&f+k<=mod;k++){
if(!dp[i][f][p][o])continue;
int u=(o&1||k>(L[i]-'0')),v=(o&2||k<(R[i]-'0'));
dp[i+1][f+k][(p*10+k)%mod][u|(v*2)]+=dp[i][f][p][o];
}
for(int x=0;x<4;x++)ans+=dp[n][mod][0][x];
}
cout<<ans<<"\n";
return 0;
}
简单总结概括一下
学数位 DP 之前,我一直以为大多题目都是绿的。现在我才知道,数位 DP 的题都是蓝起步吗?我都看到一堆紫题了。
数位 DP,大多人都喜欢写记忆化搜索。一翻题解区,记搜占满大几页,想找个递推的题解都难。大家似乎都认为记搜更优,而我不这么认为。我认为正常的递推比较清晰方便,也比较好写。
可以发现,其实上数位 DP 的套路基本都是一样的,一个板子套在所有题上。只需要稍微修一修中间的一些细节,多加几层循环之类的,就能够又过一个题。
而在这之中,上下界标记是精华。
顺便提一嘴,由于数位 DP 通常定的维数都比较多,所以它常和状压挂钩。你经常需要把各种东西压成一个二进制数来表示,这样能使你的代码简略不少,还能减少你的常数。
码这么多字也不容易,还希望各位留个赞支持一下,真是太感谢啦!