Topic
- Dynamic Programming
Description
https://leetcode.com/problems/longest-common-subsequence/
Given two strings text1
and text2
, return the length of their longest common subsequence. If there is no common subsequence, return 0
.
A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.
For example, "ace"
is a subsequence of "abcde"
. A common subsequence of two strings is a subsequence that is common to both strings.
Example 1:
Input: text1 = "abcde", text2 = "ace"
Output: 3
Explanation: The longest common subsequence is "ace" and its length is 3.
Example 2:
Input: text1 = "abc", text2 = "abc"
Output: 3
Explanation: The longest common subsequence is "abc" and its length is 3.
Example 3:
Input: text1 = "abc", text2 = "def"
Output: 0
Explanation: There is no such common subsequence, so the result is 0.
Constraints:
1 <= text1.length, text2.length <= 1000
text1
andtext2
consist of only lowercase English characters.
Analysis
方法一:標準DP
動態規劃五部曲:
- 確定dp數組(dp table)以及下標的含義;
- 確定遞推公式;
- dp數組如何初始化;
- 確定遍歷順序;
- 舉例推導dp數組。
DP數組意,歸納遞推式,初始元素值,定序來遍歷,最後舉個例。
用動規五部曲分析如下
1.確定dp數組(dp table)以及下標的含義
dp[i][j]表示 從text1的下標0開始截取長度爲i的字符串 與 從text2的下標0開始截取長度爲j的字符串 的最長公共子序列的長度。
也可以換句話說。
dp[i][j]表示 從text1的下標0開始截取到下標i-1截取的字符串(包含下標i-1處的字符) 與 從text2的下標0開始截取到下標j-1截取的字符串(包含下標j-1處的字符) 的最長公共子序列的長度。
2.確定遞推公式
主要就是兩大情況:text1[i - 1] 與 text2[j - 1]相同,text1[i - 1] 與 text2[j - 1]不相同
- 如果text1[i - 1] 與 text2[j - 1]相同,那麼找到了一個公共元素,所以
dp[i][j] = dp[i - 1][j - 1] + 1;
。 - 如果text1[i - 1] 與 text2[j - 1]不相同,那就看看text1[0, i - 2]與text2[0, j - 1]的最長公共子序列 和 text1[0, i - 1]與text2[0, j - 2]的最長公共子序列,取最大的,即:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
。
代碼如下:
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
3.dp數組如何初始化
先看看dp[i][0]應該是多少呢?
test1[0, i-1]和空字符串的最長公共子序列自然是0,所以dp[i][0] = 0;
同理dp[0][j]也是0。
其他下標都是隨着遞推公式逐步覆蓋,初始值是默認的即可。
總之,全部都是0,即默認的。
int[][] dp = new int[text1.length() + 1][text2.length() + 1];
4.確定遍歷順序
從遞推公式,可以看出,有三個方向可以推出dp[i][j],如圖:
那麼爲了在遞推的過程中,這三個方向都是經過計算的數值,所以要從前向後,從上到下來遍歷這個矩陣。
5.舉例推導dp數組
以輸入:text1 = "abcde", text2 = "ace" 爲例,dp狀態如圖:
最後紅框dp[text1.length()][text2.length()]爲最終結果。
再次重申,dp[i][j]表示 從text1的下標0開始截取長度爲i的字符串 與 從text2的下標0開始截取長度爲j的字符串 的最長公共子序列的長度。
方法二:方法一的空間優化
進一步地觀察,方法一的代碼遍歷時只需前一行以及目前一行的信息即可。因此,dp數組初始化兩行的。
注意,用k ^ 1、k ^= 1來切換 dp[0] (第一行) 與 dp[1] (第二行)。
注意,m % 2 與 m & 1 意同。
方法三:方法一的空間優化Plus
再進一步地觀察,方法一的代碼遍歷時只需前一行以及目前一行的前一元素信息即可。因此,dp數組初始化一行的,另加3個變量進行輔助。
最後,優化後的時空雜度分別爲:O(m * n),O(min(m, n))。
參考資料
- 動態規劃:最長公共子序列
- <a href="https://leetcode.com/problems/longest-common-subsequence/discuss/351689/JavaPython-3-Two-DP-codes-of-O(mn)-and-O(min(m-n))-spaces-w-picture-and-analysis">[Java/Python 3] Two DP codes of O(mn) & O(min(m, n)) spaces w/ picture and analysis</a>
Submission
public class LongestCommonSubsequence {
// 方法一:標準DP
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp = new int[text1.length() + 1][text2.length() + 1];
for (int i = 1; i <= text1.length(); i++) {
for (int j = 1; j <= text2.length(); j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.length()][text2.length()];
}
// 方法二:方法一的空間優化
public int longestCommonSubsequence2(String s1, String s2) {
int m = s1.length(), n = s2.length();
if (m < n)// 本判斷語句目的可以減少空間複雜度。可以移除本判斷語句。
return longestCommonSubsequence2(s2, s1);
int[][] dp = new int[2][n + 1];
for (int i = 1, k = 1; i <= m; ++i, k ^= 1)
for (int j = 1; j <= n; ++j)
if (s1.charAt(i - 1) == s2.charAt(j - 1))
dp[k][j] = 1 + dp[k ^ 1][j - 1];
else
dp[k][j] = Math.max(dp[k ^ 1][j], dp[k][j - 1]);
return dp[m & 1][n];
}
// 方法三:方法一的空間優化Plus
public int longestCommonSubsequence3(String text1, String text2) {
int m = text1.length(), n = text2.length();
if (m < n) {// 本判斷語句目的可以減少空間複雜度。可以移除本判斷語句。
return longestCommonSubsequence3(text2, text1);
}
int[] dp = new int[n + 1];
for (int i = 1; i <= text1.length(); ++i) {
for (int j = 1, left = 0, leftUp = 0, newOne = 0; j <= text2.length(); ++j) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
newOne = leftUp + 1;
} else {
newOne = Math.max(left, dp[j]);
}
leftUp = dp[j];
left = dp[j] = newOne;// 爲同行下一元素以及下行元素做準備
}
}
return dp[n];
}
}
Test
import static org.junit.Assert.*;
import org.junit.Test;
public class LongestCommonSubsequenceTest {
@Test
public void test() {
LongestCommonSubsequence obj = new LongestCommonSubsequence();
assertEquals(3, obj.longestCommonSubsequence("abcde", "ace"));
assertEquals(3, obj.longestCommonSubsequence("ace", "abcde"));
assertEquals(3, obj.longestCommonSubsequence("abc", "abc"));
assertEquals(0, obj.longestCommonSubsequence("abc", "def"));
}
@Test
public void test2() {
LongestCommonSubsequence obj = new LongestCommonSubsequence();
assertEquals(3, obj.longestCommonSubsequence2("abcde", "ace"));
assertEquals(3, obj.longestCommonSubsequence2("ace", "abcde"));
assertEquals(3, obj.longestCommonSubsequence2("abc", "abc"));
assertEquals(0, obj.longestCommonSubsequence2("abc", "def"));
}
@Test
public void test3() {
LongestCommonSubsequence obj = new LongestCommonSubsequence();
assertEquals(3, obj.longestCommonSubsequence3("abcde", "ace"));
assertEquals(3, obj.longestCommonSubsequence3("ace", "abcde"));
assertEquals(3, obj.longestCommonSubsequence3("abc", "abc"));
assertEquals(0, obj.longestCommonSubsequence3("abc", "def"));
}
@Test
public void testOther() {
assertEquals(1 & 1, 1 % 2);
assertEquals(2 & 1, 2 % 2);
assertEquals(3 & 1, 3 % 2);
assertEquals(4 & 1, 4 % 2);
}
}