问题描述
给定两个序列X和Y,找出它们最长的公共子序列的长度。子序列不要求连续,但要求相对顺序一致。
例如:X = “ABCBDAB”, Y = “BDCABA”
最长公共子序列为 “BCBA”,长度为4。
最优子结构
设dp[i][j]表示X的前i个字符和Y的前j个字符的LCS长度。
状态转移方程:
– 如果X[i-1] == Y[j-1],则dp[i][j] = dp[i-1][j-1] + 1
– 如果X[i-1] != Y[j-1],则dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Java完整实现
import java.util.*;
/**
* 最长公共子序列(LCS)完整Java实现
* 包含长度计算、路径回溯重建LCS、空间优化版本
*/
public class LongestCommonSubsequence {
/**
* 标准二维DP求解LCS长度
* 时间复杂度:O(m*n),空间复杂度:O(m*n)
*/
public static int lcsLength(String X, String Y) {
int m = X.length();
int n = Y.length();
// dp[i][j]表示X[0..i-1]和Y[0..j-1]的LCS长度
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (X.charAt(i - 1) == Y.charAt(j - 1)) {
// 当前字符匹配,LCS长度+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];
}
/**
* 回溯重建LCS字符串
*/
public static String reconstructLCS(String X, String Y) {
int m = X.length();
int n = Y.length();
int[][] dp = new int[m + 1][n + 1];
// 填充DP表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (X.charAt(i - 1) == Y.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]);
}
}
}
// 从右下角回溯到左上角重建LCS
StringBuilder lcs = new StringBuilder();
int i = m, j = n;
while (i > 0 && j > 0) {
if (X.charAt(i - 1) == Y.charAt(j - 1)) {
// 字符匹配,加入LCS
lcs.append(X.charAt(i - 1));
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
// 上方值更大,向上移动
i--;
} else {
// 左方值更大或相等,向左移动
j--;
}
}
return lcs.reverse().toString();
}
/**
* 空间优化版本:仅使用两行
* 时间复杂度:O(m*n),空间复杂度:O(min(m,n))
*/
public static int lcsLengthOptimized(String X, String Y) {
// 确保X是较短的字符串以优化空间
if (X.length() < Y.length()) {
String temp = X;
X = Y;
Y = temp;
}
int m = X.length();
int n = Y.length();
int[] prev = new int[n + 1];
int[] curr = new int[n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (X.charAt(i - 1) == Y.charAt(j - 1)) {
curr[j] = prev[j - 1] + 1;
} else {
curr[j] = Math.max(prev[j], curr[j - 1]);
}
}
// 交换prev和curr
int[] temp = prev;
prev = curr;
curr = temp;
}
return prev[n];
}
/**
* 打印DP表(用于调试和理解)
*/
public static void printDPTable(String X, String Y) {
int m = X.length();
int n = Y.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (X.charAt(i - 1) == Y.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]);
}
}
}
System.out.print(" ");
for (int j = 0; j < n; j++) {
System.out.print(Y.charAt(j) + " ");
}
System.out.println();
for (int i = 0; i <= m; i++) {
if (i > 0) {
System.out.print(X.charAt(i - 1) + " ");
} else {
System.out.print(" ");
}
for (int j = 0; j <= n; j++) {
System.out.print(dp[i][j] + " ");
}
System.out.println();
}
}
/**
* 获取所有LCS(当有多个时)
*/
public static List<String> getAllLCS(String X, String Y) {
int m = X.length();
int n = Y.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (X.charAt(i - 1) == Y.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]);
}
}
}
List<String> result = new ArrayList<>();
StringBuilder current = new StringBuilder();
findAllLCS(X, Y, m, n, dp, current, result);
return result;
}
private static void findAllLCS(String X, String Y, int i, int j,
int[][] dp, StringBuilder current, List<String> result) {
if (i == 0 || j == 0) {
result.add(current.reverse().toString());
current.reverse(); // 恢复
return;
}
if (X.charAt(i - 1) == Y.charAt(j - 1)) {
current.append(X.charAt(i - 1));
findAllLCS(X, Y, i - 1, j - 1, dp, current, result);
current.deleteCharAt(current.length() - 1);
} else {
if (dp[i - 1][j] >= dp[i][j - 1]) {
findAllLCS(X, Y, i - 1, j, dp, current, result);
}
if (dp[i][j - 1] >= dp[i - 1][j]) {
findAllLCS(X, Y, i, j - 1, dp, current, result);
}
}
}
public static void main(String[] args) {
String X = "ABCBDAB";
String Y = "BDCABA";
System.out.println("字符串X: " + X);
System.out.println("字符串Y: " + Y);
System.out.println();
System.out.println("DP表:");
printDPTable(X, Y);
System.out.println();
System.out.println("LCS长度:" + lcsLength(X, Y));
System.out.println("LCS字符串:" + reconstructLCS(X, Y));
System.out.println("空间优化版本LCS长度:" + lcsLengthOptimized(X, Y));
System.out.println("
所有LCS:");
List<String> allLCS = getAllLCS(X, Y);
for (String s : allLCS) {
System.out.println(" " + s);
}
}
}
状态转移图解
以X=”ABC”, Y=”AC”为例:
A C
0 0 0
A 0 1 1
B 0 1 1
C 0 1 2
最终dp[3][2]=2,LCS为”AC”。
复杂度分析
| 版本 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 标准DP | O(m×n) | O(m×n) | 需要回溯重建LCS |
| 空间优化 | O(m×n) | O(min(m,n)) | 仅需长度 |
| 所有LCS | O(m×n + k) | O(m×n) | k为LCS个数 |
应用场景
- 文本差异比对:Git diff、文件比较工具
- DNA序列分析:生物信息学中比较基因序列
- 拼写纠错:衡量字符串相似度
- 版本控制:追踪代码变更历史
扩展思考
- 最长公共子串:要求子序列连续,状态转移不同
- 最短公共超序列:dp[i][j] = dp[i-1][j-1]+1 (X[i-1]==Y[j-1]) 否则 min(dp[i-1][j], dp[i][j-1])+1
- 编辑距离:允许插入、删除、替换操作
总结
LCS问题是理解动态规划的经典范例。其核心在于定义合适的状态表示,并通过最优子结构将大问题分解为重叠的子问题。掌握LCS有助于理解更复杂的序列比对算法如Needleman-Wunsch和Smith-Waterman。