三角形最小路徑和 Triangle
數組 動態規划
問題
給定一個三角形,找出自頂向下的最小路徑和
。每一步只能移動到下一行中相鄰的結點
上。
例如,給定三角形:
[2],
[3,4],
[6,5,7],
[4,1,8,3]
自頂向下的最小路徑和為 11(即,2 + 3 + 5 + 1 = 11)。
方法聲明:
class Solution { public int minimumTotal(List<List<Integer>> triangle) { } }
動態規划(基礎)
分析
如果只是簡單分析的話,這道題和其他動態規划沒啥區別,問題是代碼寫起來可能稍有點麻煩。
用動態規划分析的關鍵是發現遞推式,我們定義一個二維數組 dp,其中 dp[i][j] 的含義為:
dp[i][j]:到達三角形第 i 行 第 j 那個元素所需要的最小步數
那么我們就可以分析出 dp[i][j] 的值的規律,所以很明顯,遞推式是這樣的:
dp[i][j] = V(i,j) + min(dp[i-1][j-1] ,dp[i-1][j])
當然,針對邊界元素我們需要單獨處理一下,具體的代碼實現如下:
代碼
class Solution { public int minimumTotal(List<List<Integer>> triangle) { int len = triangle.size(); int[][] total = new int[len][len]; //保存的是到達最后一行各個元素的最短距離 total[0][0] = triangle.get(0).get(0); for (int i = 1; i < len; i++) { for (int j = 0; j <= i; j++) { if (j == 0) { total[i][j] = triangle.get(i).get(j) + total[i - 1][j]; } else if (j == i) { total[i][j] = triangle.get(i).get(j) + total[i - 1][j - 1]; } else { total[i][j] = triangle.get(i).get(j) + Math.min(total[i - 1][j], total[i - 1][j - 1]); } } } int min = Integer.MAX_VALUE; for (int i = 0; i < len; i++) { min = Math.min(min, total[len - 1][i]); } System.out.println(Arrays.toString(total[len - 1])); return min; } }
時間復雜度 O(n^2)
空間復雜度 O(n^2)
動態規划(逆向)
分析
假如我們是從倒數第二行(統一稱為第 i 行)開始走的話,那么從倒數第二行的第 j 個元素走到最后一行所需要的最小步數為:
P(j) = V(i,j) + min(V(i+1,j),V(i+1,j+1))
我們對倒數第二行的所有元素都進行此計算,並將結果保存到集合 array 中,且 array 的第 j 個元素的值為 P(j)。
我們知道 P(j) 代表的含義為倒數第二行的第 j 個元素
走到最后一行
所需要的最小步數,所以如果讓我們計算從倒數第二行
走到最后一行
所需要的最小步數,那么我們只需從 array 中選擇值最小的那個元素即可。
同樣道理,如果讓我們從倒數第三行開始走的話,那么我們只需按同樣的方式計算從倒數第三行的第 j 個元
素走到 array 列
所需要的最小步數即可,而不需要關注倒數第二行和倒數第三行的數了。
經過此步驟后,我們發現問題的規模變小了,動態規划思想也就體現出來了。
遍歷過程:
[2],
[3,4],
[6,5,7],
[4,1,8,3]
[2],
[3,4],
[7,6,10],
[2],
[9,10],
[11]
代碼
class Solution { public int minimumTotal(List<List<Integer>> triangle) { int[][] dp = new int[triangle.size() + 1][triangle.size() + 1];// 加1可以不用初始化最后一層 for (int i = triangle.size() - 1; i >= 0; i--) { List<Integer> curTr = triangle.get(i); for (int j = 0; j < curTr.size(); j++) { dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + curTr.get(j); } } return dp[0][0]; } }
時間復雜度 O(n^2)
空間復雜度 O(n^2)
動態規划(逆向 + 優化)
對於上面那種方式,我們可以繼續優化。
我們發現在計算倒數第三行時,array 的長度和倒數第二行的長度相同,且有 替換
倒數第二行的效果,所以,我們可以不必再定義一個 array ,而只需把計算的結果賦給倒數第二行即可。
而在計算過程中,我們需要的 array 的長度會越來越小,這沒關系,我們只需要關心自己需要的那些元素即可。
class Solution { public int minimumTotal(List<List<Integer>> triangle) { int[] dp = new int[triangle.size() + 1]; // 加1可以不用初始化最后一層 for (int i = triangle.size() - 1; i >= 0; i--) { List<Integer> curTr = triangle.get(i); for (int j = 0; j < curTr.size(); j++) { dp[j] = curTr.get(j) + Math.min(dp[j], dp[j + 1]); //這里的dp[j] 使用的時候默認是上一層的,賦值之后變成當前層 } } return dp[0]; } }
時間復雜度 O(n^2)
空間復雜度 O(n)
實際測試會發現,雖然空間復雜度大大降低了,但是總耗時卻增加了。這也很好解釋,因為時間復雜度並沒有變,而空間復雜度減小又是在建立在增加了計算的基礎上的,所以總時間當然會增加了。