動態規劃(一)
這篇部落格是動態規劃的最入門的內容,具體的一些題目的練習留在後面的部落格中來寫,這裡只介紹最基本的內容。
首先先引入一題例題:數字三角形(poj 1163)
題目描述:在上面的數字三角形中尋找一條從頂部到底部的路徑,使路徑上所經歷的數字之和最大。路徑上每一步只能往左下走,或者右下走。只需求出這個最大和即可,不必給出具體路徑。
很明顯啊這個三角形就是用二維陣列來儲存。
方法(一):
#include <iostream> #include <algorithm> using namespace std; #define MAX 101 intD[MAX][MAX]; int n; int MaxSum (int i ,int j){ //注意這個地方有兩個引數,所以這個是用二維陣列儲存 if(i == n){ return D[i][j]; } int x = MaxSum(i+1,j); int y = MaxSum(i+1,j+1); return max(x,y)+D[i][j]; } int main(){ int i,j; cin>>n; for(i = 1;i<=n;i++){ for(j = 1;j<=i;j++){ //這裡迴圈控制了一下這樣就是第二行輸入2個這樣下去 cin>>D[i][j]; } } cout<<MaxSum(1,1)<<endl; }
但是如果這個程式碼在poj上提交是沒辦法提交的,因為超時了,為什麼超時呢?因為有大量的重複計算。
如果使用遞迴的方式深度遍歷每條路徑的話存在大量的重複計算,時間複雜度是O(2^n)。如果這個n是100的話肯定是超時的了。
對上面的方法進行一下改進,如果是因為重複計算二而導致的超時,我可不可以使用一個數組把計算一次的值用一個同樣的二維陣列來記住呢?這樣的話就可以用n方的時間複雜度完成。因為三角形數字的總數是n*(n+1)/2。
#include <iostream> #include <algorithm> using namespace std; #define MAX 101 int D[MAX][MAX]; int n; int maxSum[MAX][MAX]; int MaxSum(int i,int j ){ if(maxSum[i][j] != -1){ return maxSum[i][j]; } if(i == n){ return D[i][j]; }else{ int x = MaxSum(i+1,j); int y = MaxSum(i+1,j+1); maxSum[i][j] = max(x,y)+D[i][j]; } } int main(){ int i,j; cin>>n; for(i = 1;i<=n;i++){ for(j = 1;j<=i;j++){ cin>>D[i][j]; maxSum[i][j] = -1; } } cout<<MaxSum(1,1)<<endl; }
這個程式碼其實就是一開始把maxSum這個陣列全部值給置成-1唄。在函式裡面自己加上一個判斷咯,如果是負1的話就沒不必要算了直接返回相應的值就好了。(之前因為上學期的疏忽,對遞迴理解真的太淺了,所以現在經過痛苦的啃程式碼終於(^ - ^)).
再改進:
往往遞迴是有很大的侷限性的,速度慢的同時還可能把棧給佔滿(很少發生)。所以一般來說我們就要做一件比較酷的事就是把遞迴轉成遞推(迴圈)。因為像這種題目用遞迴的思維就很好理解,但是用遞推沒法直接瞭解是怎麼做的。
我現在把最後一層的內容先放到一個數組的最後一行裡面。
用遞推式不斷的往上推,最上面的數其實就是這個和,這個陣列的每一個位置都是相應位置到底邊的最大和。這裡說一下一步步推的過程,首先7是用2與4或者5裡面大的相加,以此類推的往上走。
後面再進行一下空間的優化:
其實沒必要用一個單獨的二維陣列來進行儲存,只要用一維陣列就好了,我可以把7直接存在4的位置。因為再算12的時候這個四已經沒有用了。
這個是把第二層全部存於這個一位陣列的結果。再來進一步來考慮的話其實都可以不需要這個一維陣列,直接用D的最後一行就好了。
#include <iostream> #include <algorithm> using namespace std; #define MAX 101 int D[MAX][MAX]; int n; int *maxSum; int main(){ int i,j; for(i = 1;i<=n;i++){ for(j = 1;j<=i;j++){ cin>>D[i][j]; } } maxSum = D[n]; //注意這個迴圈是從倒數第二行開始的 for(i = n-1;i>=1;i--){ for(j = 1;j<=i;j++){ maxSum[j] = max(maxSum[j],maxSum[j+1])+D[i][j]; } } cout<<maxSum[1]<<endl; return 0; }
從這個例題入手後面來總結一下動態規劃的一些解題的方法:
遞迴到動規的一般轉化方法:
遞迴函式有n個引數,就定義一個n維的陣列,比如上面遞迴函式有兩個陣列,那麼就定義一個2維的陣列,陣列的下標是遞迴函式引數的取值範圍,陣列元素的值是遞迴函式的返回值,這樣就可以從邊界值開始逐步填充陣列,相當於計算遞迴函式值的逆過程。
遞迴到動規的一般解題過程:
1,將原問題分解為子問題:
把原問題分解為若干個小的子問題,子問題和原問題形式相同或者是類似,這只不過規模變小了。子問題都解決,原問題即解決了。子問題一旦求出就會被儲存,所以每個子問題只需要解一次。舉一個例子,比如說上面的數字三角形問題,子問題就是一個數正下方到底面最大和這個數右下方到地面最大。
2,確定狀態:
在用動態規劃解題時,我們往往將和子問題相關的各個變數的一組取值稱之為一個“狀態”。一個“狀態”對應一個或多個子問題,所謂某個狀態下的“值”,就是這個狀態所對應子問題的解
這兩段話可以再看看,我感覺雖然第一遍看很抽象但是如果對第一個例題有一個比較好的理解還是可以看懂的。
3,確定一些邊界狀態的值:
對於這個數字三角形而言的話,這個初始狀態其實就是底邊數字。
4,確定狀態轉移方程:
能用動規解決的問題的特點:
這裡先用我的理解解釋一下這些語句吧:最優子結構可以理解為數字三角形中一個子問題:就是一個數字的後面兩個數字儲存的也是到底下的最大值。無後效其實就是不用追究過程只要結果的感覺。所以這個也是為什麼這個題目裡面有提到只要結果不要路徑。