数位DP
数位DP通常指在\([l,r]\)区间中有多少个满足条件\(k\)的个树
常见的数据范围都很大
也就是说,
把数字的枚举,变成数字的构造
不要把数字看作是\(10^{18}\)
而把数字看作是\(18\)位数的填数过程
就是把原本枚举的问题转化为了构造的问题
然而数位dp常通过记忆化搜索实现
tips:
1.求区间\([l,r]\)的问题也要常转化为\([1,r]-[1,l-1]\)
2.在枚举前我们需要考虑好枚举的方向,也就是从低到高或从高到低
例题
P8801 [蓝桥杯 2022 国 B] 最大数字
题目
这是一个很典型的数位dp
对于每一位数字
我们从高位到低位枚举其可能变成的数字
而在高位的数字越大越好
所以对于每一位数,我们从\(9\)开始枚举
在这一位第一次递推成功后就break
因为再往下枚举就没有更优解了
//当前枚举到第id位,a,b,当前的答案位number
void dfs(int id,int a,int b,ll number){
//1.出口
if(id==0){
ans=max(ans,number);
return ;
}
//2.能做的事情
for(int i=9;i>=0;i--){
int acost=(i-num[id]+10)%10;
int bcost=(num[id]-i+10)%10;
bool flag=false;
if(a>=acost){
flag=true;
dfs(id-1,a-acost,b,number*10+i);
}
if(b>=bcost){
flag=true;
dfs(id-1,a,b-bcost,number*10+i);
}
if(flag) break;
}
}
P8764 [蓝桥杯 2021 国 BC] 二进制问题
题目
同样对于每一位要枚举
但要加上对上界的考虑
枚举不能超过\(n\)
(注意记忆化搜索数组的大小,不要开小了)
//当前枚举到第id位,前面有num个1,这一位是否有限制
ll dfs(int id,int num,int limit){
//1.出口
if(id==0||num==k) return num==k;
if(dp[id][num][limit]>=0) return dp[id][num][limit];
//2.能做的事情
ll res=0;
int bound= limit==1?a[id]:1;
for(int i=0;i<=bound;i++)
res+=dfs(id-1,num+i,limit&&i==bound);
return dp[id][num][limit]=res;
}
P4999 烦人的数学作业
题目
和前面一道题没有什么大的区别
但要注意减法取模时要\((a-b+mod) \mod mod\)
而由于多组数据,记忆化数组\(dp\)其实是可以重复使用的
对于每位数,如果后面没有限制,那么\(dp[id][sum]\)是不变的
所以不必每次清空\(dp\)
//当前枚举到第id位,前面数字和为sum,是否达到上界limit
ll dfs(int id,ll sum,int limit){
//1.出口
if(id==0) return sum;
if(!limit&&dp[id][sum]>=0) return dp[id][sum];
//2.能做的事情
ll res=0;
int bound= limit==1 ? a[id] : 9;
for(int i=0;i<=bound;i++){
res=(res+dfs(id-1,sum+i,limit&&i==bound))%mod;
}
if(!limit) dp[id][sum]=res%mod;
return res%mod;
}
其他题目
P2657 [SCOI2009] windy 数
P2602 [ZJOI2010] 数字计数
P4798 [CEOI2015 Day1] 卡尔文球锦标赛
P3281 [SCOI2013]数数
P2518 [HAOI2010]计数
P3286 [SCOI2014]方伯伯的商场之旅