[您有新的未分配科技点]数位dp:从懵X到板子(例题:HDU2089 不要62)
数位dp主要用来处理一系列需要数数的问题,一般套路为“求[l,r]区间内满足要求的数/数位的个数”
要求五花八门……比如“不出现某个数字序列”,“某种数的出现次数”等等……
面对这种数数题,暴力的想法是枚举每个数,判断是否满足条件
比如这样:
#include<cstdio> using namespace std; typedef long long LL; LL l,r,cnt; int main() { scanf("%lld%lld",&l,&r); for(LL i=l;i<=r;i++) if(/*i符合条件*/)cnt++; printf("%lld",cnt); }
这样很显然会T......所以我们考虑利用一些奇怪的性质来数数(一般这些性质可以用来递推、或是dp一样的转移)
比如看下面一道例题:
对于给定闭区间[L,R],求非0数位出现的个数
sample input:23 233
sample output: 515
首先转化为calc[1~R]-calc[1~L-1](我们设数字的最低位为第1位,次低为位第2位,以此类推)
做法1:专门统计数位的方法:我们先预处理bin[i]为10的i次方,再预处理dp[i]表示在i位数的范围内(1~99...999(i个9))某种数字的个数
那么考虑dp[i]和dp[i-1]之间的转移:
首先,第i位的数字为0~9时都可以对第i位数字的dp值产生dp[i-1]的贡献
也即:[0~10...(i-1个0)...0)的末i-1位贡献+[10...(i-1个0)...0~20...(i-1个0)...0)的末i-1位贡献+……+[90...(i-1个0)...0~10...0(i个0))的末i-1位贡献
接着,后面i-1位为任意数是都会给第i位的数字产生+1的贡献,由排列组合知总共有bin[i-1]的贡献
所以转移是dp[i]=10*dp[i-1]+bin[i-1](其实化简一下式子,也可以写成dp[i]=i*bin[i-1])
有了这个dp数组我们考虑如何计算对于某个数字x计算[1~x]某个数位st的出现个数,这个过程和之前递推的过程很相似
我们枚举x的第i位位bit
1° bit>st 第i位取0~bit时都可以增加dp[i-1]的贡献,而0~bit里肯定会有这一位取st的情况,贡献加上bin[i-1]
因此ans+=bin[b-1]+d*dp[b-1];
2° bit==st 设tail=x%bin[i-1](x的前i-1位数的值)第i位取0~bit时都可以增加dp[i-1]的贡献,但是当第i位取st(bit)时,只有tail+1种数比x小,因此贡献只有tail+1
此时ans+=tail+1+d*dp[b-1];
3°bit<st 第i位取0~bit时都可以增加dp[i-1]的贡献,但0~bit取不到st
所以ans+=d*dp[b-1];
但是在统计完答案之后,如果我们统计的st==0,前导0被多统计了(第i位不能取0,因此多加了bin[0]+bin[1]+...+bin[数的位数-1]),在最后减去即可
代码见下:
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #include<cmath> 5 using namespace std; 6 typedef long long LL; 7 typedef unsigned long long ULL; 8 LL l,r,bin[20],dp[20]; 9 inline void intn() 10 { 11 bin[0]=1;//bin[i]是10的j次方 12 for(int i=1;i<=18;i++) 13 bin[i]=bin[i-1]*10,dp[i]=dp[i-1]*10+bin[i-1]; 14 //第i位是0~9的时候,都会有+dp[i-1]的新贡献 15 //而后面i-1位任一情况时都会给第i位的数字贡献+1,一共有bin[i-1]种情况 16 } 17 inline LL calc(LL sum,int state) 18 { 19 LL tmp=sum; 20 int b=0,d;LL ans=0;//tail是末几位的数字大小 21 while(sum) 22 { 23 d=sum%10;sum/=10,b++; 24 if(d>state)ans+=bin[b-1]+d*dp[b-1]; 25 //对本位(指第i位)有bin[b-1](末i-1位随意选择)的贡献,第i位是0~d的时候都会有贡献 26 else if(d==state)ans+=(tmp%bin[b-1])+1+d*dp[b-1]; 27 //本位的贡献仅限于末位的数字大小+1(全0也会提供贡献) 28 else ans+=d*dp[b-1];//本位没有贡献 29 } 30 d=0; 31 if(state==0) 32 //前导0被重复计算了,一位数多算了一个0,两位数多算了10个零,如此我们减去多算的就好了 33 while(tmp) 34 ans-=bin[d++],tmp/=10; 35 return ans; 36 } 37 inline LL work(LL data) 38 { 39 LL ans=0; 40 for(int i=1;i<=9;i++) 41 ans+=calc(data,i); 42 return ans; 43 } 44 int main() 45 { 46 scanf("%lld%lld",&l,&r); 47 intn(); 48 LL ansr=work(r); 49 if(l==0)printf("%lld",ansr); 50 else printf("%lld",ansr-work(l-1)); 51 }
做法2:正经(?)dp法
我们设f[i][j]为x的前i位数并且第i位数为j时j的出现次数,依然预处理bin[i]同上
那么类别上面的做法
首先f[i][j]+=Σ(f[i-1][k],0<=k<=9);接着,如果j不是0,我们在加上第i位对这个数的贡献bin[i-1](其实就实现了上面统计时最后消去前导0的过程)
要注意的一点是[0~10...(i-1个0)...0)等都是一个左闭右开区间,如果计算work(R)-work(L-1),就无法统计R的贡献
因此我们计算时也使用左闭右开区间,计算work(R+1)-work(L)就好啦
代码见下:
#include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; typedef long long LL; typedef unsigned long long ULL; LL l,r,bin[20]; LL f[30][15];//f[i][j]表示前i位,第i位数字为j的j出现次数 int bit[20]; inline void intn() { bin[0]=1;//bin[i]是10的j次方 for(int i=1;i<=18;i++) bin[i]=bin[i-1]*10; for(int i=1;i<=18;i++) for(int j=0;j<10;j++) { for(int k=0;k<10;k++) f[i][j]+=f[i-1][k]; f[i][j]+=(j==0)?0:bin[i-1]; } } inline LL work(LL x){ int cnt=0,b=0;while(x)bit[++b]=x%10,x/=10;//bit表示每一位 LL ans=0; for(int i=b;i;i--) { for(int j=0;j<bit[i];j++) ans+=f[i][j];//第i位是0~bit[i]-1的情况 ans+=bin[i-1]*bit[i]*cnt;//第i位是bit[i]的情况 //这里的cnt记录了“第i位以前(第i+1位到最高位)有多少个数不是0”,因为他们也算是非0数位。 if(bit[i])cnt++; } return ans; } int main() { scanf("%lld%lld",&l,&r); intn(); LL ansr=work(r+1); if(l==0)printf("%lld",ansr); else printf("%lld",ansr-work(l)); }
下面我们再来一道例题:[HDU2089]不要62
不要62
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
1 #include<cstdio> 2 #include<cstring> 3 using namespace std; 4 typedef long long LL; 5 int l,r,bit[15],f[10][15]; 6 inline void intn() 7 { 8 f[0][0]=1; 9 for(int i=1;i<=8;i++) 10 for(int j=0;j<10;j++) 11 { 12 if(j==4)continue; 13 for(int k=0;k<10;k++) 14 { 15 if(j==6&&k==2)continue; 16 f[i][j]+=f[i-1][k]; 17 } 18 } 19 } 20 inline int work(int x) 21 { 22 memset(bit,0,sizeof(bit)); 23 int b=0,cnt=0,ans=0; 24 while(x)bit[++b]=x%10,x/=10; 25 for(int i=b;i;i--) 26 { 27 for(int j=0;j<bit[i];j++) 28 { 29 if(j==4||(bit[i+1]==6&&j==2))continue; 30 ans+=f[i][j]; 31 } 32 if(bit[i]==4||(bit[i+1]==6&&bit[i]==2))break; 33 } 34 return ans; 35 } 36 int main() 37 { 38 intn(); 39 while(scanf("%d%d",&l,&r)==2) 40 { 41 if(l==r&&r==0)break; 42 printf("%d\n",work(r+1)-work(l)); 43 } 44 } 45