Description
给定两个字符串,返回两个字符串的最长公共子序列(不是最长公共子字符串),可能是多个。
Input
输入第一行为用例个数, 每个测试用例输入为两行,一行一个字符串
Output
如果没有公共子序列,不输出,如果有多个则分为多行,按字典序排序。
Sample Input 1
1
1A2BD3G4H56JK
23EFG4I5J6K7
Sample Output 1
23G456K
23G45JK
思路
动态规划
1.确定状态
对于str1和str2两个字符串,它们长度分别为m,n,设一个dp[m+1][n+1]的矩阵,dp[i][j]表示字符串str1的前i个字符配上str2的前j个字符的最长公共子序列的长度,这里要注意的是比较str1的第i个字符和str2的第j个字符时是比较的str1.charAt(i-1)和str2.charAt(j-1),注意下标.
str1[i-1]!=str2[j-1]时,dp[i][j] = max(dp[i-1][j], dp[i][j-1])
str1[i-1]==str2[j-1]时,dp[i][j] = dp[i-1][j-1] + 1
其中第一种情况表示,str1的第i个字符或str2的第j个字符(或二者都)不在dp[i][j]所代表的最长公共子序列中.
第二种情况表示,str1的第i个字符和str2的第j个字符相同,则dp[i][j]等于其子问题dp[i-1][j-1]加上当前字符的长度1.
2.初始化
dp[0...i][0] = 0,dp[0][0...j] = 0.
无论是对于str1还是str2,只要对另一字符串不取任何字符,那么他们所组成的最长公共子序列长度都为0.
3.边界情况
dp[m][n]就是str1和str2的最长公共子序列长度
4.计算顺序
从左到右,自上而下填写dp矩阵
如果只要求最长公共子序列长度那么到这里就结束了,该题中还要求求出所有公共子序列。这里采用递归回溯。
从dp矩阵最右下角dp[m][n]开始往上、往左、往左上方回溯,用一个字符串数组resCharArray保存最长公共子序列,用一个集合resSet保存不同的最长公共子序列;
1.左上
dp[i][j] > dp[i-1][j-1],那么在计算dp[i][j]时一定是选择了dp[i-1][j-1]+1这个决策,即str1[i-1]==str2[j-1],该字符属于最长公共子序列,将该字符放至resCharArray中.
2.上
dp[i][j] == dp[i-1][j],说明计算dp[i][j]时,str1[i-1]不属于最长公共子序列,往上回溯.
3.左
dp[i][j] == dp[i][j-1],说明计算dp[i][j]时,str2[j-1]不属于最长公共子序列,往左回溯.
4.分别向上和向左
dp[i][j] == dp[i-1][j] && dp[i][j] == dp[i][j-1],说明计算dp[i][j]时,要么str1[i-1]不在最长公共子序列中,要么str2[j-1]不在最长公共子序列中,但二者子问题dp[i-1][j]和dp[i][j-1]的最长公共子字符串长度相同,即存在两种路径得到不同的最长公共子序列,此时分别往上和往左递归.
代码
import java.util.*;
public class Main {
private static List<String> LCS(String str1, String str2) {
char[] cstr1 = str1.toCharArray();
char[] cstr2 = str2.toCharArray();
int[][] dp = fillDP(cstr1, cstr2);
int m = cstr1.length, n = cstr2.length;
// maxLen为最长公共子序列长度
int maxLen = dp[m][n];
// resCharArray用于保存回溯得到的子序列
char[] resCharArray = new char[maxLen];
// resSet用于无重复地保存回溯得到的可能的多个最长公共子序列
Set<String> resSet = new HashSet<String>();
// 递归回溯
getLCSString(cstr1, cstr2, m, n, dp, maxLen, resCharArray, resSet);
// 将resSet中的所有结果转存到一个链表中以对这些最长公共子序列按字典序排序
List<String> resList = new ArrayList<String>();
for (String temp : resSet) {
resList.add(temp.trim());
}
Collections.sort(resList);
return resList;
}
private static int[][] fillDP(char[] str1, char[] str2) {
int m = str1.length, n = str2.length;
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (str1[i - 1] == str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
return dp;
}
private static void getLCSString(char[] str1, char[] str2, int i, int j, int[][] dp, int index, char[] resCharArray,
Set<String> res) {
if (str1 == null || str2 == null || str1.equals("") || str2.equals("")) {
return;
}
// index初始值为最长公共子序列长度,将字符保存到结果数组中时注意现将index--再存入相应下标位置
while (index > 0) {
// dp[i][j]同时等于dp[i][j-1]和dp[i-1][j]时,说明此时有两种路径得到最长公共子序列
if (i > 1 && j > 1 && dp[i][j] == dp[i][j - 1] && dp[i][j] == dp[i - 1][j]) {
// 要分别往上(i-1)回溯,往左(j-1)回溯
getLCSString(str1, str2, i - 1, j, dp, index, resCharArray, res);
getLCSString(str1, str2, i, j - 1, dp, index, resCharArray, res);
// 完成以上回溯已则经得到结果直接返回即可
return;
} else if (i > 1 && dp[i][j] == dp[i - 1][j]) {
i--;
} else if (j > 1 && dp[i][j] == dp[i][j - 1]) {
j--;
} else {
index--;
i--;
j--;
resCharArray[index] = str1[i];
}
}
res.add(String.valueOf(resCharArray));
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int cases = Integer.parseInt(sc.nextLine());
while (cases-- > 0) {
String str1 = sc.nextLine();
String str2 = sc.nextLine();
List<String> res = LCS(str1, str2);
for (String resString : res) {
System.out.println(resString);
}
}
sc.close();
}
}