最长公共子序列(力扣第1143题)
1143.最长公共子序列
给定两个字符串text1和text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。
对于两个子序列 S1 和 S2,找出它们最长的公共子序列。
定义一个二维数组dp用来存储最长公共子序列的长度,其中 dp[i][j]表示S1的前i个字符与S2的前j个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:
- 当 S1i==S2j 时,那么就能在S1的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加1,即 dp[i][j] = dp[i-1][j-1] + 1。
- 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前j个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。
综上,最长公共子序列的状态转移方程为:
对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。
与最长递增子序列相比,最长公共子序列有以下不同点:
- 针对的是两个序列,求它们的最长公共子序列。
- 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j。
- 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
public static int longestCommonSubsequence(String text1, String text2) {
int n = text1.length();
int m = text2.length();
int[][] dp = new int[n+1][m+1];
for (int i = 1; i <= text1.toCharArray().length; i++) {
for (int i1 = 1; i1 <= text2.toCharArray().length; i1++) {
if (text1.charAt(i-1) == text2.charAt(i1-1)){
dp[i][i1] = dp[i-1][i1-1] + 1;
}else {
dp[i][i1] = Math.max(dp[i][i1-1],dp[i-1][i1]);
}
}
}
return dp[n][m];
}
上面求的是两个字符串的最大公共子序列的长度,如果想要得到两个字符串的最大公共子序列是什么,那么可以拿根据状态转移数组dp来获取:
易知,两个字符串的最大公共子序列的长度值位于状态转移数组的最右下方位置,即dp[n][m],那我们就从这个位置出发寻找最长公共子序列:
1、从右下角出发,可以移动的方式一共有三种:向上,向左,向左上。设移动的过程中,i表示此时所在的行数、j表示此时所在的列数;
2、如果dp[i][j] == dp[i-1][j] 或者dp[i][j] == dp[i][j-1] ,说明str1[i] != str2[j] ,那么直接向左或者向上移动即可;
3、如果dp[i][j]大于dp[i-1][j]和dp[i][j-1],那么说明在计算dp[i][j]的时候,str1[i] == str2[j],所以选择了dp[i-1][j-1] + 1,此字符一定属于最长公共子序列,将此字符放入数组中,然后继续向左上方移动。
public static int[][] longestCommonSubsequence2(String text1, String text2) {
int n = text1.length();
int m = text2.length();
int[][] dp = new int[n+1][m+1];
for (int i = 1; i <= text1.toCharArray().length; i++) {
for (int i1 = 1; i1 <= text2.toCharArray().length; i1++) {
if (text1.charAt(i-1) == text2.charAt(i1-1)){
dp[i][i1] = dp[i-1][i1-1] + 1;
}else {
dp[i][i1] = Math.max(dp[i][i1-1],dp[i-1][i1]);
}
}
}
return dp;
}
public static String lcse(String text1, String text2){
char[] str1 = text1.toCharArray();
int n = text1.length();
int m = text2.length();
int[][] dp = longestCommonSubsequence2(text1,text2);
char[] res = new char[dp[n][m]];
int index = res.length - 1;
while (index >= 0){
if ( m > 0 && n > 0 && dp[n][m] > dp[n-1][m] && dp[n][m] > dp[n][m-1]){
res[index--] = str1[n-1];
m--;
n--;
}else if (m > 0 && dp[n][m] == dp[n][m-1]){
m--;
}else if (n > 0 && dp[n][m] == dp[n-1][m]){
n--;
}
}
return String.valueOf(res);
}
其实寻找最长公共子序列的过程,就是回顾状态转移数组计算的过程,我们可以根据状态转移数组中的每一个元素的值与其左边和上方的元素值的大小关系确定,元素的行数和列数各自代表的在两个字符串中对应的字符是否相等,从而确定它们是否在最长公共子序列中。