數位DP


這篇博客轉載自我的一個同學,這里給出鏈接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 }

 


免責聲明!

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



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