來自劉汝佳的《算法競賽入門經典(第二版)》,下面實現代碼均爲Java
動態規劃初步
數字三角形問題(數字塔):有一個非負整數組成的三角形,第一行只有一個數,除了最下行之外的每個數的左下方和右下方各有一個數。如下圖所示:計算從頂至底的路徑,使得總和最大。
解題思路:
定義狀態d(i, j)爲從(i, j)出發時能得到的最大和,從(i, j)出發有兩種決策,往左或者往右。
要求從(i, j)出發走到底部的最大值d(i, j),則相當於選擇從左下走或者從右下走中的較大值,即d(i+1, j)和d(i+1, j+1)的較大值,然後加上a(i, j),可得狀態轉移方程 d(i, j) = a(i, j) + max(d(i+1, j), d(i+1, j+1))。接下來是計算狀態轉移的不同代碼實現(下面i,j均從0開始)
1. 直接遞歸
自頂向下: 如果原來的三角形有n層,則調用關係樹也會有n層,一共有2^n - 1個子問題。子問題重複計算,因此時間效率不高。
/**
* 遞歸:沒有考慮子問題的重複計算
* 注意邊界
* @param a
* @param i
* @param j
* @return
*/
int solver1(int a[][], int i, int j){
int n = a.length;
return i == n ? 0 : a[i][j] + Math.max(solver1(a, i+1, j), solver1(a, i+1, j+1));
}
2. 遞推計算
自底向上:d(i+1, j)和d(i+1, j+1)需要在d(i, j)之前完成計算。遞推完成後,直接返回輔助數組b[0][0]即可。
時間複雜度爲n^2
/**
* 遞推:自底向上
* 注意邊界
* @param a
* @return
*/
int solver2(int a[][]){
//構造輔助數組b,避免修改原數組a
int n = a.length;
int[][] b = new int[n][a[n-1].length];
for(int j = 0;j<a[n-1].length;j++){
b[n-1][j] = a[n-1][j];
}
for(int i = a.length-2;i>=0;i--){
for(int j = 0;j<a[i].length;j++){
b[i][j] = a[i][j] + Math.max(b[i+1][j], b[i+1][j+1]);
}
}
return b[0][0];
}
3. 記憶化搜索
自底向上:時間複雜度爲n^2。構造記憶化數組,用於記錄已經計算過的子問題。
/**
* 記憶化搜索:暴力遞歸的改進版,仍然是遞歸,不過保存了計算的中間結果,避免計算重複子問題
* @param a
* @return
*/
int solver3(int a[][], int dp[][], int i, int j){
if(dp[i][j] > 0){
return dp[i][j];
}
/* 返回賦值語句,否則就等同與第一種方法,子問題重複計算了 */
return dp[i][j] = a[i][j] + Math.max(solver3(a,dp,i+1,j), solver3(a,dp,i+1,j+1));
}
主函數輸出
需要注意的是第三種記憶化搜索,記憶化數組用來保存已經計算過的狀態,因此記憶化數組初始化(Java默認初始化爲0)時,-1代表該狀態沒有被計算。
void solution(){
int[][]a = {{1},{3, 2},{4, 10 ,1},{4, 3, 2, 20}};
System.out.println("遞歸:"+solver1(a, 0, 0));
System.out.println("遞推:"+solver2(a));
int n = a.length;
/* 初始化記憶矩陣 */
int[][] dp = new int[n][a[n-1].length];
for(int i = 0;i<dp.length;i++){
for(int j = 0;j<dp[0].length;j++){
dp[i][j] = -1;
}
}
for(int j = 0;j<a[n-1].length;j++){
dp[n-1][j] = a[n-1][j];
}
System.out.println("記憶化搜索:" + solver3(a, dp, 0, 0));
}