【動態規劃】閆氏dp分析
來源於Acwing yxc的閆氏dp分析講解,本文為幾道經典例題的筆記
目錄
53. 最大子序和
給定一個整數陣列 nums
,找到一個具有最大和的連續子陣列(子陣列最少包含一個元素),返回其最大和。
示例:
輸入: [-2,1,-3,4,-1,2,1,-5,4]
輸出: 6
解釋: 連續子陣列 [4,-1,2,1] 的和最大,為 6。
進階:
如果你已經實現複雜度為 O(n) 的解法,嘗試使用更為精妙的分治法求解。
狀態表示:\(f[i]\)表示以第i個數字為結尾的是最大連續子序列的總和 。
狀態表示的屬性:max最大值。
初始化:\(f[0] = nums[0]\)
集合的劃分【轉移方程】: \(f[i] = max(f[i - 1], 0)+ nums[i]\)
返回結果:\(res = max(f[0],f[1],f[2]...f[n])\)
public int maxSubArray(int[] nums) { int[] f = new int[nums.length]; f[0] = nums[0]; int res = f[0]; for(int i = 1; i < nums.length; i++){ f[i] = Math.max(f[i - 1],0) + nums[i]; res = Math.max(res, f[i]); } return res; }
時間複雜度:狀態數為O(N),轉移時間為O(1),總時間為O(N)。
空間複雜度:需要額外O(N)的空間儲存狀態。
優化:通過變數代替陣列儲存空間複雜度,優化到常數。
public int maxSubArray(int[] nums) { int res = Integer.MIN_VALUE, prev = 0; for(int i = 0; i < nums.length; i++){ int now = Math.max(prev,0) + nums[i]; res = Math.max(now, res); prev = now; } return res; }
120. 三角形最小路徑和
給定一個三角形,找出自頂向下的最小路徑和。每一步只能移動到下一行中相鄰的結點上。
相鄰的結點 在這裡指的是 下標
與 上一層結點下標
相同或者等於 上一層結點下標 + 1
的兩個結點。
例如,給定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自頂向下的最小路徑和為 11
(即,2 + 3 + 5 + 1 = 11)。
說明:
如果你可以只使用 O(n) 的額外空間(n 為三角形的總行數)來解決這個問題,那麼你的演算法會很加分。
狀態表示:$f[i][j] $表示所有從起點走到第i行,第j個數的路徑
狀態表示的屬性:所有路徑上的數的和的最小值。
初始化:\(f[0][0] = t.get(0).get(0)\)
集合的劃分【轉移方程】:
- 最後一步從左上方下來的:\(left = f[i-1][j-1]+ nums[i][j]\)
- 最後一步從右上方下來的:\(right = f[i-1][j] + nums[i][j]\)
- 結果:\(f[i][j] = min(left, right)\)
返回結果:\(res = min(f[n-1][1-> n-1])\)
public int minimumTotal(List<List<Integer>> t) {
int n = t.size();
int[][] f = new int[2][n]; //f[i][j]代表從起點走到第i行第j列的最小值
f[0][0] = t.get(0).get(0);
for(int i = 1; i < n; i ++){
for(int j = 0; j <= i ; j ++){
f[i&1][j] = Integer.MAX_VALUE;
if(j > 0) f[i&1][j] = Math.min(f[i&1][j],f[i-1&1][j-1]+t.get(i).get(j));//代表上下層座標相等的情況
if( j < i) f[i&1][j] = Math.min(f[i&1][j],f[i-1&1][j]+t.get(i).get(j));//代表是上層下層座標相等的情況
}
}
int res = Integer.MAX_VALUE;
for(int i = 0; i< n; i++){
res = Math.min(res,f[n-1&1][i]);
}//返回結果為最後一層的最小值 min(f[n-1][0->n-1])
return res;
}
91. 解碼方法
一條包含字母 A-Z
的訊息通過以下方式進行了編碼:
'A' -> 1
'B' -> 2
...
'Z' -> 26
給定一個只包含數字的非空字串,請計算解碼方法的總數。
示例 1:
輸入: "12"
輸出: 2
解釋: 它可以解碼為 "AB"(1 2)或者 "L"(12)。
示例 2:
輸入: "226"
輸出: 3
解釋: 它可以解碼為 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
狀態表示:\(f[i]\)表示所有由前i個數字解碼得到的字串 。
狀態表示的屬性:數量。
初始化:\(f[0] = 1\)
集合的劃分【轉移方程】:
-
最後一個字母由\(s[i]\)解碼【最後一位是一位數】:\(cnt1 = f[i - 1]\)
-
最後一個字母由\(s[i - 1],s[i]\)解碼【最後一位是兩位數】: \(cnt2 = f[i - 2]\)
-
\(f[i] = cnt1 + cnt2\)
返回結果:\(res = f[n]\)
public int numDecodings(String s) {
int n = s.length();
int[] f = new int[n + 1]; //減少邊界的判斷
char[] chs = s.toCharArray();
f[0] = 1;
for(int i = 1; i<= n ; i++){
if( chs[i - 1]!= '0') f[i] += f[i - 1];
if(i >= 2){
int t = (chs[i - 2] -'0') * 10 + chs[i - 1] -'0';
if( t>= 10 && t<=26) f[i] += f[i - 2];
}
}
return f[n];
}
62. 不同路徑
一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記為“Start” )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為“Finish”)。
問總共有多少條不同的路徑?
例如,上圖是一個7 x 3 的網格。有多少可能的路徑?
示例 1:
輸入: m = 3, n = 2
輸出: 3
解釋:
從左上角開始,總共有 3 條路徑可以到達右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
示例 2:
輸入: m = 7, n = 3
輸出: 28
提示:
1 <= m, n <= 100
- 題目資料保證答案小於等於
2 * 10 ^ 9
狀態表示:$f[i][j] \(表示所有從起點走到\)[i,j]$的路徑
狀態表示的屬性:路徑的數量。
初始化:第一行第一列都在邊界上,路徑為1,因此f[0][j]
和f[i][0]
都為1。
集合的劃分【轉移方程】:
- 最後一步向下走:\(down = f[i-1][j]\)
- 最後一步向右走:\(right = f[i][j -1]\)
- 結果:\(f[i][j] = down + right\)
返回結果:\(res = f[i-1][j-1]\)
-
\[f[i][j] = \begin{cases} 1,& \mbox{i = 0 or j = 0} \\ dp[i - 1][j] + dp[i][j - 1], & \mbox{others} \end{cases} \]
public int uniquePaths(int m, int n) {
//f[m,n]表示走到m,n的路徑 res = f[m-1][n-1]
int[][] f = new int[m][n];
for(int i = 0; i < m; i ++){
for(int j = 0; j < n; j++){
if(i == 0 || j == 0) f[i][j] = 1;
else f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
return f[m - 1][n - 1];
}
63. 不同路徑 II
一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記為“Start” )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為“Finish”)。
現在考慮網格中有障礙物。那麼從左上角到右下角將會有多少條不同的路徑?
網格中的障礙物和空位置分別用 1
和 0
來表示。
說明:m 和n的值均不超過 100。
示例 1:
輸入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
輸出: 2
解釋:
3x3 網格的正中間有一個障礙物。
從左上角到右下角一共有 2 條不同的路徑:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
狀態表示:$f[i][j] \(表示所有從起點走到\)[i,j]$的路徑
狀態表示的屬性:路徑的數量。
初始化:第一行,第一列的路徑數。
集合的劃分【轉移方程】:
- 最後一步向下走:\(down = f[i-1][j]\)
- 最後一步向右走:\(right = f[i][j -1]\)
- 結果:\(f[i][j] = down + right\)
返回結果:\(res = f[i-1][j-1]\)
class Solution {
public int uniquePathsWithObstacles(int[][] g) {
if (g == null || g.length == 0) {
return 0;
}
// 定義 dp 陣列並初始化第 1 行和第 1 列。
int m = g.length, n = g[0].length;
int[][] f = new int[m][n];
for(int i = 0; i < m && g[i][0] != 1; i++){ //為第一行的路徑賦值,直到遇到障礙物
f[i][0] = 1;
}
for(int i = 0; i < n && g[0][i] != 1; i++){//為第一列的路徑賦值,直到遇到障礙物
f[0][i] = 1;
}
for(int i = 1; i < m; i ++){
for(int j = 1; j < n; j++){
if(g[i][j] == 1) continue; //如果遇到障礙物,跳過,預設為0
else{
f[i][j] = f[i - 1][j] + f[i][j - 1];//等於上面來的+左面來的
}
}
}
return f[m - 1][n - 1];
}
}
另一種做法:
public int uniquePathsWithObstacles(int[][] g) {
if (g == null || g.length == 0) {
return 0;
}
// 定義 dp 陣列並初始化第 1 行和第 1 列。
int m = g.length, n = g[0].length;
int[][] f = new int[m][n];
for(int i = 0; i < m; i ++){
for(int j = 0; j < n; j++){
if(g[i][j] == 1) continue; //遇到障礙物 跳過
if( i == 0 && j == 0 ) f[i][j] = 1; //左上角頂點處,初始化為1
if( i > 0 ) f[i][j] += f[i - 1][j];
if( j > 0 ) f[i][j] += f[i][j - 1];
}
}
return f[m - 1][n - 1];
}
198. 打家劫舍
你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
給定一個代表每個房屋存放金額的非負整數陣列,計算你 不觸動警報裝置的情況下 ,一夜之內能夠偷竊到的最高金額。
示例 1:
輸入:[1,2,3,1]
輸出:4
解釋:偷竊 1 號房屋 (金額 = 1) ,然後偷竊 3 號房屋 (金額 = 3)。
偷竊到的最高金額 = 1 + 3 = 4 。
示例 2:
輸入:[2,7,9,3,1]
輸出:12
解釋:偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接著偷竊 5 號房屋 (金額 = 1)。
偷竊到的最高金額 = 2 + 9 + 1 = 12 。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 400
public int rob(int[] nums) {
int n = nums.length;
int[] f = new int[n+1];//n+1 長度,不再需要考慮邊界問題 [x,1,2,3,4 ....n] 1-n
int[] g = new int[n+1];
for(int i = 1; i <= n; i++){
f[i] = Math.max(f[i-1],g[i-1]);//f[i]代表沒選num[i]的Max
g[i] = f[i-1]+nums[i-1]; // g[i]代表選nums[i]的選法max
}
return Math.max(f[n],g[n]);
}
72. 編輯距離
給你兩個單詞 word1 和 word2,請你計算出將 word1 轉換成 word2 所使用的最少運算元 。
你可以對一個單詞進行如下三種操作:
- 插入一個字元
- 刪除一個字元
- 替換一個字元
示例 1:
輸入:word1 = "horse", word2 = "ros"
輸出:3
解釋:
horse -> rorse (將 'h' 替換為 'r')
rorse -> rose (刪除 'r')
rose -> ros (刪除 'e')
示例 2:
輸入:word1 = "intention", word2 = "execution"
輸出:5
解釋:
intention -> inention (刪除 't')
inention -> enention (將 'i' 替換為 'e')
enention -> exention (將 'n' 替換為 'x')
exention -> exection (將 'n' 替換為 'c')
exection -> execution (插入 'u')
狀態表示:$f[i][j] \(所有將第一個字串前\)i\(個字母變成第二個字串前\)j$個字母的方案
狀態表示的屬性:最小值。
初始化:其中一個字串為空,結果為另一個字串的長度。
集合的劃分【轉移方程】:
- insert:\(f[i] = f[i,j-1]+1\),還沒插的時候,前\(i\)個字母已經和word2的前\(j-1\)個字母相同,插入\(word[j]\)才可能相同,因此運算元是\(f[i,j-1]+1\)
- delete:\(f[i-1,j]+1\) 保證刪除之後和w1和w2相同,表示\(i\)之前的數和\(j\)同。
- replace:\(f[i-1,j-1]+1\) 表示將w1前面的所有數轉化為w2,再加上replace操作。
- 不需要替換:不需要替換 \(f[i-1][j-1]\),第\(i\)個字母和第\(j\)個字母相等。
- 結果\(f[i][j] = min(f_1,f_2,f_3,f_4)\)
返回結果:$res = f[m][n] $
public int minDistance(String word1, String word2) {
char[] ch1 = word1.toCharArray();
char[] ch2 = word2.toCharArray();
int n1 = word1.length(),n2 = word2.length();
int[][] f = new int[n1+1][n2+1];
for(int i = 0; i <= n1; i++) f[i][0] = i; //填邊
for(int i = 0; i <= n2; i++) f[0][i] = i;
for(int i = 1; i <= n1 ; i++){
for(int j = 1; j<= n2; j++){
f[i][j] = Math.min(f[i-1][j],f[i][j-1])+1;// insert和delete
int rep = 0;//交換的操作
if(ch1[i-1] != ch2[j-1]){
rep = 1;
}
f[i][j] = Math.min(f[i][j],f[i-1][j-1]+rep); // replace or not
}
}
return f[n1][n2];
}