【算法 - 动态规划】最长公共子序列问题

在上两篇文章中,我们将 暴力递归 逐步修改成为 动态规划 ,并介绍了有严格 dp表依赖 和无表依赖结构的解题方法。其中,前篇文章中的纸牌博弈问题属于 [L , R]上范围尝试模型。该模型给定一个范围,在该范围上进行尝试,套路就是 思考 [L ,R] 两端该如何取舍。

本篇文章我们学习一个新的模型: 样本对应模型,该模型的套路就是 以结尾位置为出发点,思考两个样本的结尾都会产生哪些可能性

力扣1143:最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

输入: text1 = “abcde”, text2 = “ace”

*输出:*3

*解释:*最长公共子序列是 “ace” ,它的长度为 3 。

示例 2:

*输入:*text1 = “abc”, text2 = “def”

*输出:*0

*解释:*两个字符串没有公共子序列,返回 0 。

首先我们依然采用最朴素的 暴力递归 来思考这道题目。

递归的准备

定义递归函数的功能: 返回 str1 中 [0...i] ,str2 中 [0...j] 范围上两个字符串的最长公共子序列。

思考递归需要的参数: str1, str2 两个字符串,分别要比较的范围 i, j。

明确递归的边界条件: 当两个字符串长度均为 1 时:若两个字符相同返回 1 ,不同返回 0 。

思路

这道题就是典型的 样本对应模型 ,因此,递归就可以按照 两个样本的结尾都会产生哪些可能性 的思路来划分情况:

  • 公共子序列 既不以 str1 结尾,也不以 str2 结尾;
  • 公共子序列 str1 结尾,不以 str2 结尾;
  • 公共子序列 不以 str1 结尾, str2 结尾;
  • 公共子序列 既以 str1 结尾,也以 str2 结尾。

因为要求 最长 子序列,因此要返回这四种情况当中的最大值。

代码

public static int longestCommonSubsequence(String s1, String s2) {
    if (s1 == null || s2 == null || s1.length() == 0 || s2.length() == 0) {
        return 0;
    }
    char[] str1 = s1.toCharArray();
    char[] str2 = s2.toCharArray();
    return process(str1, str2, str1.length - 1, str2.length - 1);
}

public static int process(char[] str1, char[] str2, int i, int j) {
    if (i == 0 && j == 0) {
        return str1[i] == str2[j] ? 1 : 0;
    } else if (i == 0) {
        return str1[i] == str2[j] ? 1 : process(str1, str2, i, j - 1);
    } else if (j == 0) {
        return str1[i] == str2[j] ? 1 : process(str1, str2, i - 1, j);
    } else {
        int p1 = process(str1, str2, i - 1, j - 1);
        int p2 = process(str1, str2, i, j - 1);
        int p3 = process(str1, str2, i - 1, j);
        int p4 = str1[i] == str2[j] ? 1 + process(str1, str2, i - 1, j - 1) : 0;
        return Math.max(Math.max(p1, p2), Math.max(p3, p4));
    }
}

代码解释

因为递归调用中使用到了 i-1 , j-1,因此需要做边界处理。例如:当str1 中只剩下一个字符时,若与 str2 最后一个字符相等即找到了此时最长子序列,否则排除掉 str2 的最后一个字符继续递归寻找。


写出该暴力版的递归之后发现,最终要求的是最大值,因此其实 p1 的情况是一定不会比 p2 ,p3 的情况返回值要大,所以可以减少一次递归的调用,做个小小的优化。

动态规划版

public static int longestCommonSubsequence(String s1, String s2) {
    if (s1 == null || s2 == null || s1.length() == 0 || s2.length() == 0) {
        return 0;
    }
    char[] str1 = s1.toCharArray();
    char[] str2 = s2.toCharArray();
    int N = str1.length;
    int M = str2.length;
    int[][] dp = new int[N][M];
    // 处理 str1 和 str2 均剩下一个字符的情况
    dp[0][0] = 1;
    // 处理 str1 剩下一个字符的情况 
    for (int j = 1; j < M; j++) {
        dp[0][j] = str1[0] == str2[j] ? 1 : dp[0][j - 1];
    }
    // 处理 str2 剩下一个字符的情况 
    for (int i = 1; i < N; i++) {
        dp[i][0] = str1[i] == str2[0] ? 1 : dp[i - 1][0];
    }
    // str1 str2 均有多个字符的情况
    for (int i = 1; i < N; i++) {
        for (int j = 1; j < M; j++) {
            int p1 = dp[i][j - 1];
            int p2 = dp[i - 1][j];
            int p3 = str1[i] == str2[j] ? 1 + dp[i - 1][j - 1] : 0;
            dp[i][j] = Math.max(Math.max(p1, p2), p3);
        }
    }
    return dp[N - 1][M - 1];
}

代码解释

根据递归函数修改出这套动态规划版代码就非常容易了。

以 str1 = “abcde”, str2 = “ace” 为例

可变的参数有两个:str1str2 判断长度范围的 ij。因此,需要设置一个二维的 dp 表数组,由于 i, j 的取值范围从 0 开始到字符串长度减一,因此数组大小设置为 dp[N][M]

递归代码 i==0 && j==0 可知,初始只有 dp[0][0] 的值为 1 。

i == 0j == 0时,只依赖左侧或上侧的数值。

当 str1 和 str2 均有多个字符时,会依赖 左,上,左上 三个地方的 最大值


由递归调用函数 process(str1, str2, str1.length - 1, str2.length - 1) 可知,最终要求的是 dp[N - 1][M - 1] 的值 3

总结

本篇文章我们学习了一个新的分析模型 —— 样本对应模型 。该模型的套路就是: 以结尾位置为出发点,思考两个样本的结尾都会产生哪些可能性

下篇文章我们继续学习类似的题型,但分析模型可以完全不一样哦,敬请期待!

~ 点赞 ~ 关注 ~ 不迷路 ~!!!

------------- 往期回顾 -------------
【算法 - 动态规划】力扣 691. 贴纸拼词
【算法 - 动态规划】原来写出动态规划如此简单!
【算法 - 动态规划】从零开始学动态规划!(总纲)
【堆 - 专题】“加强堆” 解决 TopK 问题!
AC 此题,链表无敌!!!

  • 32
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值