1143. 最长公共子序列
问题描述
给定两个字符串 text1
和 text2
,返回这两个字符串的最长公共子序列(LCS)的长度。如果不存在公共子序列,返回 0。
示例:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",长度为 3。
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",长度为 3。
输入:text1 = "abc", text2 = "def"
输出:0
解释:没有公共子序列。
算法思路
动态规划(DP 数组):
- 状态定义:
dp[i][j]
表示text1[0..i-1]
和text2[0..j-1]
的最长公共子序列长度。
- 状态转移:
- 当
text1[i-1] == text2[j-1]
时:
dp[i][j] = dp[i-1][j-1] + 1
- 当
text1[i-1] != text2[j-1]
时:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
- 当
- 初始化:
dp[0][j] = 0
(空字符串与任何字符串无公共子序列)dp[i][0] = 0
(任何字符串与空字符串无公共子序列)
空间优化(一维 DP):
- 由于
dp[i][j]
仅依赖左侧(dp[j-1]
)、上方(dp[j]
)和左上角(prev
)的值,可用一维数组代替二维数组。 - 用
prev
保存左上角值(上一轮循环中的dp[j]
),避免覆盖。
代码实现
方法一:动态规划(DP 数组)
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1]; // dp[i][j] 表示 text1[0:i) 和 text2[0:j) 的 LCS 长度
// 从 1 开始遍历(0 行/列已初始化为 0)
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; 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[m][n];
}
}
方法二:动态规划(空间优化)
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[] dp = new int[n + 1]; // dp[j] 表示当前行 text1[0:i) 和 text2[0:j) 的 LCS 长度
for (int i = 1; i <= m; i++) {
int prev = 0; // 保存左上角值(dp[i-1][j-1])
for (int j = 1; j <= n; j++) {
int temp = dp[j]; // 保存旧值(dp[i-1][j])
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
// 状态转移:LCS 长度 = 左上角值 + 1
dp[j] = prev + 1;
} else {
// 状态转移:取上方或左侧的最大值
dp[j] = Math.max(dp[j], dp[j - 1]);
}
prev = temp; // 更新左上角值为旧值(用于下一轮)
}
}
return dp[n];
}
}
算法分析
- 时间复杂度:O(mn)
需要遍历两个字符串的所有字符组合。 - 空间复杂度:
- 二维 DP:O(mn)
- 一维 DP:O(n)
算法过程
输入:text1 = "abcde", text2 = "ace"
- 二维 DP 初始化:
dp[0][*] = 0
,dp[*][0] = 0
- 状态转移:
i=1, j=1
:'a'=='a'
→dp[1][1] = dp[0][0]+1 = 1
i=1, j=2
:'a'!='c'
→dp[1][2] = max(dp[0][2], dp[1][1]) = max(0,1)=1
i=1, j=3
:'a'!='e'
→dp[1][3]=1
i=2, j=1
:'b'!='a'
→dp[2][1]=max(1,0)=1
i=2, j=2
:'b'!='c'
→dp[2][2]=max(1,1)=1
i=2, j=3
:'b'!='e'
→dp[2][3]=1
i=3, j=1
:'c'!='a'
→dp[3][1]=1
i=3, j=2
:'c'=='c'
→dp[3][2]=dp[2][1]+1=2
i=3, j=3
:'c'!='e'
→dp[3][3]=max(1,2)=2
- 后续同理,最终
dp[5][3]=3
- 结果:3
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1: 标准示例
String s1 = "abcde", t1 = "ace";
System.out.println("Test 1: " + solution.longestCommonSubsequence(s1, t1)); // 3
// 测试用例2: 完全相同字符串
String s2 = "abc", t2 = "abc";
System.out.println("Test 2: " + solution.longestCommonSubsequence(s2, t2)); // 3
// 测试用例3: 无公共子序列
String s3 = "abc", t3 = "def";
System.out.println("Test 3: " + solution.longestCommonSubsequence(s3, t3)); // 0
// 测试用例4: 部分公共子序列
String s4 = "oxcpqrsvwf", t4 = "shmtulqrypy";
System.out.println("Test 4: " + solution.longestCommonSubsequence(s4, t4)); // 2
// 测试用例5: 空字符串
String s5 = "", t5 = "abc";
System.out.println("Test 5: " + solution.longestCommonSubsequence(s5, t5)); // 0
// 测试用例6: 长字符串
String s6 = "mhunuzqrkzsnidwbun", t6 = "szulspmhwpazoxijwbe";
System.out.println("Test 6: " + solution.longestCommonSubsequence(s6, t6)); // 6
}
关键点
- 状态转移逻辑:
- 当前字符匹配:LCS 长度 = 左上角值 + 1。
- 当前字符不匹配:取上方或左侧的最大值。
- 空间优化核心:
prev
保存左上角值(dp[i-1][j-1]
)。temp
保存上方值(dp[i-1][j]
),用于更新下一轮的prev
。
- 边界处理:
- 空字符串直接返回 0。
- DP 数组
多一行一列
简化初始化。
常见问题
-
为什么需要
prev
和temp
?
prev
保存左上角值(dp[i-1][j-1]
),temp
保存上方值(dp[i-1][j]
),用于状态转移和值传递。 -
如何处理空字符串?
初始化时dp[0][j] = 0
和dp[i][0] = 0
已涵盖空字符串情况。 -
空间优化后如何保证正确性?
按行更新时,prev
和temp
精确保存了所需的历史状态,与二维 DP 等价。