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))。
参考资料
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);
}
}