1. 程式人生 > >動態規劃--數字三角形問題

動態規劃--數字三角形問題

動態規劃的核心是狀態和狀態轉移方程。


問題描述與狀態定義

動態規劃的典型問題是數字三角形問題,如上圖的圖1所示,有一個非負整陣列成的三角形,第一行只有一個數,除了最下行之外每個數的左下方和右下方各有一個數,從第一行的數開始,每次可以往左下或者右下走一格,知道走到最下行,把沿途經過的數全部加起來,如何走才能使得這個和儘量大。

分析

  • 解決這個問題的時候,每次有兩種選擇:左下或者右上。如果用回溯法求出所有可能的路線,就可以從中選出最優的路線。但是和往常一樣,回溯法的效率太低:一個n層數字三角形的完整路線有2^(n-1)條,當n很大時回溯法的速度將變得很慢。
  • 為了得到高效的演算法,需要抽象的方法思考問題:把當前得位置(i,j)看成是一個狀態,然後定義狀態(i,j)的指標函式d(i,j)為從格子(i,j)出發時能得到的最大和,a(i,j)為格子本身的值。
  • 從格子(i,j)出發有兩種決策,如果往左走,則走到(i+1,j)後需要求“從(i+1,j)出發後能得到的最大和”這一問題,即d(i+1,j),類似的,往右走之後需要求解d(i+1,j+1)。總結為下面的狀態轉移方程: d(i,j)=a(i,j)+max{d(i+1,j),d(i+1,j+1)}

方法                  

   1.遞迴計算

int solve(int i,int j){
    return a[i][j]+(i==n?0:max(solve(i+1,j),solve(i+1,j+1)))
}

  • 這樣做是正確的,但是時間效率太低了,其原因在於重複計算,如圖2中solve(1,1)對應的呼叫關係,solve(3,2)被計算了2次。
     2.遞推計算
int i,j;
for(int j=1;j<=n;j++)
    d[n][j]=a[n][j];
for(int i=n-1;i>=1;i--){
    for(int j=1;j<=i;j++){
        d[i][j]=a[i][j]+max(d[i+1][j],d[i+1][j+1])
    }
}
  • 程式的時間複雜度是O(n^2),這樣計算的原因在於i是逆序列舉的,因此計算d[i][j]前,它所需要的d[i+1][j]和d[i+1][j+1]一定已經計算出來了。
  • 在多數情況下,遞推的時間複雜度是:狀態的總數X每個狀態的決策個數X決策時間。
    3:記憶化搜尋
  • 程式分為兩部分:第一部分把d全部初始化為-1。
memset(d,-1,sizeof(d))
  • 然後編寫遞迴函式。
int solve(int i,int j){
    if(d[i][j]>=0)
        return d[i][j];
    return d[i][j]=a[i][j]+(1==n?0:max(solve(i+1,j),solve(i+1,j+1)));
}
  • 上述程式依然是遞迴的,但是同時也把計算結果儲存在陣列d中。題目中說各個數都是非負數,因此如果已經計算過某個d[i][j],則它應該是非負的,這樣通過判斷d[i][j]是否大於等於0來得知它是否被計算過。時間複雜度是O(n^2)。

  • 上述的方法稱為記憶化,如圖2所示,它雖然不像遞推法那樣顯式的指明瞭計算順序,但仍然可以保證每個節點只訪問一次。