算法27:最长公共子序列(力扣1143题)——样本模型(4)

目录

简介

题目:

思路:

递归版本:

根据递归 分析推导

动态规划版本:


简介

前面刷了几道题目,都是从暴力递归到递归+动态规划的版本,最后演变成纯动态规划的版本。接下来的题目,将会跳过 递归 + 动态规划的版本。而是转头直接分享递归版本和纯动态规划版本。其实,动态规划是有技巧的,只要递归版本写的好,直接改成动态规划是手到擒来的一件事情,非常的简单。而递归,也是分模型的。即从左到右模型,范围模型,样本模型,业务模型。以后刷题的时候,可以直接套用这些模型,思路会清晰很多。

今天,我们来分析 样本模型。而样本模型的套路,就是直接套路样本数据的最后一个元素的无限可能,即存在的各种各样的可能性。以样本数据的最后一个元素即基础,进行分析。

题目:

https://leetcode.com/problems/longest-common-subsequence/

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

思路:

1. 如果样本数据都不存在,即无效的样本。

2. 如果一个样本长度为1,另一个样本很长,则长度最多为1,也有可能为0;

3. 如果2个样本数据都很长,那就逐层讨论。每次递归,都以当前样本的最后一个元素,讨论样本存在的可能性。最后把所有的样本数据拿出来比较,找到符合条件的数据即可,

递归版本:

package code03.动态规划_07.lesson3;

/**
 * 链接:https://leetcode.com/problems/longest-common-subsequence/
 *
 * 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
 *
 * 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
 *
 * 给你一些样本数据,要求找出共性的,就属于样本模型。 而样本模型的核心,就是以最后一个字符为基础进行讨论
 */
public class LongestCommonSubsequence_04 {

    public static int longestCommonSubsequence(String text1, String text2)
    {
        //对应解题思路1. 如果样本数据都不存在,即无效的样本。
        if (text1 == null || text2 == null || text1.isEmpty() || text2.isEmpty()) {
            return 0;
        }

        char[] s1 = text1.toCharArray();
        char[] s2 = text2.toCharArray();

        //此处传递的是数组下标, 给的都是最后一个下标
        return process(s1, s2, text1.length()-1, text2.length()-1);
    }

    //样本模型,就是要考虑样本数据的最后一个数据为基础进行讨论。最后一个数据,而不是最后一个
    //下标,因为递归的时候,最后一个数据会一直进行变化的
    public static int process(char[] s1, char[] s2, int index1, int index2)
    {
        //如果下标都为0,说明数组都来到末尾处。
        if (index1 == 0 && index2 == 0) {
            return s1[index1] == s2[index2] ? 1 : 0;
        }
        else if (index1 == 0) { //如果数组1以当前下标结尾,而数组2不确定 对应解题思路2. 如果一个样本长度为1,另一个样本很长,则长度最多为1,也有可能为0;

            //如果当前下标的字符数组相等,此时text1已经结束了,并且找到了和text2相等的字符,直接返回1.
            //如果没找到,继续找text2的字符
            return s1[index1] == s2[index2] ? 1 : process(s1, s2, index1, index2-1);
        }
        else if (index2 == 0) { //数组2来到结尾,数组1不一定。 对应解题思路2. 如果一个样本长度为1,另一个样本很长,则长度最多为1,也有可能为0;

            //原理同上。此时继续往下找的是字符数组text1
            return s1[index1] == s2[index2] ? 1 :process(s1, s2, index1-1, index2);
        }
        else {//此种case为剩余字符数组都没有结束,大部分case的逻辑,需要讨论样本模型的case。

            /**
             * 既然是最长公共子序列, 那就有可能是:
             * 1. 第一个字符数组的末尾结尾,
             * 2. 也节能是第二个末尾结尾
             * 3. 最后就是可能同时以2个数组的末尾结尾。
             *
             * 对应解题思路3 如果2个样本数据都很长,那就逐层讨论。每次递归,都
             * 以当前样本的最后一个元素,讨论样本存在的可能性。最后把所有的
             * 样本数据拿出来比较,找到符合条件的数据即可,
             * */

            //以第一个数组结尾结束,第二个不确定。 就是上方的index1==0的过程
            int p1 =  process(s1, s2, index1, index2-1);

            //以第二个数组结尾结束,第一个不确定。 index2==0的过程
            int p2 =  process(s1, s2, index1-1, index2);

            //同时以数组1的当前下标index1  和 数组2的当前下标index2结尾。
            //数组1为 ab123c45d
            //数组2位 123abc45d
            //最长公共子序为 12345
            //等价与上方的case index1 == 0 && index2 == 0
            int p3 = s1[index1] == s2[index2] ? (process(s1, s2, index1-1, index2-1) + 1) : 0;

            return Math.max(p1, Math.max(p2, p3));
        }
    }

    public static void main(String[] args) {
        String s1 = "ace";
        String s2 = "abcde";

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

递归好不好,是动态规划的关键。如果递归写不好,那么动态规划也就会非常的复杂,甚至完全没有版本写出来。因此,尝试写好一个递归,才是动态规划的关键所在。

根据递归 分析推导

1. 假设样本1数据为 ace 样本2数据为abcde得到的结果应该是3

2. 以样本1作行,样本2作列绘制二维数组。可得到以下表 (样本1或2,即可作行,也可左列)

a(0)b(1)c(2)d(3)e(4)
a(0)
c(1)
e(2)

3. 根据递归内部

if (index1 == 0 && index2 == 0) {
    return s1[index1] == s2[index2] ? 1 : 0;
}

可知数组dp[0][0] 为1或者0.  而样本开头都是a,因此此位置为 1

a(0)b(1)c(2)d(3)e(4)
a(0)1
c(1)
e(2)

4. 根据递归内部

else if (index1 == 0) { //如果数组1以当前下标结尾,而数组2不确定 对应解题思路2. 如果一个样本长度为1,另一个样本很长,则长度最多为1,也有可能为0;

    //如果当前下标的字符数组相等,此时text1已经结束了并且找到了和text2相等的字符,直接返回1.
    //如果没找到,继续找text2的字符
    return s1[index1] == s2[index2] ? 1 : process(s1, s2, index1, index2-1);

由于index1对应的是样本1的数据,即行数据。而index2对应的是样本2数据,即列数据。此处列的下标对应的为0,也就是说此处可以推导出行下标为0对应的所有的列。如果相等则为1, 不相等则继续递归,依次类推。

0行  0 列已经推导出为 1,即 dp[0][0] = 1;

a != e 依赖index2 - 1

a != d 依赖 index2 - 1

a != c 依赖 index2 - 1

a != b 依赖 index2 - 1

a == a 得到1,所以第一行都为1

a(0)b(1)c(2)d(3)e(4)
a(0)11111
c(1)
e(2)

5. 根据递归内部:

else if (index2 == 0) { //数组2来到结尾,数组1不一定。 对应解题思路2. 如果一个样本长度为1,另一个样本很长,则长度最多为1,也有可能为0;

    //原理同上。此时继续往下找的是字符数组text1
    return s1[index1] == s2[index2] ? 1 :process(s1, s2, index1-1, index2);
}

推导方法一样:

a != c 依赖上一行 index1 - 1

a != b  依赖上一行 index1 - 1

a == a 之前推导过的是1, 因此,此列都为1

a(0)b(1)c(2)d(3)e(4)
a(0)11111
c(1)1
e(2)1

6. 最后推导

//以第一个数组结尾结束,第二个不确定。 就是上方的index1==0的过程
int p1 =  process(s1, s2, index1, index2-1);

//以第二个数组结尾结束,第一个不确定。 index2==0的过程
int p2 =  process(s1, s2, index1-1, index2);

//同时以数组1的当前下标index1  和 数组2的当前下标index2结尾。
//数组1为 ab123c45d
//数组2位 123abc45d
//最长公共子序为 12345
//等价与上方的case index1 == 0 && index2 == 0
int p3 = s1[index1] == s2[index2] ? (process(s1, s2, index1-1, index2-1) + 1) : 0;

return Math.max(p1, Math.max(p2, p3));
a(0)b(1)c(2)d(3)e(4)
a(0)11111
c(1)112
e(2)1

a(0)b(1)c(2)d(3)e(4)
a(0)11111
c(1)11222
e(2)1122

                最后一步: p1 依赖上一列  p2 依赖前一列,p3是判断是否相等。如果相等,则是获取

                上一行和上一列的max值,并且加 1。 即得到 2 + 1 = 3;

a(0)b(1)c(2)d(3)e(4)
a(0)11111
c(1)11222
e(2)11223

由于递归传入的参数是: process(s1, s2, text1.length()-1, text2.length()-1); 所以最终得到的数据是dp[2][4], 即3.  由此,我们设计动态规划的代码:

动态规划版本:

package code03.动态规划_07;

/**
 * 链接:https://leetcode.com/problems/longest-common-subsequence/
 *
 * 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
 *
 * 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
 *
 * 给你一些样本数据,要求找出共性的,就属于样本模型。 而样本模型的核心,就是以最后一个字符为基础进行讨论
 */
public class LongestCommonSubsequence_04_opt {

    public static int longestCommonSubsequence(String text1, String text2)
    {
        if (text1 == null || text2 == null || text1.isEmpty() || text2.isEmpty()) {
            return 0;
        }

        char[] s1 = text1.toCharArray();
        char[] s2 = text2.toCharArray();

        //以s1做行,s2做列
        int[][] dp = new int[ s1.length][s2.length];

        int index1 = s1.length - 1;
        int index2 = s2.length - 1;

        //根据递归   if (index1 == 0 && index2 == 0)  而来
        dp[0][0] = s1[0] == s2[0] ? 1 : 0;

        //根据递归  else if (index1 == 0)  而来, 此处代表先处理 第一行的所有列
        for (int i = 1; i <= index2; i++) {
            dp[0][i] = s1[0] == s2[i] ? 1 : dp[0][i-1];
        }

        //根据递归  else if (index2 == 0)  而来, 此处代表先处理 第一列的所有行
        for (int j = 1; j <= index1; j++) {
            dp[j][0] = s1[j] == s2[0] ? 1 : dp[j-1][0];
        }

        //通用case  根据递归中最后一个else而来
        for (int row = 1; row <= index1; row++) {
            for (int col = 1; col <= index2; col++) {

                //根据 int p1 =  process(s1, s2, index1, index2-1) 改写
                int p1 = dp[row][col - 1];

                //根据 int p2 =  process(s1, s2, index1-1, index2) 改写
                int p2 = dp[row -1][col];

                //int p3 = s1[index1] == s2[index2] ? (process(s1, s2, index1-1, index2-1) + 1) : 0;
                int p3 = s1[row] == s2[col] ? (dp[row -1][col -1] + 1) : 0;

                dp[row][col] = Math.max(p1, Math.max(p2, p3));
            }
        }

       //返回值对应递归中的下标
       return dp[index1][index2];
    }

    public static void main(String[] args) {
        String s1 = "ace";
        String s2 = "abcde";

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

试想一下,如果没有递归做辅助,这些动态规划的代码该如何写。甚至写好了,我们完全搞不清楚这些代码的意思。

为啥

dp[0][0] = s1[index1] == s2[index1] ? 1 : 0;

为啥要求以下三种情况的最大值

//根据 int p1 =  process(s1, s2, index1, index2-1) 改写
int p1 = dp[row][col - 1];

//根据 int p2 =  process(s1, s2, index1-1, index2) 改写
int p2 = dp[row -1][col];

//int p3 = s1[index1] == s2[index2] ? (process(s1, s2, index1-1, index2-1) + 1) : 0;
int p3 = s1[row] == s2[col] ? (dp[row -1][col -1] + 1) : 0;
dp[row][col] = Math.max(p1, Math.max(p2, p3));

为啥最后要返回的值 下标 是下列这样的

return dp[index1 - 1][index2 - 1];

没有递归做辅助,这一些的为啥可能根本就解释不清楚。更别谈写出动态规划的代码了。

由于这是一道力扣原题,结果是动态规划版本可以顺利通过,而递归版本一直是超时,这也能够说明不同的业务场景,不用的写法,是非常大重要的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值