首先区分一下子序列和子串:子串一定要连续,子序列则不一定。比如,对于字符串1AB2345CD
,子串可以是1AB,2345,CD等等,子序列可以是12345,ABCD等等;
例如这道题,是求最长公共子串,我们先求一个最长公共子序列
最长公共子序列
思路分析:
对于这种分别用两个指针i,j在两个字符串/数组上移动,得出一个"最长/最大"的问题,一般可以用动态规划。
- 定义:
dp[i][j]
表示s1[i..]和s2[j..]的最长公共子序列长度,当i=s1.length()时表示空串,s2同理 - base case: 空字符跟任何非空字符都不是公共子序列,
dp[s1.length()][j..] = 0
,dp[i..][s2.length()] = 0
; - 递推公式:如果
s1.charAt(i) = s2.charAt(j)
,长度+1:dp[i][j] = dp[i-1][j-1]+1
;否则就在s1和s2中选择一个字符跳过,选择结果较大的那个
// 相等 则为s1[i+1..]和s2[j+1..]的最长公共子序列长度+1
if(s1.charAt(i)==s2.charAt(j)){
dp[i][j] = dp[i+1][j+1]+1;
// 不相等,在(s1[i+1..]和s2[j..]匹配)和(s1[i..]和s2[j+1..]匹配)两个当中选择一个大的
}else{
dp[i][j] = Math.max(dp[i+1][j],dp[i][j+1]);
}
代码实现
public int isMatching(String s1,String s2){
int m = s1.length();
int n = s2.length();
//dp[i][j] 表示s1[i..]和s2[j..]的公共子序列长度
int[][] dp = new int[m+1][n+1];
//初始化:空串和非空字符的不构成公共子序列
for(int i=0;i<m;i++){
dp[i][n] = 0;
}
for(int j=0;j<n;j++){
dp[m][j] = 0;
}
// 空字符是空字符的公共子序列
dp[m][n] = 0;
for(int i=m-1;i>=0;i--){
for(int j=n-1;j>=0;j--){
if(s1.charAt(i)==s2.charAt(j)){
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[0][0];
}
最长公共子串
思路分析:
- 定义:dp[i][j]表示以第i个字符结尾的s1和第j个字符结尾的s2的最长公共子串
- base case: dp数组初始化为0
- 递推公式:
- 如果两个字符相等,那么在这个位置上(i,j) 的公共子串长度为1,再加上前面一个位置(i-1,j-1) 的公共子串长度构成了(0..i)和(0..j)的公共子串长度;
- 如果两个字符不相等,
dp[i][j]
直接记为0就好了 - 为什么不记为
dp[i-1][j-1]
?子串必须是连续的,这里已经不相等了,后面的子串从这里开始公共长度就是0;
代码实现
public String LCS (String str1, String str2) {
// write code here
int m = str1.length();
int n = str2.length();
int[][] dp = new int[m+1][n+1];
int maxlength = Integer.MIN_VALUE;
int maxLastIndex = -1;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(str1.charAt(i-1)==str2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1]+1;
// 更新最大长度
if(dp[i][j]>maxlength){
maxlength = dp[i][j];
// 记录下标
maxLastIndex = j;
}
}else{
// 不相等 公共子串断开 为0
dp[i][j] = 0;
}
}
}
// 题目保证最长公共子串存在
return str2.subSequence(maxLastIndex-maxlength,maxLastIndex).toString();
}
状态压缩
在计算最长公共子串的代码中可以发现,dp[i][j]
要么等于0,要么只跟前面一个状态(dp[i-1][j-1]
)有关,使用一维数组就可以完成状态转移:dp[j] = dp[j-1] + 1
;
代码实现
public static String LCS (String str1, String str2) {
// write code here
int m = str1.length();
int n = str2.length();
int[] dp = new int[n+1];
int maxlength = Integer.MIN_VALUE;
int maxLastIndex = -1;
for(int i = 1;i<=m;i++){
// 注意这里遍历顺序是相反的
for(int j = n;j>=1;j--){
if (str1.charAt(i-1)==str2.charAt(j-1)){
dp[j] = dp[j-1] + 1;
if(dp[j]>maxlength){
maxlength = dp[j];
maxLastIndex = j;
}
}else{
dp[j] = 0;
}
}
}
// 题目保证最长公共子串存在
return str2.subSequence(maxLastIndex-maxlength,maxLastIndex).toString();
}
Question: 为什么要用相反的顺序遍历? 如果用相同的顺序遍历:
如果用相反的顺序遍历:
总结
因为子串和子序列的区别,在状态转移时会有不同; 时间复杂度都是O(mn),空间复杂度也是O(mn),状态压缩后空间复杂度可以优化到O(n)