https://labuladong.github.io/ebook/動態規劃系列/
- 最值+最優子結構+重疊子問題:
- 兩個數組/字符串
- dp[i][j]:SelArray1[0~i] SelArray2[0~j]
- 一般與dp[i-1][j-1],dp[i-1][j],dp[i][j-1]有關
- dp[i][j]:SelArray1[0~i] SelArray2[0~j]
- 一個數組/字符串
- dp[i]:以i爲尾的...
- dp[i][j]:SelArray[i,i+1,,,j-1,j]
- 一般與dp[i+1][j-1],dp[i][j-1],dp[i+1][j]有關
- 兩個數組/字符串
一、兩種思路
1、第一種思路模板是一個一維的 dp 數組:
int n = array.length;
int[] dp = new int[n];
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
dp[i] = 最值(dp[i], dp[j] + ...)
}
}
舉個我們寫過的例子「最長遞增子序列」,在這個思路中 dp 數組的定義是:
在子數組 array[0..i] 中,我們要求的子序列(最長遞增子序列)的長度是 dp[i]。
爲啥最長遞增子序列需要這種思路呢?前文說得很清楚了,因爲這樣符合歸納法,可以找到狀態轉移的關係,這裏就不具體展開了。
2、第二種思路模板是一個二維的 dp 數組:
int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (arr[i] == arr[j])
dp[i][j] = dp[i][j] + ...
else
dp[i][j] = 最值(...)
}
}
這種思路運用相對更多一些,尤其是涉及兩個字符串/數組的子序列,比如前文講的「最長公共子序列」。本思路中 dp 數組含義又分爲「只涉及一個字符串」和「涉及兩個字符串」兩種情況。
2.1 涉及兩個字符串/數組時(比如最長公共子序列),dp 數組的含義如下:
在子數組 arr1[0..i] 和子數組 arr2[0..j] 中,我們要求的子序列(最長公共子序列)長度爲 dp[i][j]。
2.2 只涉及一個字符串/數組時(比如本文要講的最長迴文子序列),dp 數組的含義如下:
在子數組 array[i..j] 中,我們要求的子序列(最長迴文子序列)的長度爲 dp[i][j]。
解決兩個字符串的動態規劃問題,一般都是用兩個指針 i,j
分別指向兩個字符串的最後,然後一步步往前走,縮小問題的規模。
int minDistance(String s1, String s2) {//動態規劃 二維
int m = s1.length(), n = s2.length();
int[][] dp = new int[m + 1][n + 1];//空串算1個
// base case
for (int i = 1; i <= m; i++)
dp[i][0] = i;
for (int j = 1; j <= n; j++)
dp[0][j] = j;
// 自底向上求解
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
if (s1.charAt(i-1) == s2.charAt(j-1))
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(
dp[i - 1][j] + 1,//刪除
dp[i][j - 1] + 1,//插入
dp[i-1][j-1] + 1//替換
);
// 儲存着整個 s1 和 s2 的最小編輯距離
return dp[m][n];
}
int minDistance(String s1, String s2) {//動態規劃 一維
int m = s1.length(), n = s2.length();
int[] dp = new int[n + 1];
// base case
for (int j = 0; j <= n; j++)
dp[j] = j;
// 自底向上求解
for (int i = 1; i <= m; i++)
for (int j = 1,temp; j <= n; j++){
temp=dp[j];
if(j==1) dp[j]=i;
else if (s1.charAt(i-1) == s2.charAt(j-1))
dp[j] = dpij;
else
dp[j] = min(
dp[j] + 1,//刪除
dp[j - 1] + 1,//插入
dpij+ 1);//替換
dpij=temp;
}
// 儲存着整個 s1 和 s2 的最小編輯距離
return dp[n];
}
最長公共子序列(Longest Common Subsequence,簡稱 LCS)是一道非常經典的面試題目,因爲它的解法是典型的二維動態規劃,大部分比較困難的字符串問題都和這個問題一個套路,比如說編輯距離。而且,這個算法稍加改造就可以用於解決其他問題,所以說 LCS 算法是值得掌握的。
題目就是讓我們求兩個字符串的 LCS 長度:
輸入: str1 = "abcde", str2 = "ace"
輸出: 3
解釋: 最長公共子序列是 "ace",它的長度是 3
用兩個指針 i
和 j
從後往前遍歷 s1
和 s2
,如果 s1[i]==s2[j]
,那麼這個字符一定在 lcs
中;否則的話,s1[i]
和 s2[j]
這兩個字符至少有一個不在 lcs
中,需要丟棄一個。
對於第一種情況,找到一個 lcs
中的字符,同時將 i
j
向前移動一位,並給 lcs
的長度加一;對於後者,則嘗試兩種情況,取更大的結果。
def longestCommonSubsequence(str1, str2) -> int:
m, n = len(str1), len(str2)
# 構建 DP table 和 base case
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 進行狀態轉移
for i in range(1, m + 1):
for j in range(1, n + 1):
if str1[i - 1] == str2[j - 1]:
# 找到一個 lcs 中的字符
dp[i][j] = 1 + dp[i-1][j-1]
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])#dp[i-1][j-1]一定是最小,所以不用比較
return dp[-1][-1]
dp[i][j]和dp[i-1][j],dp[i][j-1],dp[i-1][j-1]有關的狀態轉移方程都可以最終優化成一維dp
int longestPalindromeSubseq(string s) {//動態規劃 二維(可優化爲一維)
int n = s.size();
// dp 數組全部初始化爲 0
vector<vector<int>> dp(n, vector<int>(n, 0));
// base case
for (int i = 0; i < n; i++)
dp[i][i] = 1;
// 反着遍歷保證正確的狀態轉移
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
// 狀態轉移方程
if (s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
// 整個 s 的最長迴文子串長度
return dp[0][n - 1];
}