---恢復內容開始---
因為最近做比賽經常會出現數位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; }
好了講完了