[您有新的未分配科技點]數位dp:從懵X到板子
數位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)
Problem Description 杭州人稱那些傻乎乎粘嗒嗒的人為62(音:laoer)。杭州交通管理局經常會擴充一些的士車牌照,新近出來一個好消息,以後上牌照,不再含有不吉利的數字了,這樣一來,就可以消除個別的士司機和乘客的心理障礙,更安全地服務大眾。不吉利的數字為所有含有4或62的號碼。例如:62315 73418 88914都屬於不吉利號碼。但是,61152雖然含有6和2,但不是62連號,所以不屬於不吉利數字之列。 你的任務是,對於每次給出的一個牌照區間號,推斷出交管局今次又要實際上給多少輛新的士車上牌照了。 Input 輸入的都是整數對n、m(0<n≤m<1000000),如果遇到都是0的整數對,則輸入結束。 Output 對於每個整數對,輸出一個不含有不吉利數字的統計個數,該數值占一行位置。 Sample Input 1 100 0 0 Sample Output 80 題解: 我們依然定義f[i][j]數組,為前i位數第i位為j時的合法數的數量 初始化時f[0][0]=1; 那麽顯然當且僅當j!=4&&!(j==6&&k==2)時,能有轉移f[i][j]+=f[i-1][k]; 統計答案時,我們還是從高位向低位統計,當我們枚舉的數位j滿足j!=4&&!(x的第i+1位==6&&j==2)時可以統計答案。 需要註意的是:如果我們發現原數中的某一處出現了62或者4,我們直接結束統計,因為在高位相等的前提下更低的位數一定不合法了。 代碼見下: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
[您有新的未分配科技點]數位dp:從懵X到板子