数位dp
强烈推荐https://blog.csdn.net/wust_zzwh/article/details/52100392,写的真的太棒了Orz,(所以下面的都引自这篇博客咳咳
一、模板
数位dp是一种计数用的dp,一般就是要统计一个区间[le,ri]内满足一些条件数的个数。所谓数位dp,是在数位上进行dp,实质就是换一种暴力枚举的方式,使得新的枚举方式满足dp的性质,然后记忆化就可以了。
如果是暴力枚举:
for(int i=le;i<=ri;i++) if(right(i)) ans++;
而数位dp的话,是控制上界枚举,从最高位开始往下枚举,例如:ri=213,那么我们从百位开始枚举:百位可能的情况有0,1,2(觉得这里枚举0有问题的继续看)
然后每一位枚举都不能让枚举的这个数超过上界213(下界就是0或者1,这个次要),当百位枚举了1,那么十位枚举就是从0到9,因为百位1已经比上界2小了,后面数位枚举什么都不可能超过上界。所以问题就在于:当高位枚举刚好达到上界是,那么紧接着的一位枚举就有上界限制了。具体的这里如果百位枚举了2,那么十位的枚举情况就是0到1,如果前两位枚举了21,最后一位之是0到3(这一点正好对于代码模板里的一个变量limit 专门用来判断枚举范围)。最后一个问题:最高位枚举0:百位枚举0,相当于此时我枚举的这个数最多是两位数,如果十位继续枚举0,那么我枚举的就是个位数咯,因为我们要枚举的是小于等于ri的所以数,当然不能少了位数比ri小的咯!(这样枚举是为了无遗漏的枚举,不过可能会带来一个问题,就是前导零的问题,模板里用lead变量表示,不过这个不是每个题目都是会有影响的,可能前导零不会影响我们计数,具体要看题目)
然后是大神的模板
typedef long long ll; int a[20]; ll dp[20][state];//不同题目状态不同 ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零 { //递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了 if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */ //第二个就是记忆化(在此前可能不同题目还能有一些剪枝) if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state]; /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/ int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了 ll ans=0; //开始计数 for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了 { if() ... else if()... ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的 /*这里还算比较灵活,不过做几个题就觉得这里也是套路了 大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论 去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目 要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类, 前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/ } //计算完,记录状态 if(!limit && !lead) dp[pos][state]=ans; /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/ return ans; } ll solve(ll x) { int pos=0; while(x)//把数位都分解出来 { a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行 x/=10; } return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛 } int main() { ll le,ri; while(~scanf("%lld%lld",&le,&ri)) { //初始化dp数组为-1,这里还有更加优美的优化,后面讲 printf("%lld\n",solve(ri)-solve(le-1)); } }
然后有些优化什么的还是去看原博客吧qwq
二、例题
1.hdu 2089 不要62
不要连续的62,所以只需要记录上一位是不是6就可以了,f[pos][sta]表示第pos位,上一位是不是6
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> #define eps 0.000001 using namespace std; int l,r; int f[20][2],a[20]; int dfs(int pos,int pre,int sta,bool limit) { if (pos==-1) return 1; if (!limit && f[pos][sta]!=-1) return f[pos][sta]; int up; if (limit) up=a[pos]; else up=9; int tmp=0; for (int i=0;i<=up;i++) { if (pre==6&&i==2|| i==4) continue; tmp+=dfs(pos-1,i,i==6,limit&&i==a[pos]); } if (!limit) f[pos][sta]=tmp; return tmp; } int solve(int x) { int pos=0; while (x) { a[pos++]=x%10; x/=10; } return dfs(pos-1,-1,0,1); } int main() { while (scanf("%d%d",&l,&r)&&l+r) { memset(f,-1,sizeof(f)); printf("%d\n",solve(r)-solve(l-1)); } return 0; }
2.hdu 4734 F(x)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> #define eps 0.000001 using namespace std; const int N=1e4+50; int T,r,all,A; int f[20][N],a[20]; int F(int x) { if (x==0) return 0; int ans=F(x/10); return ans*2+(x%10); } int dfs(int pos,int sum,bool limit) { if (pos==-1) return sum<=all; if (sum>all) return 0; if (!limit && f[pos][all-sum]!=-1) return f[pos][all-sum]; int up= limit? a[pos] :9; int tmp=0; for (int i=0;i<=up;i++) { tmp+=dfs(pos-1,sum+i*(1<<pos),limit&&i==a[pos]); } if (!limit) f[pos][all-sum]=tmp; return tmp; } int solve(int x) { int pos=0; while (x) { a[pos++]=x%10; x/=10; } return dfs(pos-1,0,1); } int main() { scanf("%d",&T); memset(f,-1,sizeof(f)); for (int ca=1;ca<=T;ca++) { scanf("%d%d",&A,&r); all=F(A); printf("Case #%d: %d\n",ca,solve(r)); } return 0; }
3.poj 3252 Round Numbers
二进制中0的数量要不少于1的数量的数的个数,f[pos][num],到当前数位pos,0的数量减去1的数量不少于num的方案数,一个简单的问题,中间某个pos位上num可能为负数(这不一定是非法的,因为我还没枚举完嘛,只要最终的num>=0j就可以),因为最小就-32吧,直接加上32,把32当0用。这题主要是要想讲一下lead的用法,显然我要统计0的数量,前导零是有影响的,所以!lead&&!limit才能dp。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <algorithm> //#include <bits/stdc++.h> #define eps 0.000001 using namespace std; const int N=1e4+50; int l,r; int f[35][70],a[35]; int dfs(int pos,int sta,bool lead,bool limit) { if (pos==-1) return sta>=32; if (!lead && !limit && f[pos][sta]!=-1) return f[pos][sta]; int up= limit? a[pos] :1; int tmp=0; for (int i=0;i<=up;i++) { if (lead && i==0) tmp+=dfs(pos-1,sta,lead,limit && i==a[pos]); else tmp+=dfs(pos-1,sta+(i==0?1:-1),lead && i==0,limit && i==a[pos]); } if (!lead && !limit) f[pos][sta]=tmp; return tmp; } int solve(int x) { int pos=0; while (x) { a[pos++]=x&1; x>>=1; } return dfs(pos-1,32,1,1); } int main() { memset(f,-1,sizeof(f)); scanf("%d%d",&l,&r); printf("%d\n",solve(r)-solve(l-1)); return 0; }
4.bzoj 1799 self同类分布
一个数是它自己数位和的倍数。
枚举数位和,因为总就162,然后问题就变成了一个数%mod=0,mod是枚举的,想想状态:dp[pos][sum][val],当前pos位上数位和是sum,val就是在算这个数%mod,(从高位算 *10+i),因为我们枚举的数要保证数位和等于mod,还要保证这个数是mod的倍数,很自然就能找到这些状态,显然对于每一个mod,val不能保证状态唯一,所以直接对每一个mod,memset一次。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <algorithm> //#include <bits/stdc++.h> #define eps 0.000001 #define ll long long using namespace std; const int N=1e4+50; int T; ll l,r; ll f[20][170][170]; int a[20]; ll dfs(int pos,int sum,int val,int mod,bool limit) { if (sum-9*pos-9>0) return 0; if (pos==-1) return sum==0 && val==0; if (!limit && f[pos][sum][val]!=-1) return f[pos][sum][val]; int up= limit? a[pos] :9; ll tmp=0; for (int i=0;i<=up;i++) { if (sum-i<0) break; tmp+=dfs(pos-1,sum-i,(val*10+i)%mod,mod,limit && i==a[pos]); } if (!limit) f[pos][sum][val]=tmp; return tmp; } ll solve(ll x) { int pos=0; while (x) { a[pos++]=x%10; x/=10; } ll ans=0; for (int i=1;i<=pos*9;i++) { memset(f,-1,sizeof(f)); ll tmp=dfs(pos-1,i,0,i,1); ans+=tmp; } return ans; } int main() { /* scanf("%d",&T); for (int ca=1;ca<=T;ca++) { scanf("%lld",&r); printf("Case %d: %lld\n",ca,solve(r)); }*/ scanf("%lld%lld",&l,&r); printf("%lld\n",solve(r)-solve(l-1)); return 0; }