LeetCode1143. 最长公共子序列

☆☆☆☆思路:Longest Common Sequence (LCS) 问题。要对两个字符串进行扫描,因此需要两个维度进行动态规划。

  1. 状态的定义:dp[m][n] 表示 S1[0...m] 和 S2[0...n] 的最长公共子序列的长度。

  2. 状态转移:每次对最新需要考虑的字符,s1[m] 和 s2[n]

      ① 如果s1[m] == s2[n],则 dp[m][n] = 1 + dp[m-1][n-1]

      ②如果s1[m] != s2[n],则 dp[m][n] = max {dp[m-1][n], dp[m][n-1]}             (往前缩一个)

  3. 初始化:对于dp矩阵(m*n)的第一列,需要注意一旦dp[i][0]被设置为1,那么之后的dp[i+1...m][0]也都为1; dp矩阵的第一行,同理。

  4. 结果:从左到右,再从上到下计算dp矩阵,右下角的值即为所求。

代码1:

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        // 状态的定义:dp[m][n]表示s1[0...m] 和 s2[0...n] 的最长公共子序列的长度
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m][n];
        dp[0][0] = text1.charAt(0) == text2.charAt(0) ? 1 : 0;
        for (int i = 1; i < m; i++) {
            dp[i][0] = Math.max(dp[i-1][0], text1.charAt(i) == text2.charAt(0) ? 1 : 0); // 注意还需要考虑以前的
        }
        for (int j = 1; j < n; j++) {
            dp[0][j] = Math.max(dp[0][j-1], text1.charAt(0) == text2.charAt(j) ? 1 : 0); // 注意还需要考虑以前的
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (text1.charAt(i) == text2.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[m-1][n-1];
    }
}

代码2(如果只求长度,参考“矩阵的最小路径和”,空间复杂度可压缩为O(min{M, N}))

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        /**
         *  空间压缩:
         *      二维dp数组中每个格子内的值会依赖它的左边,上边和左上边,和其他的值无关。
         *      而左上角的值有可能会被上一步计算的时候就被替换掉了,所以必须要先保存下来。
         */
        int m = text1.length(), n = text2.length();
        int[] dp = new int[n];
        // 初始化第一行
        dp[0] = text1.charAt(0) == text2.charAt(0) ? 1 : 0;
        for (int j = 1; j < n; j++) {
            dp[j] = Math.max(dp[j-1], text1.charAt(0) == text2.charAt(j) ? 1 : 0);
        }
        for (int i = 1; i < m; i++) {
            // 单独算第一列
            int pre = dp[0];
            dp[0] = Math.max(dp[0], text1.charAt(i) == text2.charAt(0) ? 1 : 0);
            for (int j = 1; j < n; j++) {
                int temp = dp[j];  // dp[j] 这个值会被替换,所以替换之前要把他保存下来
                if (text1.charAt(i) == text2.charAt(j)) {
                    dp[j] = pre + 1;  // 左上
                }else {
                    dp[j] = Math.max(dp[j-1], dp[j]);  // 左 或者 上
                }
                pre = temp;
            }
        }
        return dp[n-1];
    }
}

【举一反三】:除了求出最长公共子序列的长度,还需要会根据dp数组求出最长公共子序列是什么!-----------参考左神书P210

思路为根据dp状态还原出求解dp的过程,来自哪个策略就朝哪个方向移动。具体方法为:

  1. 从dp矩阵右下角开始,有三种移动方式:向上,向左,向左上。设移动过程中, i 表示此时的行数, j 表示此时的列数,同时 res 表示最长公共子序列;

  2. 如果 dp[i][j] 大于 dp[i-1][j] 和 dp[i][j-1],说明之前在计算dp[i][j] 时,一定是选择了决策 dp[i-1][j-1] + 1,可以确定str[1]等于str[2],并且这个字符一定属于最长公共子序列,放入res,然后向左上方移动;

  3. 如果 dp[i][j]  等于dp[i-1][j] , 说明之前在计算dp[i][j] 时, dp[i-1][j-1] + 1这个决策不是必须选择的决策,向上方移动;

  4.  如果 dp[i][j]  等于dp[i][j-1],与步骤3同理,向左方移动

  5. 如果 dp[i][j] 等于 dp[i-1][j] 和 dp[i][j-1],向上还是向下无所谓,选择其一即可, 反正不会错过必须选择的字符。

    private String lcse(String s1, String s2) {
        if (s1 == null || s2 == null || s1.equals("") || s2.equals("")) {
            return "";
        }
        int[][] dp = getdp(s1, s2);
        int m = s1.length() - 1, n = s2.length() - 1;
        char[] res = new char[dp[m][n]];
        int index = res.length - 1;
        while (index >= 0) {
            if (n > 0 && dp[m][n] == dp[m][n-1]) {
                n --;
            }else if (m > 0 && dp[m][n] == dp[m-1][n]) {
                m --;
            }else {
                res[index--] = s1.charAt(m);
                m --;
                n --;
            }
        }
        return String.valueOf(res);
    }
    public int[][] getdp(String text1, String text2) {
        // 状态的定义:dp[m][n]表示s1[0...m] 和 s2[0...n] 的最长公共子序列的长度
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m][n];
        dp[0][0] = text1.charAt(0) == text2.charAt(0) ? 1 : 0;
        for (int i = 1; i < m; i++) {
            dp[i][0] = Math.max(dp[i-1][0], text1.charAt(i) == text2.charAt(0) ? 1 : 0);
        }
        for (int j = 1; j < n; j++) {
            dp[0][j] = Math.max(dp[0][j-1], text1.charAt(0) == text2.charAt(j) ? 1 : 0);
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (text1.charAt(i) == text2.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;
    }

【复杂度分析】:总的时间复杂度为O(M*N),空间复杂度为O(M*N)。  如果只求长度,空间复杂度可压缩为O(min{M, N})

  1. 求dp矩阵的某个位置就是简单比较值,时间复杂度为O(1),由于dp大小为M*N,所以求dp矩阵的时间复杂度为O(M*N)

  2. 通过dp得到最长公共子序列的过程为O(M+N),因为向左最多移动N个位置,向上最多移动M个位置。

  3. 因此,总的时间复杂度为O(M*N)

原文地址:https://www.cnblogs.com/HuangYJ/p/14232685.html