數位DP入門


 

切換至❤新頁面,體驗更加哦

 

數位dp,是一種用來計數的dp

如果現在給你一道題,需要你求在區間[l,r]內滿足條件的解的個數,我們很容易想到去暴力枚舉,但要是數據范圍太大這種辦法就行不通了,這時候數位dp就派上了用場,所謂數位就是把一個數拆成一個一個進制位,然后逐一比較看是否滿足題目要求,這其實也是一種暴力方法,只不過時間復雜度小了很多

那么到底要如何做呢?下面我們來看一道例題

HDU2089

概括一下題目意思

就是給你一個區間[n,m],要你求區間內不含"62"或"4"的數字的個數,如8134(含4),21262455(含62)均不滿足題意,而61342這種"6"和"2"並不連在一起的數字則滿足題意

直接統計對於暴力枚舉很好求,但是對於數位dp並不容易,所以我們還需要用到差分的思想,即統計0到b+1(注意不是b,至於為什么后面會講)和0到a的滿足條件的個數,再兩者相減

進一步化簡,求0到i位數不含4和62的個數

i=1,求0~9的滿足條件的個數

i=2,求0~99的滿足條件的個數

i=3,求0~999的滿足條件的個數

i=4,求0~9999的滿足條件的個數

...

用dp[i][0]表示i位數中幸運數的個數

用dp[i][1]表示i位數中以2開頭的幸運數的個數

用dp[i][2]表示i位數中非幸運數的個數

那么,就有以下的遞推公式

dp[i][0]=dp[i-1][0]*9-dp[i-1][1]//表示前i-1位數字中的幸運數前面加上除4以外的0~9的其他數字,共9個,還要減去前i-1位數字中的以2開頭的幸運數加上這一位以6開頭的數字的個數

dp[i][1]=dp[i-1][0]//表示前i-1位數字中的幸運數加上這一位的2

dp[i][2]=dp[i-1][2]*10+dp[i-1][1]+dp[i-1][0]//表示前面已經不合法的數字這一位無論放什么都不合法,所以0~9隨便放,加上前i-1位數字中的以2開頭的幸運數加上這一位的6,再加上前i-1位數字中的幸運數加上這一位的4的個數

初始值 dp[0][0]=1,其他均為0

根據初始值和遞推公式,我們就能得到從0到任意i位數字的吉利數字的個數。

找到0 ~ n 的吉利數字的個數

我們先求出0 ~ n 之間非吉利數字的個數,用總數減去即可。那,非吉利數字的個數怎么求呢?

用具體的數字舉例來說吧:設 n = 583626

用digit[10]記錄n+1每一位對應的數字,此例中有6位數字(令cnt = 6 表示數字位數),分別是

digit[6] = 5

digit[5] = 8

digit[4] = 3

digit[3] = 6

digit[2] = 2

digit[1] = 7

digit[0] = 任意數字,占位用的

用sum記錄非吉利數字的個數,初始化為0

需要一個bool量 flag,記錄是否出現了非吉利數字。初始化為false, 未出現。

我們從數字的最高位起進行判斷:digit[6] = 5, 我們求 0 ~ 499999 之間非吉利數的個數。

  首先:加上0 ~ 99999中所有非吉利數字前面添加0~4的任意一個數字的情況 sum += dp[5][2] * digit[6]

  其次:5大於4,故我們要加上 0~99999中所有吉利數字前面添加4的情況 sum += dp[5][0]

接着,判斷第5位digit[5] = 8,即判斷500000 ~ 579999 之間的非吉利數字的個數,其實就是判斷0 ~ 79999之間的,前面的數字不是6就沒有什么用

  首先:加上0 ~ 9999中所有非吉利數字前面添加0~7的任意一個數字的情況 sum += dp[4][2] * digit[5]

  其次:8大於4,故我們要加上 0~9999中所有吉利數字前面添加4的情況 sum += dp[4][0]

  此外:8大於6,故我們要加上0~9999中所有以2開頭的吉利數字前添加6的情況 sum += dp[4][1]

接着,判斷第4位digit[4] = 3,即判斷580000 ~ 582999 之間的非吉利數字的個數,其實就是判斷0 ~ 2999之間的

  首先:加上0 ~ 999中所有非吉利數字前面添加0~2的任意一個數字的情況 sum += dp[3][2] * digit[4]

  其次:2小於4,沒有需要特別考慮的

  此外:2小於6,沒有需要特別考慮的

接着,判斷第3位digit[3] = 6,即判斷583000 ~ 583599 之間的非吉利數字的個數,其實就是判斷0 ~ 599之間的

  首先:加上0 ~ 99中所有非吉利數字前面添加0~5的任意一個數字的情況 sum += dp[2][2] * digit[3]

  其次:6大於4,故我們要加上 0~99中所有吉利數字前面添加4的情況 sum += dp[2][0]

接着,判斷第2位digit[2] = 2,即判斷583600 ~ 583619 之間的非吉利數字的個數,其實就是判斷0 ~ 19之間的,

  首先:加上0 ~ 9中所有非吉利數字前面添加0~1的任意一個數字的情況 sum += dp[1][2] * digit[2]

  其次:2小於4,沒有需要特別考慮的

  此外:2小於6,沒有需要特別考慮的

  但是,需要注意的是,這里判斷的數字出現了62,我們要把flag標識為true。

最后,判斷第1位digit[1] = 7, 判斷583620 ~ 583626但是這里flag為true了,表示前面的數字里面已經包含了非吉利數字,所以后面需要把所有的數字情況都加入到非吉利里面。(正是因為每次判斷的數字末尾都比該位的數字少1,所以最開始要記錄n + 1 的值)

sum += digit[1] * dp[0][2] + digit[1] * dp[0][0]

 1 #include<bits/stdc++.h>
 2 #define in(i) (i=read())
 3 using namespace std;
 4 int read() {
 5     int ans=0,f=1; char i=getchar();
 6     while(i<'0'||i>'9') {if(i=='-') f=-1; i=getchar();}
 7     while(i>='0'&&i<='9') {ans=(ans<<3)+(ans<<1)+i-'0'; i=getchar();}
 8     return ans*f;
 9 }
10 int dp[10][3],digit[15];
11 void init() {
12     memset(dp,0,sizeof(dp));
13     dp[0][0]=1;
14     for(int i=1;i<=8;i++) {
15         dp[i][0]=dp[i-1][0]*9-dp[i-1][1];
16         dp[i][1]=dp[i-1][0];
17         dp[i][2]=dp[i-1][2]*10+dp[i-1][1]+dp[i-1][0];
18     }
19 }
20 int solve(int x)
21 {
22     memset(digit,0,sizeof(digit));
23     int cnt=0,tmp=x;
24     while(tmp) {
25         digit[++cnt]=tmp%10;
26         tmp/=10;
27     }
28     digit[cnt+1]=0; int flag=0,ans=0;
29     for(int i=cnt;i>=1;i--) {
30         ans+=digit[i]*dp[i-1][2];
31         if(flag) ans+=digit[i]*dp[i-1][0];
32         else {
33             if(digit[i]>4) ans+=dp[i-1][0];
34             if(digit[i]>6) ans+=dp[i-1][1];
35             if(digit[i+1]==6 && digit[i]>2) ans+=dp[i][1];
36         }
37         if(digit[i]==4 || (digit[i+1]==6 && digit[i]==2)) flag=1;
38     }
39     return x-ans;
40 }
41 int main()
42 {
43     int a,b; init();
44     while(1) {
45         in(a);in(b);
46         if(!a && !b)  break;
47         cout<<solve(b+1)-solve(a)<<endl;
48     }
49     return 0;
50 }

 


 

最后說那個b+1的情況,我們看到代碼中有判斷digit[i]>4和digit[i]>6等類似的語句,我們處理第i位時,實際上是處理0~digit[i]-1,即[ 0,digit[i] ),而把digit[i]放到下一次去判斷,但我們處理個位時,最后一個是不會去統計的,所以我們把統計的范圍+1,即為[ 0,digit[i]+1 )-->[ 0,digit[i] ],所以就有了solve(b+1)-solve(a)這樣的語句.(還是高二dalaoNavi-Awson告訴我的,%%%)

 

數位dp記憶化搜索寫法

 

 1 #include<bits/stdc++.h>
 2 #define in(i) (i=read())
 3 using namespace std;
 4 typedef long long lol;
 5 lol read() {
 6     lol ans=0,f=1;    
 7     char i=getchar();
 8     while(i<'0'|| i>'9') {if(i=='-') f=-1; i=getchar();}
 9     while(i>='0' && i<='9') {ans=(ans<<1)+(ans<<3)+i-'0'; i=getchar();}
10     return ans*f;
11 }
12 lol a,b;
13 lol dp[19][11];
14 lol bit[19];
15 lol dfs(lol len,lol pre,lol limit) {
16     if(!len) return 1;//如果搜到最后一位了,返回下界值1
17     if(!limit && dp[len][pre]) return dp[len][pre];//記憶化部分
18     lol maxn=limit?bit[len]:9;//求出最高可以枚舉到哪個數字
19     lol ans=0;
20     for(lol i=0;i<=maxn;i++) {
21         if(i!=4 && !(pre && i==2))//如果這一位不為4並且上一位不為6且這一位不為2
22             ans+=dfs(len-1,i==6,limit && i==maxn);//滿足條件
23     }
24     if(!limit) dp[len][pre]=ans;//如果沒有限制,代表搜滿了,可以記憶化,否則就不能
25     return ans;
26 }
27 lol solve(lol a) {
28     memset(bit,0,sizeof(bit));
29     lol k=0;
30     while(a) {//取出數字的每一位
31         bit[++k]=a%10;
32         a/=10;
33     }
34     return dfs(k,0,1);
35 }
36 int main()
37 {
38     //freopen("number.in","r",stdin);
39     //freopen("number.out","w",stdout);
40     lol t; in(t);
41     for(int i=1;i<=t;i++) {
42         lol a,b; in(a);in(b);
43         cout<<solve(b)-solve(a-1)<<endl;//差分思想
44     }
45     return 0;
46 }

 

應用

放幾道題目上來

[SCOI2009]windy數 代碼

[HNOI2010]計數 

[ZJOI2010]數字計數 題解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM