re:从零开始的数位dp
起源:唔,,前几天打cf,edu50那场被C题虐了,决定学学数位dp。(此文持续更新至9.19)
ps:我也什么都不会遇到一些胡话大家不要喷我啊。。。
数位dp问题:就是求在区间l到r上满足规定条件的数的个数。
ex1:hdu3555
题意:给你n,求从一到n中有多少个数不包含“49”。(t<=1e4,n<=2^63-1)
首先数位dp顾名思义就是对数位进行dp嘛,所以dp数组的第一维我们用来保存数字的位数,第二位我们用来判定当前位是否为4,
所以就是 dp[20][2]; 这个样子。 在这之前我们先考虑一下常规的搜索思路,一件非常显然的事情,在[1,1000]和[1001,2000]以及所有类似区间里符合要求的数都是一样的,这样我们就可以通过记忆化的方式来保存某些结果。
先给出solve函数
1 ll solve(ll num){ 2 int k = 0;//记录数位 3 while(num){ 4 k++; 5 digit[k]=num%10; 6 num/=10; 7 } 8 return dfs(k,false,true); 9 }
这个很好理解嘛,保存这个数各位上的数字。然后我们就可以进行记忆化搜索了,
dp[20][2]:表示 1.有4的时候有几个含有49, 2.没有4的时候,有几个含有49。
ll dfs(int len,bool if4,bool limit){
//当前是第几位,上一位是否是4,上一位是否是上界
if(len==0)//统计完了直接返回1
return 1;
if(!limit&&dp[len][if4])//不是上界并且这种情况已经统计过
return dp[len][if4];
ll cnt=0,up_bound=(limit?digit[len]:9);//up_bound是当前位能满足的最大值,如果上一位是上界的话,当前位最大只能取到当前位的数字,如果不是,当前位可以从0取到9
for(int i=0;i<=up_bound;i++){
if(if4&&i==9)
continue;//上一位是4并且这一位是9,GG了啊
cnt+=dfs(len-1,i==4,limit&&i==up_bound);//上一位是上界的情况下我们才会考虑这一位是否是上界
}
if(!limit)//不是上界,属于通用的情况,我们进行赋值
dp[len][if4]=cnt;
return cnt;
}
最后结果差分一下就好。
ex2:hdu2089
和上道题几乎一样,条件是没有“4”并且没有“62”,
这时候我们掏出上一道题的板子了嘛肯定要,只需要在判断时加入一句话就行,在代码中加上注释了,就不做多解释了
#include <bits/stdc++.h> using namespace std; int n,m; int digit[10]; int dp[10][2]; int dfs(int len,bool if6, bool limit){ if(len==0) return 1; if(!limit&&dp[len][if6]) return dp[len][if6]; int cnt = 0,up_bound = (limit?digit[len]:9); for(int i=0;i<=up_bound;i++){ if(i==4)//如果遇到四就直接GG continue; if(if6&&i==2) continue; cnt+=dfs(len-1,i==6,limit&&i==up_bound); } if(!limit) dp[len][if6]=cnt; return cnt; } int solve(int num){ int k = 0;//记录数位 while(num){ k++; digit[k]=num%10; num/=10; } return dfs(k,false,true); } int main(){ while (scanf("%d%d",&n,&m)&&(n+m)) { cout << solve(m) - solve(n - 1) << endl; } }
ex3:codeforces1036C,也就是EDU50的C题嘛,一道非常简单的板子题,(问题是我当时还没听说过数位dp,,真的,,不然就可以骑学长了,,哭)
条件是 零的个数不大于3个。
很显然我们只要把dp数组的第二维开到3就可以了嘛,,然后就是套板子,, 我这里把0的情况单独拿出来了,因为1到9都可以一起考虑嘛
#include <bits/stdc++.h> using namespace std; typedef long long ll; int digit[20]; ll dp[20][4]; ll dfs(int len,int not0,bool limit){ if(len==0)//完了 return 1; if(!limit&&dp[len][not0])//已经统计过 return dp[len][not0];// ll cnt=0,up_bound=(limit?digit[len]:9);// cnt+=dfs(len-1,not0,limit&&digit[len]==0);// for(int i=1;i<=up_bound;i++){ if(not0==3) continue; cnt+=dfs(len-1,not0+1,limit&&i==up_bound); } if(!limit) dp[len][not0]=cnt; return cnt; } ll solve(ll num){ int k = 0;//记录数位 while(num){ k++; digit[k]=num%10; num/=10; } return dfs(k,0,true); } int t;ll l,r; int main(){ ios::sync_with_stdio(false); cin>>t; while (t--){ cin>>l>>r; cout<<(solve(r)-solve(l-1))<<endl; } }
ex4:
hdu3652
条件:包括“13”并且能被13整除
首先我们想到用余数来分类嘛,然后结合最初的板子,开出来的dp数组就是这样的 dp[17][17][3];//分别是数位,余数,对于“13”的三种状态(包括1,包括13,啥都没有)
还有取模的那个地方需要稍微理解一下,剩下的也就是板子了
1 #include <bits/stdc++.h> 2 using namespace std; 3 int digit[17]; 4 int dp[17][17][3]; 5 int dfs(int len,int mod,int have,int limit){ 6 if(len==0) 7 return mod==0&&have==2; 8 if(!limit&&dp[len][mod][have]) 9 return dp[len][mod][have]; 10 int cnt = 0,up_bound=(limit?digit[len]:9); 11 for(int i=0;i<=up_bound;i++){ 12 int mod_ = (mod*10+i)%13; 13 int tmp = have; 14 if(have==0&&i==1) 15 tmp = 1; 16 if(have==1&&i!=1) 17 tmp = 0; 18 if(have==1&&i==3) 19 tmp = 2; 20 cnt+=dfs(len-1,mod_,tmp,limit&&i==up_bound); 21 } 22 if(!limit) 23 dp[len][mod][have] = cnt; 24 return cnt; 25 } 26 27 int solve(int num){ 28 int k = 0; 29 while (num){ 30 k++; 31 digit[k] = num%10; 32 num/=10; 33 } 34 return dfs(k,0,0,1); 35 } 36 int n; 37 int main(){ 38 ios::sync_with_stdio(false); 39 while (scanf("%d",&n)!=EOF){ 40 // scanf("%d",&n); 41 cout<<solve(n)<<endl; 42 } 43 }
ex5: codeforces 55 d, 是上一道题略微进化版 条件“这个数能被他的每个非零位的最小公倍数整除”;
首先我们还是想到对模数来分类,这是非常显然的嘛,除此之外,我们需要保存“最小公倍数”,因为转移的时候必须要用到之前的,这样子我们开出来的数组是 dp[20][2520][2520],好恭喜你MLE on test 1,这时就需要一些奇淫技巧。我们会发现,lcm的个数非常有限吧,实际上只有48个,所以我们可以离散化嘛。然后就与上一题非常相似了,我们保存 “presum”和“prelcm”,进行记忆化搜索即可。
为什么可以%2520呢,
- 首先我们能够知道如果这个数能够整除它的每个数位上的数字,那么它一定能够整除他们的最小公倍数,是充要的。
- 那么我们定义状态dp[i][j][k]代表i位在任意组合下,得到的所有数位的数字的最小公倍数为j,且该数%2520为k的方案数。
- 我们可以知道任意多个1-9之间的数的公倍数最大不会超过2520,而且他们都是2520的约数,所以(一个数%2520)能够被该数所有数位的数字的最小公倍数整除,那么该数就能整除自己每个数位上的数字。
#include <cstdio> using namespace std; typedef long long ll; int t;ll l,r; const int mod = 2520; ll dp[20][mod][50]; int ind[mod+5]; int digit[20]; void init(){ int cnt = 0; for(int i=1;i<=mod;i++){ if(mod%i==0) ind[i]=cnt++; } } int gcd(int a, int b){ return b==0?a:gcd(b,a%b); } int lcm(int a, int b){ return a/gcd(a,b)*b; } ll dfs(int len,int presum,int prelcm,bool limit){ if(len==0) return presum%prelcm==0; if(!limit&&dp[len][presum][ind[prelcm]]) return dp[len][presum][ind[prelcm]]; ll cnt = 0;int up_bound = limit?digit[len]:9; for(int i=0;i<=up_bound;i++){ int nowsum = (presum*10+i)%mod; int nowlcm = prelcm; if(i!=0) nowlcm = lcm(nowlcm,i); cnt+=dfs(len-1,nowsum,nowlcm,limit&&i==up_bound); } if(!limit) dp[len][presum][ind[prelcm]] = cnt; return cnt; } ll solve(ll num){ int k = 0; while (num){ k++; digit[k]=num%10; num/=10; } return dfs(k,0,1,1); } int main(){ init(); scanf("%d",&t); while (t--){ scanf("%I64d%I64d",&l,&r); printf("%I64d\n",solve(r)-solve(l-1)); } }
ex6:poj 3286 求区间里包含多少个零
我随手一写竟然过了。网上题解貌似没看到和我的写法一样的。所以讲的详细些,还是套板子,dfs里的四个参数分别表示 位数,零的个数,是否前导零,是否上界
然后就很简单了嘛。注意到l可以取到0,然后我们的dfs对于零来说是没有计算在内的,所以要加上1。
#include <iostream> using namespace std; typedef long long ll; int digit[20]; int dp[20][20]; ll l,r; ll dfs(int len,int count,bool zero,bool limit){ if(len==0) return count; if(!zero&&!limit&&dp[len][count]) return dp[len][count]; ll cnt=0;int up_bound=limit?digit[len]:9; cnt+=dfs(len-1,count+(zero?0:1),zero,limit&&digit[len]==0); for(int i=1;i<=up_bound;i++){ cnt+=dfs(len-1,count,false,limit&&i==up_bound); } if(!limit&&!zero) dp[len][count]=cnt; return cnt; } ll solve(ll num){ int k = 0; while (num){ k++; digit[k]=num%10; num/=10; } return dfs(k,0,1,1); } int main(){ ios::sync_with_stdio(false); while (1) { cin >> l >> r; if(l==-1||r==-1) return 0; if (l == 0) cout << solve(r) + 1 << endl; else cout << solve(r) - solve(l - 1) << endl; } }
ex7:poj2282&&bzoj1833&&luogu2602&&zjoi1010
求区间里包含“0,1,2,3,4,5,6,7,8,9”的个数分别是多少
和ex6一毛一样嘛,,一个很直观的思路就是我们把ex6跑十遍吧,嗯正解就是这样(囍的不行),我觉着求“1,2,3,4,5,6,7,8,9”还要比“0”简单咯,不用考虑前导0,然后把代码稍微一改就可以了。哦对了!他这个鬼输入竟然l还能大于r,所以要判断swap一下,,,我一开始测样例输出了一堆负数让我受到了很大的惊吓。。。
#include <iostream> #include <cstring> using namespace std; typedef long long ll; int digit[10]; int dp[10][10][10]; int ans[10][2]; ll l,r; ll dfs0(int len,int count,bool zero,bool limit){ if(len==0) return count; if(!zero&&!limit&&dp[len][count][0]) return dp[len][count][0]; ll cnt=0;int up_bound=limit?digit[len]:9; cnt+=dfs0(len-1,count+(zero?0:1),zero,limit&&digit[len]==0); for(int i=1;i<=up_bound;i++){ cnt+=dfs0(len-1,count,false,limit&&i==up_bound); } if(!limit&&!zero) dp[len][count][0]=cnt; return cnt; } ll dfs(int len,int count,int num,bool limit){ if(len==0) return count; if(!limit&&dp[len][count][num]) return dp[len][count][num]; ll cnt = 0;int up_bound=limit?digit[len]:9; for(int i=0;i<=up_bound;i++){ cnt+=dfs(len-1,count+(i==num),num,limit&&i==up_bound); } if(!limit) dp[len][count][num]=cnt; return cnt; } void solve(ll num,int ind){ int k = 0; while (num){ k++; digit[k]=num%10; num/=10; } ans[0][ind]=dfs0(k,0,1,1); for(int i=1;i<=9;i++) ans[i][ind]=dfs(k,0,i,true); } void init(){ memset(dp,0, sizeof(dp)); memset(digit,0, sizeof(digit)); } int main(){ ios::sync_with_stdio(false); while (cin>>l>>r&&l&&r) { if(l>r) swap(l,r); init(); solve(r,0); init(); solve(l-1,1); for(int i=0;i<10;i++) cout<<ans[i][0]-ans[i][1]<<" "; cout<<endl; } }
ex8:8102上海大都会赛J题
能被各位数字和整除 数据范围(N<=1e12)
首先我们应该会求 “被1整除”,“被二整除”,“被三整除”这样子的吧,然后神妙的战法就出现了,我们可以枚举各位数字和,,,12*9=128,也就是跑100来遍就行了吧。。。
初始化要初始化成-1,因为可能有很多状态本身就没有符合条件的数。
#include <bits/stdc++.h> using namespace std; typedef long long ll; int t;ll n; int digit[17]; ll dp[17][120][120];//第i位,之前数位之和位j,对某个mod余数为k的满足条件的个数 int nowsum; ll dfs(int len, int sum,int mod,bool limit){ if(len==0) return (sum==nowsum&&mod==0); if(!limit&&dp[len][sum][mod]!=-1) return dp[len][sum][mod]; ll cnt = 0;int up_bound=limit?digit[len]:9; for(int i=0;i<=up_bound;i++){ if(sum+i>nowsum) break; cnt+=dfs(len-1,sum+i,(mod*10+i)%nowsum,limit&&i==digit[len]); } if(!limit) dp[len][sum][mod]=cnt; return cnt; } ll solve(ll num) { int k = 0; while (num) { k++; digit[k] = num % 10; num /= 10; } ll res = 0; for(int i=1;i<=9*k;i++) { nowsum = i; memset(dp,-1, sizeof(dp)); res += dfs(k,0,0, true); } return res; } int main(){ ios::sync_with_stdio(false); cin>>t; int cas = 0; while (t--){ cin>>n; cout<<"Case "<<++cas<<": "<<solve(n)<<endl; } return 0; }