10.1 什麼是動態規劃
前面學過了用遞迴的方法解決問題。但是,單純的遞迴,在解決某些問題的時候,效率
會很低。例如下面這道題目:
例題:數字三角形
問題描述
73 8
8 1 0
2 7 4 4
4 5 2 6 5
上圖給出了乙個數字三角形。從三角形的頂部到底部有很多條不同的路徑。對於每條路
徑,把路徑上面的數加起來可以得到乙個和,和最大的路徑稱為最佳路徑。你的任務就是求
出最佳路徑上的數字之和。
輸入資料
輸入的第一行是乙個整數 n (1 < n <= 100),給出三角形的行數。下面的 n行給出數字
三角形。數字三角形上的數的範圍都在 0和 100之間。
輸出要求
輸出最大的和。
輸入樣例57
3 88 1 0
2 7 4 4
4 5 2 6 5
輸出樣例
30193
解題思路
這道題目可以用遞迴的方法解決。基本思路是:
以d( r, j)表示第
r行第 j 個數字(r,j都從
1開始算
),以maxsum(r, j) 代表從第 r 行
的第 j 個數字到底邊的最佳路徑的數字之和,則本題是要求 maxsum(1, 1) 。
從某個d(r, j)出發,顯然下一步只能走
d(r+1, j)或者
d(r+1, j+1)。如果走
d(r+1, j),那
麼得到的
maxsum(r, j)就是
maxsum(r+1, j) + d(r, j);如果走
d(r+1, j+1),那麼得到的
maxsum(r, j)就是
maxsum(r+1, j+1) + d(r, j)。所以,選擇往**走,就看
maxsum(r+1, j)和
maxsum(r+1, j+1)哪個更大了。程式如下:
1. #include 2. #define max_num 100 3. int d[max_num + 10][max_num + 10]; 4. int n; 5. int maxsum( int r, int j) 6. 16. main() 17.
上面的程式,效率非常低,在
n值並不大,比如
n=100的時候,就慢得幾乎永遠算不
出結果了。為什麼會這樣呢?是因為過多的重複計算。我們不妨將對
maxsum函式的一次
呼叫稱為一次計算。那麼,每次計算
maxsum(r, j)的時候,都要計算一次
maxsum(r+1, j),
而每次計算
maxsum(r, j+1)的時候,也要計算一次
maxsum(r+1, j)。重複計算因此產生。在
題目中給出的例子裡,如果我們將
maxsum(r, j)被計算的次數都寫在位置(r, j),那麼就能
得到下面的三角形:
1 11 2 1
1 3 3 1
1941 4 6 4 1
從上圖可以看出,最後一行的計算次數總和是
16,倒數第二行的計算次數總和是
8。不難總結出規律,對於
n行的三角形,總的計算次數是
20 +21+22 ……2n-1=2n。當
n= 100時,
總的計算次數是乙個讓人無法接受的大數字。
既然問題出在重複計算,那麼解決的辦法,當然就是,乙個值一旦算出來,就要記住,
以後不必重新計算。即第一次算出
maxsum(r, j)的值時,就將該值存放起來,下次再需要計
算maxsum(r, j)時,直接取用存好的值即可,不必再次呼叫
maxsum進行函式遞迴計算了。
這樣,每個
maxsum(r, j)都只需要計算
1次即可,那麼總的計算次數(即呼叫
maxsum函式
的次數)就是三角形中的數字總數,即
1+2+3+……n = n(n+1)/2
如何存放計算出來的
maxsum(r, j)值呢?顯然,用乙個二維陣列
amaxsum[n][n]就
能解決。amaxsum[r][j]就存放
maxsum(r, j)的計算結果。下次再需要
maxsum(r, j)的值時,
不必再呼叫
maxsum函式,只需直接取
amaxsum[r][j]的值即可。程式如下:
1. #include 2. #include 3. #define max_num 100 4. int d[max_num + 10][max_num + 10]; 5. int n; 6. int amaxsum[max_num + 10][max_num + 10]; 7. int maxsum( int r, int j) 8. 20. main() 21.
這種將乙個問題分解為子問題遞迴求解,並且將中間結果儲存以避免重複計算的辦法,
就叫做「動態規劃」。動態規劃通常用來求最優解,能用動態規劃解決的求最優解問題,必
須滿足,最優解的每個區域性解也都是最優的。以上題為例,最佳路徑上面的每個數字到底部
的那一段路徑,都是從該數字出發到達到底部的最佳路徑。
實際上,遞迴的思想在程式設計時未必要實現為遞迴函式。在上面的例子裡,有遞推公式:
因此,不需要寫遞迴函式,從
amaxsum[n-1]這一行元素開始向上逐行遞推,就能求得
最終amaxsum[1][1]的值了。程式如下:
1. #include 2. #include 3. #define max_num 100 4. int d[max_num + 10][max_num + 10]; 5. int n; 6. int amaxsum[max_num + 10][max_num + 10]; 7. main() 8. 23. printf("%d", amaxsum[1][1]); 24. }
10.2 動態規劃解題的一般思路
許多求最優解的問題可以用動態規劃來解決。用動態規劃解題,首先要把原問題分解為
若干個子問題,這一點和前面的遞迴方法類似。區別在於,單純的遞迴往往會導致子問題被
重複計算,而用動態規劃的方法,子問題的解一旦求出就會被儲存,所以每個子問題只需求
解一次。
子問題經常和原問題形式相似,有時甚至完全一樣,只不過規模從原來的
n變成了
n-1,
或從原來的
n×m變成了
n×(m-1) ……等等。找到子問題,就意味著找到了將整個問題逐
漸分解的辦法,因為子問題可以用相同的思路分解成子子問題,一直分解下去,直到最底層
規模最小的的子問題可以一目了然地看出解(象上面數字三角形的遞推公式中,
r=n時,解
就是一目了然的)。每一層子問題的解決,會導致上一層子問題的解決,逐層向上,就會導
致最終整個問題的解決。如果從最底層的子問題開始,自底向上地推導出乙個個子問題的解,
那麼程式設計的時候就不需要寫遞迴函式。
在用動態規劃解題時,我們往往將和子問題相關的各個變數的一組取值,稱之為乙個「狀
態」。乙個「狀態」對應於乙個或多個子問題,所謂某個「狀態」下的「值」,就是這個「狀
態」所對應的子問題的解。
具體到數字三角形的例子,子問題就是「從位於
(r,j)數字開始,到底邊路徑的最大和」。
這個子問題和兩個變數
r和j相關,那麼乙個「狀態」,就是
r, j的一組取值,即每個數字的
位置就是乙個「狀態」。該「狀態」所對應的「值」,就是從該位置的數字開始,到底邊的最
佳路徑上的數字之和。
定義出什麼是「狀態」,以及在該「狀態」下的「值」後,就要找出不同的狀態之間如
何遷移―――即如何從乙個或多個「值」已知的「狀態」,求出另乙個「狀態」的「值」。
狀態的遷移可以用遞推公式表示,此遞推公式也可被稱作「狀態轉移方程」。
如下的遞推式就說明了狀態轉移的方式:
上面的遞推式表明了如果知道了狀態(
r+1,j)和狀態(
r+1,j+1)對應的值,該如何
求出狀態(r,j)對應的值,即兩個子問題的解決,如何導致乙個更高層的子問題的解決。
所有「狀態」的集合,構成問題的「狀態空間」。「狀態空間」的大小,與用動態規劃解
決問題的時間複雜度直接相關。在數字三角形的例子裡,一共有
n×(n+1)/2個數字,所以
這個問題的狀態空間裡一共就有
n×(n+1)/2個狀態。在該問題裡每個「狀態」只需要經過
一次,且在每個狀態上作計算所花的時間都是和
n無關的常數。
用動態規劃解題,經常碰到的情況是,
k個整型變數能構成乙個狀態(如數字三角形中
的行號和列號這兩個變數構成「狀態」)。如果這
k個整型變數的取值範圍分別是
n1, n2, ……
nk,那麼,我們就可以用乙個
k維的陣列
array[n1] [n2]……[nk]來儲存各個狀態的「值」。
這個「值」未必就是乙個整數或浮點數,可能是需要乙個結構才能表示的,那麼
array就可
以是乙個結構陣列。乙個「狀態」下的「值」通常會是乙個或多個子問題的解。
用動態規劃解題,如何尋找「子問題」,定義「狀態」,「狀態轉移方程」是什麼樣的,
並沒有一定之規,需要具體問題具體分析,題目做多了就會有感覺。甚至,對於同乙個問題,
分解成子問題的辦法可能不止一種,因而「狀態」也可以有不同的定義方法。不同的「狀態」
定義方法可能會導致時間、空間效率上的區別。
動態規劃 數字三角形
如圖所示的數字三角形,從頂部出發,在每一結點可以選擇向左走或得向右走,一直走到底層,要求找出一條路徑,使路徑上的值最大。第一行是數塔層數n 1 n 100 第二行起,按數塔圖形,有乙個或多個的整數,表示該層節點的值,共有n行。輸出最大值。5 1311 8 12 7 26 6 14 15 8 12 7...
動態規劃 數字三角形
7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 在上面的數字三角形中尋找一條從頂部到底邊的路徑,使得 路徑上所經過的數字之和最大。路徑上的每一步都只能往左下或 右下走。只需要求出這個最大和即可,不必給出具體路徑。三角形的行數大於1小於等於100,數字為 0 99 5 三角形行數。下面是三...
動態規劃 數字三角形
在用動態規劃解題時,我們往往將和子問題相關的各個變數的一組取值,稱之為乙個 狀態 乙個 狀態 對應於乙個或多個子問題,所謂某個 狀態 下的 值 就是這個 狀態 所對應的子問題的解。以 數字三角形 為例,初始狀態就是底邊數字,值就是底邊數字值。定義出什麼是 狀態 以及在該 狀態 下的 值 後,就要找出...