动态规划算法之最长公子序列详细解读(附带Java代码解读)

最长公共子序列(Longest Common Subsequence, LCS)问题是动态规划中另一个经典问题,广泛用于比较两个序列的相似度。它的目标是找到两个序列之间最长的公共子序列(不是连续的),使得这个子序列同时出现在两个序列中。

1. 问题定义

给定两个序列 XY,要找到它们的最长公共子序列,即一个序列 Z,它同时是 XY 的子序列,且 Z 的长度最大。

例如:

  • 对于序列 X = "ABCBDAB"Y = "BDCAB",它们的最长公共子序列是 "BCAB""BDAB",长度为 4。

2. 动态规划思路

LCS 问题可以通过动态规划求解。我们可以定义一个二维的 DP 表 dp[i][j],表示序列 X 的前 i 个字符与序列 Y 的前 j 个字符的最长公共子序列的长度。

状态转移方程:
  • X[i-1] == Y[j-1] 时,说明 X[i]Y[j] 是公共子序列的一部分,此时 dp[i][j] = dp[i-1][j-1] + 1
  • X[i-1] != Y[j-1] 时,最长公共子序列要么不包含 X[i-1],要么不包含 Y[j-1],因此 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
初始化:
  • i=0j=0 时,dp[i][j] = 0,表示空序列与任何序列的公共子序列长度为 0。

3. 状态转移图解

X = "ABCBDAB"Y = "BDCAB" 为例:

0 B D C A B
0 0 0 0 0 0 0
A 0 0 0 0 1 1
B 0 1 1 1 1 2
C 0 1 1 2 2 2
B 0 1 1 2 2 3
D 0 1 2 2 2 3
A 0 1 2 2 3 3
B 0 1 2 2 3 4

其中,dp[7][5] = 4,表示最长公共子序列的长度为 4。

4. 动态规划代码实现

下面是使用动态规划求解最长公共子序列的 Java 代码:

public class LongestCommonSubsequence {

    public static void main(String[] args) {
        String X = "ABCBDAB";
        String Y = "BDCAB";

        // 调用LCS函数
        int lcsLength = lcs(X, Y);
        System.out.println("The length of Longest Common Subsequence is: " + lcsLength);
    }

    // LCS动态规划实现
    public static int lcs(String X, String Y) {
        int m = X.length();
        int n = Y.length();

        // 创建dp二维数组,表示X的前i个字符与Y的前j个字符的LCS长度
        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]);
                }
            }
        }

        // 返回右下角的值,即最长公共子序列的长度
        return dp[m][n];
    }
}

5. 详细解读代码

5.1 输入和输出

main 方法中,定义了两个字符串 XY,然后调用 lcs() 方法来计算它们的最长公共子序列的长度,并输出结果。

5.2 lcs() 函数详解
int m = X.length();
int n = Y.length();

// 创建dp二维数组,表示X的前i个字符与Y的前j个字符的LCS长度
int[][] dp = new int[m + 1][n + 1];

我们首先获取字符串 XY 的长度 mn,并创建一个二维数组 dp[][]。数组的大小为 [m+1][n+1],其中 dp[i][j] 表示 X 的前 i 个字符与 Y 的前 j 个字符的最长公共子序列的长度。

5.3 动态规划填表过程
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]);
        }
    }
}

接着,使用两个嵌套循环遍历字符串 XY 的每一个字符:

  • X[i-1] == Y[j-1]:说明 X[i]Y[j] 组成了最长公共子序列的一部分,因此 dp[i][j] = dp[i-1][j-1] + 1
  • X[i-1] != Y[j-1]:最长公共子序列要么不包含 X[i-1],要么不包含 Y[j-1],因此 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
5.4 返回结果
return dp[m][n];

最后,dp[m][n] 表示 X 的前 m 个字符与 Y 的前 n 个字符的最长公共子序列的长度,因此返回该值。

6. 重建最长公共子序列

如果我们不仅想知道最长公共子序列的长度,还想知道最长公共子序列的具体内容,我们可以从 dp 数组反向推导出子序列:

public static String findLCS(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.append(X.charAt(i - 1));
            i--;
            j--;
        } else if (dp[i - 1][j] > dp[i][j - 1]) {
            i--;
        } else {
            j--;
        }
    }

    return lcs.reverse().toString(); // 由于是从后往前找到的,需反转结果
}

7. 复杂度分析

  • 时间复杂度O(m * n),其中 mn 分别是两个字符串的长度。我们需要填充一个大小为 m * n 的二维数组。
  • 空间复杂度O(m * n),由于使用了二维数组 dp[][]

8. 总结

最长公共子序列问题通过动态规划求解,利用二维 DP 数组记录子问题的解,并通过状态转移方程逐步构建最终解。通过反向追溯 DP 数组,还可以重建具体的公共子序列内容。LCS 问题在文本比对、基因序列分析等领域有广泛的应用。

你可能感兴趣的:(算法分析,算法,动态规划,java)