kuangbin带你飞 - 专题十五 - 数位dp
https://vjudge.net/contest/70324
A - Beautiful numbers
统计区间内的,被数位上各个为零数字整除的数的个数。
下面是暴力的数位dp写法,绝对会TLE的,因为这个要深入到每个数字的最后才能判断是否合法。因为(错误的状态设计导致完全变成暴力dfs搜索)记忆化的意义在询问不多的时候用处不大就去掉了。果然2400分的题不能这么暴力。
#include<bits/stdc++.h> using namespace std; #define ll long long int a[19]; int d[10]; ll dfs(int pos,bool limit,ll sum) { if(pos==-1) { for(int i=1; i<=9; i++) { if(d[i]) { if(sum%i) return 0; } } return 1; } int up=limit?a[pos]:9; ll ans=0; for(int i=0; i<=up; i++) { if(i) d[i]++; ans+=dfs(pos-1,limit && i==a[pos],sum*10ll+i); if(i) d[i]--; } return ans; } ll solve(ll x) { //特殊处理0 if(x==0) return 1; int pos=0; while(x) { a[pos++]=x%10; x/=10; } memset(d,0,sizeof(d)); return dfs(pos-1,true,0); } int main() { int t; scanf("%d",&t); ll le,ri; while(t--) { scanf("%lld%lld",&le,&ri); printf("%lld\n",solve(ri)-solve(le-1)); } }
没想出来怎么解决,去查了题解,题解暗示说,这样是和最小公倍数有关的。好像的确很有道理,细节只能自己想了。
首先考虑1~9的最小公倍数,也就是 $1*2^3*3^2*5*7=2520$ ,题解提到一个充要条件,就是一个数假如要能被某些数整除,等价于被这些数的最小公倍数整除,这个充要条件的正确性可以由质因数分解得知,就是说这个数的质因数分解必须比他的各个数位的质因数分解“高”,也就比各个数位的质因数分解的“轮廓”也就是最小公倍数“高”。(注: $lcm(a,b)=\frac{a*b}{gcd(a,b)}$ ,且满足结合律)
然后怎么计数呢?这里受到之前做的数位dp的启发,由下一位的数位dp推出上一位的状态。设计状态的时候借鉴别人的思路, $dp[i][j][k]$ 表示 $i$ 位数中能整除前面数位的最小公倍数 $j$ 的且模2520的余数为 $k$ 的数的个数。
假设某一位的枚举值为 $a$ ,那么 $dp[i][j][k]+=dp[i-1][lcm(j,a)][(k*10+a)%p]$ ,比如现在要求的数位是2836,现在已经枚举过了2,当前在处理8,枚举千位上的值,i=2,j=2,k=2,当千位枚举3时,向下转移一个i=1,j=6,k=23,意义是显然的,因为你多了一个3,必定要能整除最小公倍数6。记忆化的时候要注意,数位受限时不能取用dp值也不能更新dp值。
最后还要注意这样做会MLE,改进的方法是给每个可能的1~9的任意组合生成的最小公倍数都生成一个id值或者(假的)hash值,方法是枚举2520的各个质因数统计最后发现是48个。
#include<bits/stdc++.h> using namespace std; #define ll long long int a[19]; ll dp[19][48][2520]; int id[2521]; void gen_id() { int top=0; for(int i=1; i<=8; i*=2) { for(int j=1; j<=9; j*=3) { for(int k=1; k<=5; k*=5) { for(int l=1; l<=7; l*=7) { id[i*j*k*l]=top++; } } } } } ll dfs(int pos,bool limit,int lcm,int sum) { if(pos==-1){ return sum%lcm==0; } if(!limit&&dp[pos][id[lcm]][sum]!=-1) return dp[pos][id[lcm]][sum]; int up=limit?a[pos]:9; ll ans=0; for(int i=0; i<=up; i++) { ans+=dfs(pos-1,limit && i==a[pos],i?(i*lcm)/__gcd(i,lcm):lcm,(sum*10+i)%2520); } return !limit?dp[pos][id[lcm]][sum]=ans:ans; } ll solve(ll x) { //特殊处理0 if(x==0) return 1; int pos=0; while(x) { a[pos++]=x%10; x/=10; } return dfs(pos-1,true,1,0); } int main() { memset(dp,-1,sizeof(dp)); gen_id(); int t; scanf("%d",&t); ll le,ri; while(t--) { scanf("%lld%lld",&le,&ri); printf("%lld\n",solve(ri)-solve(le-1)); } }
最后需要注意是 return !limit?dp[pos][id[lcm]][sum]=ans:ans; ,是当不受限的时候才记录dp,这里WA的一发,不过因为是有两组数据所以很快联想到了。
B - XHXJ's LIS
C - 不要62
当然有更简单的数位dp写法。根据自动机的知识我们只需要记录上一位是不是6就可以了。最后还WA了一发是因为dp的第二维开小了。
#include<bits/stdc++.h> using namespace std; #define ll long long int a[6]; ll dp[6][2]; ll dfs(int pos,bool limit,int st) { if(pos==-1){ return 1; } if(!limit&&dp[pos][st]!=-1) return dp[pos][st]; int up=limit?a[pos]:9; ll ans=0; for(int i=0; i<=up; i++) { if(i==4) continue; if(st==1&&i==2) continue; ans+=dfs(pos-1,limit && i==a[pos],i==6); } return !limit?dp[pos][st]=ans:ans; } ll solve(ll x) { //特殊处理0 if(x==0) return 1; int pos=0; while(x) { a[pos++]=x%10; x/=10; } return dfs(pos-1,true,0); } int main() { memset(dp,-1,sizeof(dp)); ll le,ri; while(~scanf("%lld%lld",&le,&ri)) { if(le==0&&ri==0) break; printf("%lld\n",solve(ri)-solve(le-1)); } }
E - Round Numbers
这里是二进制的数位dp,首先先把基数改成2。然后我们思考怎么设计状态可以使得子问题容易重复,一个很显然的设计方法就是dp[i][j]表示i位数,有j个0的数的个数。那么转移的时候每一步可以选择0或1,每次选择0的时候cnt0-1,最后pos==-1的时候要判断cnt0是否恰为0。需要注意的是虽然在求解7位数是只有3个0的状态是没有用的,但是不代表他不需要被计算,因为在11位数的时候可以先选1个前导1,3个0,转移到7位数的状态,这时候7位数选3个是有用的。
再想想前导0会有什么影响呢?因为前导0中的0是不算的,所以要分开处理一下。
但是上面的状态设计方法是有问题的,因为i位数中有j个0的数的个数不容易区分前导0的贡献,导致dp[i][j]的值和实际要求的不一致,在从高位向低位转移时“前导0”是允许存在的,但是单独计算的时候是不可以的。一个解决的办法是再引入cnt1变成dp[i][j][k],表示包括前导0的i位数中,j个非前导0,k个1的数的个数。这样可以顺利地区分前导0带来的影响。
#include<bits/stdc++.h> using namespace std; #define ll long long int a[32]; ll dp[32][32][32]; ll dfs(int pos,bool limit,bool lead,int cnt0,int cnt1) { if(pos==-1){ return cnt0>=cnt1; } if(!limit&&dp[pos][cnt0][cnt1]!=-1) return dp[pos][cnt0][cnt1]; int up=limit?a[pos]:1; ll ans=0; for(int i=0; i<=up; i++) { ans+=dfs(pos-1,limit&&i==a[pos],lead&&i==0,cnt0+((!lead)&&(i==0)),(cnt1+int(i==1))); } return (!limit)?dp[pos][cnt0][cnt1]=ans:ans; } ll solve(ll x) { int pos=0; while(x) { a[pos++]=x%2; x/=2; } return dfs(pos-1,true,true,0,0); } int main() { memset(dp,-1,sizeof(dp)); ll le,ri; while(~scanf("%lld%lld",&le,&ri)) { printf("%lld\n",solve(ri)-solve(le-1)); } }
又被运算符结合坑了,加法运算符比逻辑与运算符的优先级还要高。
上面的代码意思是,dp[i][j][k]表示包括前导0的i位数中,j个非前导0,k个1的数的个数。而lead的真假已经包含在上述的三维(其实只需要两维)中了,所以dp[i][j][k]的存储只受limit影响。
G - B-Numbers
数位dp的水题,想清楚状态机怎么运行就可以了。
st0:当前”“,当遇到1时转到st1。
st1:当前”1“,当遇到1时回到本身,当遇到3时转到st2,否则转到st0。
st2:已发现”13“,无论遇到什么都是回到本身。
#include<bits/stdc++.h> using namespace std; #define ll long long const int MAX_LEN=10; int di[MAX_LEN+1]; ll dp[MAX_LEN+1][3][13]; ll dfs(int len,bool limit,bool lead,int st,int r) { if(len==0){ return (st==2)&&(r%13==0); } if(!limit&&dp[len][st][r]!=-1) return dp[len][st][r]; int up=limit?di[len]:9; ll ans=0; for(int i=0; i<=up; i++) { if(st==2){ ans+=dfs(len-1,limit&&i==di[len],lead&&i==0,st,(r*10+i)%13); } else{ int nst=0; if(i==1) nst=1; else if(i==3){ if(st==1) nst=2; } ans+=dfs(len-1,limit&&i==di[len],lead&&i==0,nst,(r*10+i)%13); } } return (!limit)?dp[len][st][r]=ans:ans; } ll solve(ll x) { if(x==0) return 0; int len=0; while(x) { di[++len]=x%10; x/=10; } return dfs(len,true,true,0,0); } int main() { memset(dp,-1,sizeof(dp)); ll ri; while(~scanf("%lld",&ri)) { printf("%lld\n",solve(ri)); } }
献上越写越短的模板。上面这个个位index为1,也有他的好处。
H - F(x)
给定两个数A,B,求不超过B且权不超过A的权的数的数目。一开始错误地估计了权的上界,导致自己把记忆化给去掉了。实际上权不可能超过20000,具体是多少? $9*\sum\limits_{i=1}^{8}2^i\approx9*2^9$ ,5000多一点吧?
#include<bits/stdc++.h> using namespace std; #define ll long long int a[10]; int dp[10][20005]; int weight; int dfs(int pos,bool limit,int rw) { int base=1ll<<pos; if(pos==-1){ return 1; } if(!limit&&~dp[pos][rw]) return dp[pos][rw]; int up=limit?a[pos]:9; ll ans=0; for(int i=0; i<=up; i++) { if(i*base>rw) break; ans+=dfs(pos-1,limit && i==a[pos],rw-i*base); } return !limit?dp[pos][rw]=ans:ans; } int solve(int x) { //特殊处理0 if(x==0) return 1; int pos=0; while(x) { a[pos++]=x%10; x/=10; } return dfs(pos-1,true,weight); } void cal_weight(int A){ weight=0; int base=1; while(A) { weight+=base*(A%10); A/=10; base<<=1; } } int main() { memset(dp,-1,sizeof(dp)); int t; scanf("%d",&t); for(int i=0;i<t;i++){ int A,B; scanf("%d%d",&A,&B); cal_weight(A); printf("Case #%d: %d\n",i+1,solve(B)); } }
I - BCD Code
这个一看就知道是用自动机就可以了。但是具体怎么建我就陷入了沉思。所以说瘸腿就是瘸腿啊,AC自动机还是要会。多模字符串匹配AC自动机。