關於數位DP的學習


---恢復內容開始---

因為最近做比賽經常會出現數位DP,便嘗試着去學學看數位DP。

先給出兩篇論文的鏈接:

數位計數問題解法研究

淺談數位類統計問題

然后也是尋找了很多大牛的博客,學習了很多(但是沒學會囧。),現在先總結一下已經學到的東西

“在信息學競賽中,有這樣一類問題:求給定區間中,滿足給定條件的某個D 進制數或
此類數的數量。所求的限定條件往往與數位有關,例如數位之和、指定數碼個數、數的大小
順序分組等等。題目給定的區間往往很大,無法采用朴素的方法求解。此時,我們就需要利
用數位的性質,設計log(n)級別復雜度的算法。解決這類問題最基本的思想就是“逐位確定”
的方法。下面就讓我們通過幾道例題來具體了解一下這類問題及其思考方法。”——劉聰

事實上,為什么會想到用數位DP來做,就是因為限定條件往往和數位有關,而仔細地朴素的暴力方法中,所做的重復的工作太多。這樣的條件會使得DP(記憶化搜索)有用武之地。

目前我所接觸到的大多數的題,都是可以通過記錄某些值(比如數位等)來減少重復的運算。當然,因為此類題的特殊性,可以編寫check函數確定代碼的正確性。

再偷用某個大牛的一句話:其實數位DP(或者說所有記憶化搜索)都是可以看做通過搜索來填滿狀態的值。

首先先想想數位DP的運行模式

如果我們要統計[0,54321]中滿足某個條件的個數,需要將其拆分為

[00000,09999][10000,19999],[20000,29999],[30000,39999],[40000,49999],

[50000,50999],[51000,51999],[52000,52999],[53000,53999],

[54000,54099],[54100,54199],[54200,54299],

[54300,54309],[54310,54319],

[54320,54321]

為什么要這么分呢?隨便舉個例子,如果我們統計過了[0000,9999]中的滿足條件(或者其他各種不滿足條件的狀態)的個數,那么分別在加上前綴,就可以判斷出有多少個滿足條件的個數。目的是為了將大的區間划分為小的區間進行求解。

因此,總結一句話,數位DP減少的運算量為:前面幾位固定,后面幾位可以任意取的個數統計。

比如分析一道簡單題:HDU 3652,通過我這個渣渣的錯誤歷程來分析一些細節上的問題

先貼錯誤代碼

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 int dp[20][20][20];
 6 int num[20];
 7 int dfs(int pos,int mod,int pre,int stat,int limit){
 8     if(pos==0) return mod==0&&stat;
 9     if(!limit&&dp[pos][mod][pre]!=-1) return dp[pos][mod][pre];
10     int ans=0;
11     int end=limit?num[pos]:9;
12     for(int i=0;i<=end;i++){
13         int nmod=(mod*10+i)%13;
14         int nstat=(pre==1&&i==3)||stat;
15         ans+=dfs(pos-1,nmod,i,nstat,limit&&i==end);
16     }
17     if(!limit) dp[pos][mod][pre]=ans;
18     return ans;
19 }
20 int cal(int x){
21     int cnt=0;
22     memset(num,0,sizeof(num));
23     while(x){
24         num[++cnt]=x%10;
25         x/=10;
26     }
27     return dfs(cnt,0,0,0,1);
28 }
29 int main()
30 {
31     int i,j;
32     int n;
33     memset(dp,-1,sizeof(dp));
34     while(scanf("%d",&n)!=EOF){
35         int ans=cal(n);
36         printf("%d\n",ans);
37     }
38     return 0;
39 }

我選取的記錄參數有三個:pos當前處理位,mod前綴和余數,還有前一位的數字pre

但是運算結果卻始終會小於等於正確的答案,為什么呢?

想了想,發現其實是因為參數含義的問題。

分析一下,如果我將pre作為一個關鍵參數記錄下來,其實我並不能區分我記錄的是后面幾位能不能隨機取的個數。

即當下一次搜索到pos,mod,pre的時候,不能確定前面是否有13,或者以前搜索的DP[pos][mod][pre]中的數的個數是否有13。

因此應該把記錄的參數改為pos,mod,stat(表示為記錄狀態,0為不含13,1為只含前一位為1,2為前綴含有13)。

因此得到下面的AC代碼

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int dp[20][20][3];
int num[20];
int dfs(int pos,int mod,int stat,int limit){
    if(pos==0) return mod==0&&stat==2;
    if(!limit&&dp[pos][mod][stat]!=-1) return dp[pos][mod][stat];
    int ans=0;
    int end=limit?num[pos]:9;
    for(int i=0;i<=end;i++){
        int nmod=(mod*10+i)%13;
        int nstat;
        if(stat==1&&i==3||stat==2) nstat=2;
         else if(i==1) nstat=1;
          else nstat=0;
        ans+=dfs(pos-1,nmod,nstat,limit&&i==end);
    }
    if(!limit) dp[pos][mod][stat]=ans;
    return ans;
}
int cal(int x){
    int cnt=0;
    memset(num,0,sizeof(num));
    while(x){
        num[++cnt]=x%10;
        x/=10;
    }
    return dfs(cnt,0,0,1);
}
int main()
{
    int i,j;
    int n;
    memset(dp,-1,sizeof(dp));
    while(scanf("%d",&n)!=EOF){
        int ans=cal(n);
        printf("%d\n",ans);
    }
    return 0;
}
View Code

好了講完了


免責聲明!

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



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