求字符串s1和s2的最长公共子序列的长度

求字符串s1和s2的最长公共子序列的长度

提示:互联网大厂的经典动态规划题

解决动态规划问题的重要知识!暴力递归的4种经典尝试模型
最关键的就是这个递归函数,解题的核心在这。
互联网大厂的动态规划题目中的四种经典暴力递归尝试模型:
(1)DP1:从左往右的尝试模型,关注i位置结尾,或者i位置开头的情况,或者看i联合i+1,i+2的情况,填表往往是上到下,或者下到上,左到右,右到左。
(2)DP2:从L–R范围上的尝试模型,关注L和R的情况,填表格式非常固定,主对角,副对角,倒回来填
(3)DP3:多样本位置对应的尝试模型,2个样本,一个样本做行,一个样本做列,关注i和j对应位置的情况,先填边界,再填中间
(4)DP4:业务限制类的尝试模型,比如走棋盘,固定的几个方向可以走,先填边界,再填中间。

本题是——DP3:多样本位置对应的尝试模型


题目

求字符串s1和s2的最长公共子序列的长度

所谓子序列,就是字符串中不一定连续位置的字符的组合
公共子序列:是2个字符串中公共有的子序列
最长公共子序列:就是2个字符串中最长的那个子序列,其长度就是咱们要求的结果。


一、审题

示例:
s1=ab1cd2ef345gh
s2=opq123rs4tx5yz
其最长公共子序列是:12345,长度ans=5;
在这里插入图片描述


暴力递归尝试DP3:多样本位置对应模型

敏感度:题目,给你2字符串,今后题目给你俩数组,俩个样本,对比进行的题目,显然就是多样本,让你用一个样本做行,一个样本做列,故你要敏感地想到,这就是考你动态规划题目的DP3:多样本位置对应模型

故暴力递归,就知道咋尝试了

显然咱可以这么做:
定义暴力递归函数**f(s1,i, s2,j)**为求:从s1的0–i范围与s2的0–j范围上的最长公共子序列的长度是?

主函数咋调用?f(s1,N-1, s2,M-1),N是s1长度,M是s2长度
在这里插入图片描述

多样本位置模型,关注的是i和j结尾的情况如何如何?
那咱们就来分析一下,究竟f(s1,i, s2,j)是如何处理的:
(1)最长公共子序列common:不以 s1的i位置结尾,不以 s2的j位置结尾,那相当于扩展i和j没意义
所以结果ans应该是f(s1,i - 1, s2,j - 1)
在这里插入图片描述
(2)最长公共子序列common:不以 s1的i位置结尾, s2的j位置结尾,那相当于扩展i没意义
所以结果ans应该是f(s1,i - 1, s2,j)
在这里插入图片描述
(3)最长公共子序列common: s1的i位置结尾,不以 s2的j位置结尾,那相当于扩展j没意义
所以结果ans应该是f(s1,i, s2,j - 1)
在这里插入图片描述
(4)最长公共子序列common: s1的i位置结尾, s2的j位置结尾,那必然是当**s1[i]=s2[j]**才行(这个条件千万别漏掉!!!)
所以结果ans应该是f(s1,i - 1, s2,j - 1) + 1

也就这么几种情况,以上四种情况取最大值。

(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
在这里插入图片描述

另外,看i=0,整行,如果s2的0–j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
在这里插入图片描述
另外,看j=0,整列,如果s1的0–i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
在这里插入图片描述

这很好理解吧?

okay,咱们逻辑已经捋清了,所以手撕暴力递归的代码岂不是非常非常非常非常简单了!!!

//定义暴力递归函数**f(s1,i, s2,j)**为求:**从s1的0--i范围与s2的0--j范围上的最长公共子序列的长度**是?
    public static int f(char[] str1, char[] str2, int i, int j){
        //(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
        if (i == 0 && j == 0) return str1[i] == str2[j] ? 1 : 0;
        //另外,看i=0,整行,如果s2的0--j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
        if (i == 0 && j > 0) return Math.max(f(str1, str2, i, j - 1), str1[i] == str2[j] ? 1 : 0);
        //另外,看j=0,整列,如果s1的0--i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
        if (j == 0 && i > 0) return Math.max(f(str1, str2, i - 1, j), str1[i] == str2[j] ? 1 : 0);
        //其余ij
        //(1)最长公共子序列common:**不以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展i和j没意义
        //所以结果ans应该是f(s1,i - 1, s2,j - 1)
        int p1 = f(str1, str2, i - 1, j - 1);
        //(2)最长公共子序列common:**不以** s1的i位置结尾,**以** s2的j位置结尾,那相当于扩展i没意义
        //所以结果ans应该是f(s1,i - 1, s2,j)
        int p2 = f(str1, str2, i - 1, j);
        //(3)最长公共子序列common:**以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展j没意义
        //所以结果ans应该是f(s1,i, s2,j - 1)
        int p3 = f(str1, str2, i, j - 1);
        //(4)最长公共子序列common:**以** s1的i位置结尾,**以** s2的j位置结尾,那必然是**s1[i]=s2[j]**才行
        //所以结果ans应该是f(s1,i - 1, s2,j - 1) **+ 1**
        int p4 = -1;
        if (str1[i] == str2[j]) p4 = f(str1, str2, i - 1, j - 1) + 1;
        //也就这么几种情况,以上四种情况取最大值。
        return Math.max(Math.max(p1, p2), Math.max(p3, p4));
    }
    //主函数调用:
    public static int maxLenOflongestCommonSubString(String s1, String s2){
        if (s1.compareTo("") == 0 || s1.length() == 0 ||
                s2.compareTo("") == 0 || s2.length() == 0) return 0;

        char[] str1 = s1.toCharArray();
        char[] str2 = s2.toCharArray();

        return f(str1, str2, str1.length - 1, str2.length - 1);
    }

    public static void test(){
        String s1="ab1cd2ef345gh";
        String s2="opq123rs4tx5yz";
//        String s1="abcde";
//        String s2="abcdf";

        System.out.println(maxLenOflongestCommonSubString(s1, s2));
    }

    public static void main(String[] args) {
        test();
    }

测试时速度很慢的5


修改记忆化搜索法:傻缓存dp表跟随暴力递归代码

记忆化搜索法,无非就是用一个dp表,把可能的i和j的各种状态组合的最佳结果,存到表格dp[i][j]中,
如果递归过程中,发现dp[i][j]已经求过了,就不要暴力递归继续去求了,直接返回dp[i][j]省时间

咱就来准备一个dp
由于暴力递归就2个变量i和j自然就是填写一个二维的表
i取0–N-1
j取0–M-1
故dp是N×M维的矩阵
在这里插入图片描述
dp[i][j]的含义就是暴力递归的含义:从s1的0–i范围与s2的0–j范围上的最长公共子序列的长度是?

怎么填,完全根据暴力递归的代码来写

//改为傻缓存dp表:
    // 定义暴力递归函数**f(s1,i, s2,j)**为求:
    // **从s1的0--i范围与s2的0--j范围上的最长公共子序列的长度**是?
    public static int fDP(char[] str1, char[] str2, int i, int j, int[][] dp){
        if (dp[i][j] != -1) return dp[i][j];
        //(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
        if (i == 0 && j == 0) {
            dp[i][j] = str1[i] == str2[j] ? 1 : 0;
            return dp[i][j];
        }
        //另外,看i=0,整行,如果s2的0--j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
        if (i == 0 && j > 0) {
            dp[i][j] = Math.max(fDP(str1, str2, i, j - 1, dp), str1[i] == str2[j] ? 1 : 0);
            return dp[i][j];
        }
        //另外,看j=0,整列,如果s1的0--i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
        if (j == 0 && i > 0){
            dp[i][j] = Math.max(fDP(str1, str2, i - 1, j, dp), str1[i] == str2[j] ? 1 : 0);
            return dp[i][j];
        }
        //其余ij
        //(1)最长公共子序列common:**不以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展i和j没意义
        //所以结果ans应该是f(s1,i - 1, s2,j - 1)
        int p1 = fDP(str1, str2, i - 1, j - 1, dp);
        //(2)最长公共子序列common:**不以** s1的i位置结尾,**以** s2的j位置结尾,那相当于扩展i没意义
        //所以结果ans应该是f(s1,i - 1, s2,j)
        int p2 = fDP(str1, str2, i - 1, j, dp);
        //(3)最长公共子序列common:**以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展j没意义
        //所以结果ans应该是f(s1,i, s2,j - 1)
        int p3 = fDP(str1, str2, i, j - 1, dp);
        //(4)最长公共子序列common:**以** s1的i位置结尾,**以** s2的j位置结尾,那必然是**s1[i]=s2[j]**才行
        //所以结果ans应该是f(s1,i - 1, s2,j - 1) **+ 1**
        int p4 = -1;
        if (str1[i] == str2[j]) p4 = fDP(str1, str2, i - 1, j - 1, dp) + 1;
        //也就这么几种情况,以上四种情况取最大值。
        dp[i][j] = Math.max(Math.max(p1, p2), Math.max(p3, p4));
        return dp[i][j];
    }
    //主函数调用:
    public static int maxLenOflongestCommonSubStringDP(String s1, String s2){
        if (s1.compareTo("") == 0 || s1.length() == 0 ||
                s2.compareTo("") == 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];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                dp[i][j] = -1;//初始化
            }
        }

        return fDP(str1, str2, str1.length - 1, str2.length - 1, dp);
    }

    public static void test(){
        String s1="ab1cd2ef345gh";
        String s2="opq123rs4tx5yz";
//        String s1="abcde";
//        String s2="abcdf";

//        System.out.println(maxLenOflongestCommonSubString(s1, s2));
        System.out.println(maxLenOflongestCommonSubStringDP(s1, s2));
    }

    public static void main(String[] args) {
        test();
    }

秒杀,瞬间出结果:5


暴力递归到精细化动态规划填表dp:转移方程

那么既然咱有了暴力递归代码

很容易就能直接改为精细化填写dp表的动态规划方法:
咱就来准备一个dp
由于暴力递归就2个变量i和j自然就是填写一个二维的表
i取0–N-1
j取0–M-1
故dp是N×M维的矩阵
在这里插入图片描述
dp[i][j]的含义就是暴力递归的含义:从s1的0–i范围与s2的0–j范围上的最长公共子序列的长度是?
咱们咋求每一个格子呢?

自然是根据暴力递归的代码来求

//(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
        if (i == 0 && j == 0) return str1[i] == str2[j] ? 1 : 0;
        //另外,看i=0,整行,如果s2的0--j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
        if (i == 0 && j > 0) return Math.max(f(str1, str2, i, j - 1), str1[i] == str2[j] ? 1 : 0);
        //另外,看j=0,整列,如果s1的0--i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
        if (j == 0 && i > 0) return Math.max(f(str1, str2, i - 1, j), str1[i] == str2[j] ? 1 : 0);

这三行代码,足够填dp表中0 0 格子,0行0列

根据其余ij的代码:

```java
//(1)最长公共子序列common:**不以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展i和j没意义
        //所以结果ans应该是f(s1,i - 1, s2,j - 1)
        int p1 = f(str1, str2, i - 1, j - 1);
        //(2)最长公共子序列common:**不以** s1的i位置结尾,**以** s2的j位置结尾,那相当于扩展i没意义
        //所以结果ans应该是f(s1,i - 1, s2,j)
        int p2 = f(str1, str2, i - 1, j);
        //(3)最长公共子序列common:**以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展j没意义
        //所以结果ans应该是f(s1,i, s2,j - 1)
        int p3 = f(str1, str2, i, j - 1);
        //(4)最长公共子序列common:**以** s1的i位置结尾,**以** s2的j位置结尾,那必然是**s1[i]=s2[j]**才行
        //所以结果ans应该是f(s1,i - 1, s2,j - 1) **+ 1**
        int p4 = -1;
        if (str1[i] == str2[j]) p4 = f(str1, str2, i - 1, j - 1) + 1;
        //也就这么几种情况,以上四种情况取最大值。
        return Math.max(Math.max(p1, p2), Math.max(p3, p4));

可知,其余dp[i][j](表格中粉色五角星那),其实根据暴力递归中的(1)(2)(3)(4)四种情况的最大值,发现,dp[i][j]依赖:
i - 1, j - 1格子【ij的左上角】
i - 1, j格子【ij的上方格子】
i, j - 1格子【ij的左方格子】

最后咱们要的是红色五角星那个格子为结果:ans = dp[N - 1][M - 1]

因此,我们填写dp的代码就很容易手撕了:

//精细化DP
    //主函数:
    public static int maxLenOflongestCommonSubStringDP2(String s1, String s2){
        if (s1.compareTo("") == 0 || s1.length() == 0 ||
                s2.compareTo("") == 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];

        //(0)最基本的情况就是当i=0,切j=0时的状况,看看俩相等与否
        dp[0][0] = str1[0] == str2[0] ? 1 : 0;
        //另外,看i=0,整行,如果s2的0--j-1范围中有一个s1的i字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
        for (int j = 1; j < M; j++) {//填写0行
            dp[0][j] = Math.max(dp[0][j - 1], str1[0] == str2[j] ? 1 : 0);
        }
        //另外,看j=0,整列,如果s1的0--i-1中有一个s2的j字符,那就是1,否则看i字符和j字符是否相等了?相等为1,不等就0
        for (int i = 1; i < N; i++) {//填写0列
            dp[i][0] = Math.max(dp[i - 1][0], str1[i] == str2[0] ? 1 : 0);
        }
        //其余ij
        for (int i = 1; i < N; i++) {
            for (int j = 1; j < M; j++) {
                //(1)最长公共子序列common:**不以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展i和j没意义
                //所以结果ans应该是f(s1,i - 1, s2,j - 1)
                int p1 = dp[i - 1][j - 1];
                //(2)最长公共子序列common:**不以** s1的i位置结尾,**以** s2的j位置结尾,那相当于扩展i没意义
                //所以结果ans应该是f(s1,i - 1, s2,j)
                int p2 = dp[i - 1][j];
                //(3)最长公共子序列common:**以** s1的i位置结尾,**不以** s2的j位置结尾,那相当于扩展j没意义
                //所以结果ans应该是f(s1,i, s2,j - 1)
                int p3 = dp[i][j - 1];
                //(4)最长公共子序列common:**以** s1的i位置结尾,**以** s2的j位置结尾,那必然是**s1[i]=s2[j]**才行
                //所以结果ans应该是f(s1,i - 1, s2,j - 1) **+ 1**
                int p4 = -1;
                if (str1[i] == str2[j]) p4 = p1 + 1;
                //也就这么几种情况,以上四种情况取最大值。
                dp[i][j] = Math.max(Math.max(p1, p2), Math.max(p3, p4));
            }
        }

        return dp[N - 1][M - 1];
    }

    public static void test(){
        String s1="ab1cd2ef345gh";
        String s2="opq123rs4tx5yz";
//        String s1="abcde";
//        String s2="abcdf";

//        System.out.println(maxLenOflongestCommonSubString(s1, s2));
//        System.out.println(maxLenOflongestCommonSubStringDP(s1, s2));
        System.out.println(maxLenOflongestCommonSubStringDP2(s1, s2));
    }

    public static void main(String[] args) {
        test();
    }

是不是很简单,这些转移方程都不是凭空想想出来的,完全是根据暴力递归代码改写的!
结果5


总结

提示:重要经验:

1)动态规划的关键在暴力递归的尝试,有4个模型
2)暴力递归在笔试要改为记忆化搜索方案(傻缓存dp表跟随),面试要改为精细化动态规划填写dp表,把转移方程搞出来
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值