数位DP
经典的数位Dp是要求统计符合限制的数字的个数。
一般的形式是:求区间[n,m]满足限制f(1)、f(2)、f(3)等等的数字的数量是多少。条件 f(i)一般与数的大小无关,而与数的组成有关。
善用不同进制来处理,一般问题都是10进制和二进制的数位dp。
数位dp的部分一般都是很套路的,但是有些题目在数位dp外面套了一个华丽的外衣,有时我们难以看出来。
例题:windy数
精华都在代码的注释里呢,好好看吧
代码:
#include<bits/stdc++.h> using namespace std; typedef long long ll; ll l,r; int num[20]; ll f[20][20];/*按照题目定维数*/ ll dfs(int i,int last,bool have,bool lim) /*需要传递的各种参数,视题目而定*/ /*本题中分别表示当前到了第几位,上一位是什么, 有没有前导0,有没有顶上界*/ /*一般需要有的是位数,上界,其他视情况而定*/ { if(i==0) return 1;/*判断是否已经枚举完,返回1就表示你枚举的数是合法的*/ if(!have&&!lim&&f[i][last]!=-1)/*判断边界条件以及是否已经搜索过*/ return f[i][last]; int up=0;/*这一位枚举的上界*/ if(lim==1) up=num[i];/*上一位顶到上界的话对这一位就有限制*/ else up=9;/*如果上一位没有顶到上界的话这一位就可以随意*/ ll ans=0; int op=0;/*表示这一位选择的数是什么*/ for(int j=0;j<=up;j++) { if(abs(j-last)<2) continue;/*判断不合法条件*/ op=j; if(have&&j==0) op=-2; ans+=dfs(i-1,op,op==-2,lim&&j==up);/*继续记搜,一定要注意各种限制条件的传递*/ } if(!have&&!lim) f[i][last]=ans;/*在一定条件下记录记搜的答案*/ return ans; } ll work(ll x) { memset(num,0,sizeof(num)); int tot=0; while(x)/*把数字的各个位拆到数组中存储*/ { num[++tot]=x%10; x/=10; } memset(f,-1,sizeof(f)); return dfs(tot,-2,1,1); } int main() { scanf("%lld%lld",&l,&r); printf("%lld",work(r)-work(l-1));/*前缀和思想*/ }
来看另一道题
洛谷P4317 花神的数论题
题意就是说求1~n每个数二进制含有1的个数的乘积
也是数位DP
设f[i][j]表示前i位有j个1的方案数
然后记忆化搜索
代码:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const ll mod=10000007; ll n; ll f[51][51]; int num[51]; ll dfs(int i,int cnt,bool lim) { if(i==0) return max(cnt,1); if(!lim&&f[i][cnt]!=-1) return f[i][cnt]; ll ans=1; int up=0; if(lim==1) up=num[i]; else up=1; for(int j=0;j<=up;j++) { ans=(ans*dfs(i-1,j==1?cnt+1:cnt,lim&&j==up))%mod; } if(!lim&&f[i][cnt]==-1) f[i][cnt]=ans; return ans; } inline void work(ll x) { int cnt=0; while(x) { num[++cnt]=x%2; x/=2; } cout<<dfs(cnt,0,1); } int main() { scanf("%lld",&n); memset(f,-1,sizeof(f)); work(n); }
关于数位dp的经验
1:注意很多时候带进去是n==0要特殊处理。
2:还有一般问[m,n],我们求[1,n]-[1,m-1]但是有的时候m为0就炸了。
3:求所有包含49的数,其实就是(总数-所有不包含49的数)。前者的化需要有两维限制,一个是上一位是什么,一个是之前有没有49。但是后 者只需要记一个上一位是什么。就能好写一些。
4:一般问题的数位dp部分,都是套路,但是这并不代表它外面“华丽的外衣”和与其他算法结合的的部分也是无脑的。要看出它是考数位dp,要看出问题怎么变化一下就是数位dp了。
5:dp初始化memset要置为-1。不能置为0!!!!!!因为有很多时候dp值就应该是0,然后我们如果误以为是因为之前没有计算,重新计算的话,就会tle。
6:既然是记忆化搜索,那就可以剪枝!!!!可行性剪枝!!
7:注意windy数的情况,有时前导0也需要记的!!!