LC-1092. 最短公共超序列(暴力递归=>记忆化搜索=>动态规划)

零神:从递归到递推,教你一步步思考动态规划!

https://leetcode.cn/problems/shortest-common-supersequence/solution/cong-di-gui-dao-di-tui-jiao-ni-yi-bu-bu-auy8z/

1092. 最短公共超序列

难度困难176

给出两个字符串 str1str2,返回同时以 str1str2 作为子序列的最短字符串。如果答案不止一个,则可以返回满足条件的任意一个答案。

(如果从字符串 T 中删除一些字符(也可能不删除,并且选出的这些字符可以位于 T 中的 任意位置),可以得到字符串 S,那么 S 就是 T 的子序列)

示例:

输入:str1 = "abac", str2 = "cab"
输出:"cabac"
解释:
str1 = "abac" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 的第一个 "c"得到 "abac"。 
str2 = "cab" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 末尾的 "ac" 得到 "cab"。
最终我们给出的答案是满足上述属性的最短字符串。

提示:

  1. 1 <= str1.length, str2.length <= 1000
  2. str1str2 都由小写英文字母组成。

题解:https://leetcode.cn/problems/shortest-common-supersequence/solution/cong-di-gui-dao-di-tui-jiao-ni-yi-bu-bu-auy8z/

暴力递归(超时)

class Solution {
    /**
        考虑从后往前构造答案,答案的最后一个字母是什么?要么是s的最后一个字母,要么是t的最后一个字母
        如果s[n]和t[n]的最后一个字母不同,
            如果答案最后一个字母是s的,问题变成构造s[n-1]和t[n]的答案
            如果答案最后一个字母是t的,问题变成构造s[n]和t[n-1]的答案
        如果s[n]和t[n]的最后一个字母相同
            问题变成构造s[n-1]和t[n-1]的答案
        边界条件:
            如果s是空串,答案为t
            如果t是空串,答案为s
     */
    public String shortestCommonSupersequence(String s, String t) {
        if(s.isEmpty()) return t; // s 是空串,返回剩余的 t
        if(t.isEmpty()) return s; // t 是空串,返回剩余的 s
        String s1 = s.substring(0, s.length() - 1);
        String t1 = t.substring(0, t.length() - 1);
        char x = s.charAt(s.length() - 1);
        char y = t.charAt(t.length() - 1);
        if(x == y) {// 最短公共超序列一定包含 x
            return shortestCommonSupersequence(s1, t1) + x;
        }
        String ans1 = shortestCommonSupersequence(s1, t);
        String ans2 = shortestCommonSupersequence(s, t1);
        // 取 ans1 和 ans2 中更短的组成答案
        if(ans1.length() < ans2.length()){
            return ans1 + x;
        }
        return ans2 + y;
    }
}

暴力递归转记忆化搜索(超内存)

class Solution {
    /**
        考虑从后往前构造答案,答案的最后一个字母是什么?要么是s的最后一个字母,要么是t的最后一个字母
        如果s[n]和t[n]的最后一个字母不同,
            如果答案最后一个字母是s的,问题变成构造s[n-1]和t[n]的答案
            如果答案最后一个字母是t的,问题变成构造s[n]和t[n-1]的答案
        如果s[n]和t[n]的最后一个字母相同
            问题变成构造s[n-1]和t[n-1]的答案
        边界条件:
            如果s是空串,答案为t
            如果t是空串,答案为s
     */
    private String s, t;
    private String[][] memo; // [i][j]表示s前i个字母,t前j个字母构造的最大答案
    
    public String shortestCommonSupersequence(String s, String t) {
        this.s = s;
        this.t = t;
        memo = new String[s.length()][t.length()];
        return dfs(s.length()-1, t.length()-1);

    }

    public String dfs(int i, int j){
        if(i < 0) return t.substring(0, j+1); // s 是空串,返回剩余的 t
        if(j < 0) return s.substring(0, i+1); // t 是空串,返回剩余的 s
        if(memo[i][j] != null) return memo[i][j]; // 避免重复计算
        if(s.charAt(i) == t.charAt(j)){
            return memo[i][j] = dfs(i-1, j-1) + s.charAt(i); // 最短公共超序列一定包含 x
        }
        String ans1 = dfs(i-1, j);
        String ans2 = dfs(i, j-1);
        // 取 ans1 和 ans2 中更短的组成答案
        if(ans1.length() < ans2.length()){
            return memo[i][j] = ans1 + s.charAt(i);
        }
        return memo[i][j] = ans2 + t.charAt(j);
    }
}

记忆化搜索进一步优化

如果只求最短公共超序列的长度,那么递归返回的是一个整数而不是字符串,这样就只需要 O(nm) 的时间和空间,这是可以接受的。

class Solution {
    /**
        考虑从后往前构造答案,答案的最后一个字母是什么?要么是s的最后一个字母,要么是t的最后一个字母
        如果s[n]和t[n]的最后一个字母不同,
            如果答案最后一个字母是s的,问题变成构造s[n-1]和t[n]的答案
            如果答案最后一个字母是t的,问题变成构造s[n]和t[n-1]的答案
        如果s[n]和t[n]的最后一个字母相同
            问题变成构造s[n-1]和t[n-1]的答案
        边界条件:
            如果s是空串,答案为t
            如果t是空串,答案为s
     */
    private String s, t;
    private int[][] memo; // [i][j]表示s前i个字母,t前j个字母构造的最大答案
    
    public String shortestCommonSupersequence(String s, String t) {
        this.s = s;
        this.t = t;
        memo = new int[s.length()][t.length()];
        return makeAns(s.length()-1, t.length()-1);

    }

    // dfs(i,j) 返回 s 的前 i 个字母和 t 的前 j 个字母的最短公共超序列的长度
    private int dfs(int i, int j) {
        if (i < 0) return j + 1; // s 是空串,返回剩余的 t 的长度
        if (j < 0) return i + 1; // t 是空串,返回剩余的 s 的长度
        if (memo[i][j] > 0) return memo[i][j]; // 避免重复计算 dfs 的结果
        if (s.charAt(i) == t.charAt(j)) // 最短公共超序列一定包含 s[i]
            return memo[i][j] = dfs(i - 1, j - 1) + 1;
        return memo[i][j] = Math.min(dfs(i - 1, j), dfs(i, j - 1)) + 1;
    }

    // makeAns(i,j) 返回 s 的前 i 个字母和 t 的前 j 个字母的最短公共超序列
    // 看上去和 dfs 没啥区别,但是末尾的递归是 if-else
    // makeAns(i-1,j) 和 makeAns(i,j-1) 不会都调用
    // 所以 makeAns 的递归树仅仅是一条链
    private String makeAns(int i, int j) {
        if (i < 0) return t.substring(0, j + 1); // s 是空串,返回剩余的 t
        if (j < 0) return s.substring(0, i + 1); // t 是空串,返回剩余的 s
        if (s.charAt(i) == t.charAt(j)) // 最短公共超序列一定包含 s[i]
            return makeAns(i - 1, j - 1) + s.charAt(i);

        // 如果下面 if 成立,说明上面 dfs 中的 min 取的是 dfs(i - 1, j)
        // 说明 dfs(i - 1, j) 对应的公共超序列更短
        // 那么就在 makeAns(i - 1, j) 的结果后面加上 s[i]
        // 否则说明 dfs(i, j - 1) 对应的公共超序列更短
        // 那么就在 makeAns(i, j - 1) 的结果后面加上 t[j]
        if (dfs(i, j) == dfs(i - 1, j) + 1)
            return makeAns(i - 1, j) + s.charAt(i);
        return makeAns(i, j - 1) + t.charAt(j);
    }
}

记忆化搜索改成动态规划

class Solution {
    public String shortestCommonSupersequence(String str1, String str2) {
        // f[i+1][j+1] 表示 s 的前 i 个字母和 t 的前 j 个字母的最短公共超序列的长度
        char[] s = str1.toCharArray(), t = str2.toCharArray();
        int n = s.length, m = t.length;
        var f = new int[n + 1][m + 1];
        for (int j = 1; j < m; ++j) f[0][j] = j; // 递归边界
        for (int i = 1; i < n; ++i) f[i][0] = i; // 递归边界
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < m; ++j)
                if (s[i] == t[j]) // 最短公共超序列一定包含 s[i]
                    f[i + 1][j + 1] = f[i][j] + 1;
                else // 取更短的组成答案
                    f[i + 1][j + 1] = Math.min(f[i][j + 1], f[i + 1][j]) + 1;

        int na = f[n][m];
        var ans = new char[na];
        for (int i = n - 1, j = m - 1, k = na - 1; ; ) {
            if (i < 0) { // s 是空串,剩余的 t 就是最短公共超序列
                System.arraycopy(t, 0, ans, 0, j + 1);
                break; // 相当于递归边界
            }
            if (j < 0) { // t 是空串,剩余的 s 就是最短公共超序列
                System.arraycopy(s, 0, ans, 0, i + 1);
                break; // 相当于递归边界
            }
            if (s[i] == t[j]) { // 公共超序列一定包含 s[i]
                ans[k--] = s[i--]; // 倒着填 ans
                --j; // 相当于继续递归 makeAns(i - 1, j - 1)
            } else if (f[i + 1][j + 1] == f[i][j + 1] + 1)
                ans[k--] += s[i--]; // 相当于继续递归 makeAns(i - 1, j)
            else
                ans[k--] += t[j--]; // 相当于继续递归 makeAns(i, j - 1)
        }
        return new String(ans);
    }
}

你可能感兴趣的:(算法刷题记录,动态规划,算法,leetcode)