這篇博客轉載自我的一個同學,這里給出鏈接https://blog.csdn.net/JKdd123456/article/details/81383012
一、基礎篇——介紹
1、概念:
數位dp是一種計數用的dp,一般就是要統計一個區間 [A , B ] 內滿足一些條件數的個數。
所謂數位dp,字面意思就是在數位上進行dp。
數位的含義:一個數有個位、十位、百位、千位......數的每一位就是數位啦!
之所以要引入數位的概念完全就是為了dp。數位dp的實質就是 換一種暴力枚舉的方式,使得新的枚舉方式滿足dp的性質,然后記憶化就可以了。
數位DP其實是很靈活的,所以一定不要奢求一篇文章就會遍所有數位DP的題,這一篇只能是講清楚一種情況,其他情況遇到再總結,在不斷總結中慢慢體會這個思想,以后說不定就能達到一看到題目就能靈活運用的水平。(其實DP都是這樣……)
ex:
例1.求 a~b 中不包含49的數的個數 . 0 < a、b < 2*10^9
注意到n的數據范圍非常大,暴力求解是不可能的,考慮dp,如果直接記錄下數字,數組會開不起,該怎么辦呢?
此時就要用到數位dp.
2、用處:
一般應用於:
(1)、 求出在給定區間 [A,B] 內,符合條件 P(i) 的數 i 的個數.
(2)、條件 P(i) 一般與 數 的大小無關,而與 數的組成 有關.
3、這樣,我們就要考慮一些特殊的記錄方法來做這道題.一般來說,要保存給定數的每個位置的數.然后要記錄的狀態為當前操作數的位數,剩下的就是根據題目的需要來記錄.可以發現,數位dp的題做法一般都差不多,只是定義狀態的不同罷了.
二、實戰篇——例題分析
1、
例:
這一篇要說的數位DP是一道最簡單的數位DP:題目鏈接
題目大意:多組數據,每次給定區間 [n,m] ,求在n到m中沒有 “62“ (連續)或 “4“ 的數的個數。
如62315包含62,88914包含4,這兩個數都是不合法的。
0 < n<=m < 1000000
(1)、兩種不同的枚舉:對於一個求區間[le,ri]滿足條件數的個數,最簡單的暴力如下:
for(int i=le; i<=ri; i++) if(right(i)) ans++;
然而這樣枚舉不方便記憶化,或者說根本無狀態可言。
(2)新的枚舉——————數位 dp
A:
思路分析:
試想:我們如果能有一個函數count(int x),可以返回[0,x]之間符合題意的數的個數。
那么是不是直接輸出count(m)-count(n-1)就是答案?
好,那么下面我們的關注點就在於怎么做出這個函數。我們需要一個數組。(dp原本就是空間換時間)
我們設一個數組 dp[ i ][ j ],表示 i 位數,最高位是j 的符合題意的個數。
比如dp[ 1 ][ 2 ]=1; (位數是 1,高位是2 的只能是 2本身)
dp[ 1 ][ 4 ]=0; ( 位數是 0,高位是 4 的不符合題意,個數 是 0)
dp[ 2 ][ 0 ]=9; ( 當 j 等於 4 的時候不符合題意,就是 9個,00,01,02,03,05,06,07,08,09 )
dp[ 2][ 6 ]=8 (60,61,63,65,66,67,68,69).
我們先不關注這個 dp 有什么用,我們先關注 dp 本身怎么求。
首先
if(i==4) dp[1][i]=0; else dp[1][i]=1;
這一步是很顯然的,那么根據這個題的數據范圍,只需要遞推到 dp[ 7 ][ i ] 就夠用了。那么稍微理解一下,可以想出遞推式:
if(j==4) { dp[i][j]=0; } else if(j==6) { for(int k=0; k<=9; k++) { if(k!=2) { dp[i][j]+=dp[i-1][k]; } } } else { for(int k=0; k<=9; k++) { dp[i][j]+=dp[i-1][k]; } }
上面的式子也是很顯然的,如果覺得不顯然可以這樣想:
i 位數,最高位是 j 的符合條件的數,如果 j 是4,肯定都不符合條件(因為題目不讓有4),所以直接是0;
如果 j 不是6,那么它后面隨便取,只要符合題意就可以,所以 dp[ i - 1 ][ k ] ,k可以隨便取的和;
如果 j 是6,后面只要不是 2 就行,所以是 dp[ i -1 ][ k ], k除了2都可以,求和即可
這里要說明一下,認為00052是長度為5,首位為0的符合條件的數,052是長度為3首位為0符合條件的數。
那么現在我們已經得到了 dp 數組,再重申一下它的含義:i位數,最高位是j的數,符合題意的數有多少個。
現在我們就要關注怎么利 dp 數組做出上面我們說的那個函數 count(int x) ,它可以求出 [0,x] 中符合題意的數有多少個
那么我們做這樣一個函數 int solve(int x) 它可以返回 [0,x) 中符合題意的有多少個。
那么solve(x+1) 實際上與count(x)是等價的 ( solve(x) 求得是不等於 x 的個數,那么 solve(x+1) 求的是小於 x+1 的個數,也就是小於等於 x 的個數)
那么現在問題轉化成了: 小於x,符合題意的數有多少個?
很簡單,既然小於,從最高位開始比,必定有一位要嚴格小於x(前面的都相等)。所以我們就枚舉哪一位嚴格小於(前面的都相等)
假設我們現在把x分成了 a1,a2,…,aL (ai 是每位數)這樣一個數組,長度為 L,aL 是最高位。
那么結果實際上就是這樣:長度為L,最高位取 [0,aL-1] 的所有的符合題意數的和;再加上長度為L-1,最高位取aL,次高位取[0,a(L-1)-1]的所有符合題意數的和;再加上……;一直到第一位。
B:
特判:
最高位aL如果是4,那么這句話直接就可以終止了,4 以后的都不符合題意
最高位 aL如果是6,次高位 取 2,,也可以終止了 。加上這些條件之后就很嚴謹了。C: 我們要求[a,b]不包含49的數的個數,可C:求和
想到利用 前綴和來做,具體來說,就是 [a,b] = solve(b+1)-solve(a) (等於 b的數 減去小於 a的數,就是 [a,b] 區間的值)
solve(x) 求解的是 不等於 x的數
對於這里的求和我更願意稱之為一種“剝離”,按我的理解就像是剝洋蔥一樣一層一層的下手,比如56789,就會按照位數和最高位的數一次剝離成五位數的(00000,10000,20000,30000,40000,50000),四位數的(0000,1000,2000,3000,4000,5000,6000),三位數的(000,100,200,300,400,500,600,700),二位數的(00,10,20,30,40,50,60,70,80)和一位數的(0,1,2,3,4,5,6,7,8,9)。
2、CODE:
1 #include<cstdio> 2 #include<cstring> 3 using namespace std; 4 #define ll long long int 5 ll dp[8][10]; 6 void getDp() 7 { 8 int i,j,k; 9 memset(dp,0,sizeof(dp)); 10 dp[0][0]=1; 11 for(i=1; i<=7; i++) 12 { 13 for(j=0; j<=9; j++) 14 { 15 if(j==4) 16 { 17 dp[i][j]=0; 18 } 19 else if(j==6) 20 { 21 for(k=0; k<=9; k++) 22 { 23 if(k!=2) 24 { 25 dp[i][j]+=dp[i-1][k]; 26 } 27 } 28 } 29 else 30 { 31 for(k=0; k<=9; k++) 32 { 33 dp[i][j]+=dp[i-1][k]; 34 } 35 } 36 } 37 } 38 } 39 int a[20]; 40 ll solve(int n) 41 { 42 int len=0;///len代表位數 43 int i,j; 44 while(n) 45 { 46 a[++len]=n%10;///ai代表着每位數 47 n/=10; 48 } 49 a[len+1]=0;///終止 50 ll ans=0; 51 for(i=len; i>=1; i--)///倒序 52 { 53 for(j=0; j<a[i]; j++) 54 { 55 if(j==4||(a[i+1]==6&&j==2))///不要62和4 56 { 57 continue; 58 } 59 else 60 { 61 ans+=dp[i][j]; 62 } 63 } 64 if(a[i]==4)///最高位是4或者最高位是6次高位是2就直接停止,后面的數已經包含在dp數組中了 65 break; 66 if(a[i+1]==6&&a[i]==2) 67 break; 68 } 69 return ans; 70 } 71 int main() 72 { 73 getDp(); 74 int n,m; 75 ll ans1,ans2,ans; 76 while(scanf("%d%d",&m,&n)!=EOF) 77 { 78 if(m==0||n==0) 79 { 80 break; 81 } 82 ans1=solve(m); 83 ans2=solve(n+1); 84 ans=ans2-ans1; 85 printf("%lld\n",ans); 86 } 87 }