轉載請注明出處:http://www.cnblogs.com/WABoss/p/DP.html
動態規划(Dynamic Programming, DP)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法……
(先忘了這個吧)允許我從另一個角度去理解並解釋動態規划,那么開始吧。
一、維度
首先說明維度(Dimension)這個概念。這兒有一個很好很有啟發意義的回答:http://www.guokr.com/question/584012/?answer=760222#answer760222。
里面提到所謂的維度其實就是允許某種東西自由變化的范圍。
比如說,人,人在地球上就有無數個維度,包括人當前所在的經度、緯度和高度,還有人的身高、體重和頭發顏色之類的,甚至還有時間這個維度。
因此,世間萬物都具有無數個維度。事實上在具體的問題中顯然並不需要考慮這無數個維度,只需分析具體問題所需要的維度。
比如說,一個導航的程序,人的身高、體重和頭發顏色是無所謂的,而經度和緯度是導航程序最關心的兩個基礎維度,此外還有方向、速度等等維度是和導航相關的,總之具體問題具體分析。
現在聯想一下高級語言里數組,如果把多維數組各個維度拿出來看,是不是與物理上的“維度”的概念有種異曲同工之妙?
-
int dp[n0][n1][n2];
比如上面這個多維數組每個維度n0、n1和n2都可以自由在0到ni-1變化,這正是維度的概念!只不過這些維度是些表面沒有太多意義的連續非負整數。
如果我們賦予它們意義呢?請看下一節,【狀態】。
二、狀態
事物具有多個維度,反之多個維度就能共同描述一個事物,而多個維度的值就描述了事物的狀態。
比如說,導航程序,經度和緯度這兩個維度的值就能描述當前某用戶的狀態,即這個用戶正在某經度某維度的地方。
對應於數組,經度、緯度兩個維度對應了二維數組:
-
int dp[MaxLng][MaxLat];
如果多維數組各個維度的值確定后,那么就相當於確定了在多維數組中具體哪一個單元格。因此可以這么說,各個單元格就對應了各個狀態,數組就是狀態的集合,而狀態就是通過確定數組各個維度的值定位的!
-
dp[x][y]所對應的單元格就表示人在經度x緯度y的狀態
不過,單元格可不僅僅是單元格,因為數組可以是bool類型、int類型等等,即這個單元格有值的。這個值就是狀態的值,而狀態該是什么類型的值,取值是多少這取決於具體的問題。
注意,上面說到值有兩個,紅色字,一個是維度的值,一個是狀態的值,其中維度的值描述了具體的狀態。
比如說,上面的導航程序的數組是int類型,這樣dp[x][y]就可以拿來表示人在到達經度x緯度y這個狀態所需的時間(分鍾數)。
再比如說,如果數組是bool類型,這樣dp[x][y]就可以拿來表示人能否到達經度x緯度y這個狀態。
在動態規划問題里,狀態的值經常是最小值、最大值和方案數等。一般就是從已知的初始狀態的值,通過狀態的轉移,得出最終目標狀態的值。
不過,上面提到的這個“狀態”還不是動態規划里的“狀態”,因為還少了一樣東西——請看下一節,【無后效性】。
三、無后效性
動態規划的狀態必須是無后效性的。下面我用一道題目具體說明什么是無后效性。
有一樓梯共M級,剛開始時你在第一級,若每次只能跨上一級或二級,要走上第M級,共有多少種走法?
這個問題里面很容易找到一個維度:樓梯的級數。那么到達第i級台階就是一個狀態,簡稱狀態i,問題所需要求的是到達狀態i的方案數,即:
- dp[i]表示到達第i級台階的方案數
i的取值是從1到M,也就是說有M個狀態。這M個狀態之間是有關系的。比如可以從第1級的台階直接移動到第2級或者第3級,或者第5級台階可以從第3級或者第4級台階直接移動到。
下面就M取5畫一張狀態圖:
這張圖上面的頂點表示的是各個狀態,有向邊(弧)就表示狀態的轉移。事實上這張圖是一張有向無環圖(DAG, Directed Acyclic Graph),即從任何一點出發不可能回到自身。
也就是說在那張圖中狀態i是通過狀態i-1和狀態i-2確定的,並且和狀態i-3、i-4、i-5...沒有一點關系,之后狀態i也不會影響到狀態i-3、i-4、i-5…,i-3、i-4、i-5狀態的值已經是確定好的。
這就是無后效性。動態規划的狀態必須是無后效性的狀態。
動態規划本質上可以理解成在一個DAG上的遞推:入度0的狀態點就是初始的狀態,有向邊說明了狀態轉移的方向,通過狀態的轉移按着DAG的拓撲序推出最終目標狀態的值。而由於是DAG,狀態不會重復經過,最多就經過所有的狀態。
具體如何轉移請看下一節,【狀態轉移】。
四、狀態轉移
前面提到了,動態規划的過程就是從初始狀態轉移到最終的目標狀態,而初始狀態的值是知道的,目標狀態的值就是問題需要的。
還是那一題【超級樓梯】,狀態(的值)是這么表示的:
-
dp[i]表示到達第i級台階的方案數
而轉移的表示,通常當然不是畫圖,而是寫出狀態轉移方程:
- dp[i] = 1 (i=1)
- dp[i] = dp[i-1] (i=2)
- dp[i] = dp[i-1] + dp[i-2] (i>2)
為什么是這樣呢?可以從之前畫的狀態圖中理解。
- 首先dp[1]就表示到達台階1的方案數,一開始就在台階1,這個狀態的值當然就是1了,即dp[1]=1,而事實上這個也正是初始狀態;
- 然后dp[i]=dp[i-1](i=2),也就是說dp[2]=dp[1],因為走到台階2只有一種策略就會從台階1走來,因而到達台階2的方案數就等於達到台階1的方案數,也就是1;
- 最后dp[i]=dp[i-1]+dp[i-2](i>2),這個從語義上理解是這樣的:走到i-1的方案數是dp[i-1],然后向上走一步就到了i,因而狀態i的方案數就有dp[i-1];而走到i-2的方案數是dp[i-2],然后向上走2步就到了i,因而狀態i的方案數又可以有dp[i-2]種;總共就是dp[i-1]+dp[i-2]個方案數。
那么有了狀態轉移方程,就可以動手寫程序實現了。具體程序的實現多種多樣,但要注意的是狀態的值一定要按拓撲序依次求出。下面我講講講三種方式:“人人為我”、“我為人人”和記憶化搜索。其中“人人為我”和“我為人人”這兩個是我從北大ACM-ICPC暑期課的課件中學到的。
- “人人為我”
這個很常見,就是上面轉移方程表示的那樣。
狀態圖大概可以這么看:
比如可以看到狀態5就是從3和4轉移過來的。
寫成程序就是這樣:
#include<cstdio> using namespace std; int dp[41]; int main(){ dp[1]=1; dp[2]=1; for(int i=3; i<=40; ++i){ dp[i]=dp[i-1]+dp[i-2]; } int N,M; scanf("%d",&N); while(N--){ scanf("%d",&M); printf("%d\n",dp[M]); } return 0; }
- “我為人人”
這個是從當前狀態順着推過去,更新能到達狀態的值。
這個實現起來挺直觀的,因為是順着往前推。
狀態圖可以這么看:
比如可以看到狀態1更新到2和3。
寫成程序是這樣的:
#include<cstdio> using namespace std; int dp[44]; int main(){ dp[1]=1; for(int i=1; i<40; ++i){ dp[i+1]+=dp[i]; dp[i+2]+=dp[i]; } int N,M; scanf("%d",&N); while(N--){ scanf("%d",&M); printf("%d\n",dp[M]); } return 0; }
- 記憶化搜索
一般都是用DFS實現,就是用搜索當前狀態能從哪兒轉移過來。
另外開一個數組記錄搜索過的狀態的值,避免重復搜索,時間復雜度就不會是指數級的了,而是多項式級。
有時候用記憶化搜索很直觀,而且記憶化搜索還可以避免搜索無意義、不需要的狀態,從而使效率提升。
這個直接上代碼吧:
#include<cstdio> using namespace std; int dp[44]; int dfs(int i){ if(dp[i]!=0) return dp[i]; return dp[i]=dfs(i-1)+dfs(i-2); } int main(){ dp[1]=1; dp[2]=1; int N,M; scanf("%d",&N); while(N--){ scanf("%d",&M); printf("%d\n",dfs(M)); } return 0; }
五、最后
如果有什么地方說得不好或者不對的歡迎交流。
博客地址:http://www.cnblogs.com/WABoss
或者QQ聯系:2322271369