1. 程式人生 > >[您有新的未分配科技點]數位dp:從懵X到板子

[您有新的未分配科技點]數位dp:從懵X到板子

消息 初始化 work || lld 位數 align 一個 過程

數位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到板子