poj3208 Apocalypse Someday[数位DP]
数位中出现至少3个连续的'6'的数字(称魔鬼数),询问满足要求的排名k的数。
经典题型。采用试填法。
递推做法:预处理出$i$位数字中满足要求的数(下记为'魔鬼数')。对每一位都从0到9试一遍,然而卡在了试填时试到6这个数时该怎么办,不太会做。然后才知道可以记录填到目前的上一位已有多少个连续的6,这样可以配合当前的计算出来。
所以就有这个$f[i][0]=9*(f[i-1][0]+f[i-1][1]+f[i-1][2]),f[i][1]=f[i-1][0],f[i][2]=f[i-1][1],f[i][3]=10*f[i-1][3]+f[i-1][2]$
其中0表示开头有0个6,1表示开头有一个6,2同理,3表示当前小于等于i位的魔鬼数的数量(也就相当于把各位数的魔鬼数累积起来)
注意是算入前导0的。比如有004566674,因为试填时他也算一种。而开头不必担心,看代码第1次循环就会发现考虑0的时候恰好把小于i位的魔鬼数数量都减掉了,说不太清楚,还是自己想想吧。
用k以辅助记录当前填过的末尾连续的6数量,用于处理填6时计算,每填一个6就多算一下。k到3时表示我已经诞生连续3个的6了,所以后面所有的填法都合法,这是要再单独特殊处理,看code。
其他就是常规的试填。
说两点细节:
注意边界处理 ,考虑预处理DP值的边界,尤其是f[0],f[1]等;
试填也要考虑边界。比如开始时。结束时(最后一位),这直接和f[0][0]的初始化产生了联系。所以两个边界都要看一下检查正确性。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cmath> 5 #include<algorithm> 6 #define dbg(x) cerr<<#x<<" = "<<x<<endl 7 #define ddbg(x,y) cerr<<#x<<" = "<<x<<" "<<#y<<" = "<<y<<endl 8 using namespace std; 9 typedef pair<int,int> pii; 10 typedef long long ll; 11 template<typename T>inline char MIN(T&A,T B){return A>B?A=B,1:0;} 12 template<typename T>inline char MAX(T&A,T B){return A<B?A=B,1:0;} 13 template<typename T>inline T _min(T A,T B){return A<B?A:B;} 14 template<typename T>inline T _max(T A,T B){return A>B?A:B;} 15 template<typename T>inline T read(T&x){ 16 x=0;int f=0;char c;while(!isdigit(c=getchar()))if(c=='-')f=1; 17 while(isdigit(c))x=x*10+(c&15),c=getchar();return f?x=-x:x; 18 } 19 ll f[11][4],rk,cnt; 20 int T; 21 inline void preprocess(){ 22 f[1][0]=9,f[1][1]=1;f[0][0]=1;//←注意边界处理 ,DP和试填都要检验一下 23 for(register int i=2;i<=10;++i)f[i][0]=9*(f[i-1][0]+f[i-1][1]+f[i-1][2]),f[i][1]=f[i-1][0],f[i][2]=f[i-1][1],f[i][3]=10*f[i-1][3]+f[i-1][2]; 24 } 25 26 int main(){//freopen("test.in","r",stdin);freopen("test.out","w",stdout); 27 read(T);preprocess();while(T--){ 28 read(rk);int x;for(x=3;x<=10;++x)if(rk<=f[x][3])break; 29 for(register int i=x,k=0;i;--i){ 30 for(register int j=0;j<=9;++j){ 31 cnt=f[i-1][3]; 32 if(k==3)cnt+=f[i-1][0]+f[i-1][1]+f[i-1][2]; 33 else if(j==6)for(register int l=2-k;l<3;++l)cnt+=f[i-1][l]; 34 if(rk<=cnt){ 35 if(k<3){if(j==6)++k;else k=0;} 36 printf("%d",j);break; 37 } 38 else rk-=cnt; 39 } 40 } 41 puts(""); 42 } 43 return 0; 44 }
然后讲一下记搜的,可以套模板。求第k大的数,可以转化为二分枚举区间[1,n]的右边界n,看[1,n]的满足要求的数,由于其随区间变大而单调增,所以是二分。通过不断调整,可以找到最小n使得区间[1,n]恰有k个满足要求的数。这个因为是套模板,所以很好写。但是二分的话会让复杂度多一个log。。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cmath> 5 #include<algorithm> 6 #define dbg(x) cerr<<#x<<" = "<<x<<endl 7 using namespace std; 8 typedef long long ll; 9 template<typename T>inline char MIN(T&A,T B){return A>B?A=B,1:0;} 10 template<typename T>inline char MAX(T&A,T B){return A<B?A=B,1:0;} 11 template<typename T>inline T _min(T A,T B){return A<B?A:B;} 12 template<typename T>inline T _max(T A,T B){return A>B?A:B;} 13 template<typename T>inline T read(T&x){ 14 x=0;int f=0;char c;while(!isdigit(c=getchar()))if(c=='-')f=1; 15 while(isdigit(c))x=x*10+(c&15),c=getchar();return f?x=-x:x; 16 } 17 int T; 18 ll L,R,f[12][2][2][2],b[12],aim; 19 ll dp(int len,bool las,bool las2,bool fil,bool limit){ 20 if(!len)return fil; 21 if(!limit&&~f[len][las][las2][fil])return f[len][las][las2][fil]; 22 ll cnt=0;int num=limit?b[len]:9; 23 for(register int i=0;i<=num;++i)cnt+=dp(len-1,i==6,las,fil||(las&&las2&&i==6),limit&&i==num); 24 return limit?cnt:f[len][las][las2][fil]=cnt; 25 } 26 inline ll solve(ll x){ 27 int k=0;while(x)b[++k]=x%10,x/=10; 28 return dp(k,0,0,0,1); 29 } 30 31 int main(){//freopen("tmp.txt","r",stdin);//freopen("test.out","w",stdout)£» 32 memset(f,-1,sizeof f); 33 read(T);while(T--){ 34 read(aim);L=666,R=1e10;ll mid; 35 while(L<R){ 36 mid=L+R>>1; 37 if(solve(mid)<aim)L=mid+1; 38 else R=mid; 39 } 40 printf("%lld\n",L); 41 } 42 return 0; 43 }