20.3:最长公共子序列

给定两个字符串str1和str2,
返回这两个字符串的最长公共子序列长度
比如 : str1 = "a12b3c456d",str2 = "1ef23ghi4j56k"
最长公共子序列是"123456",所以返回长度6

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

一:暴力方法

java 复制代码
/**
 * 给定两个字符串str1和str2,
 * 返回这两个字符串的最长公共子序列长度
 * 比如 : str1 = "a12b3c456d",str2 = "1ef23ghi4j56k"
 * 最长公共子序列是"123456",所以返回长度6
 */

/**
 * 样本对应模型 -- 分析样本的尾部
 * abc12hg34t
 * defkl123m
 */

public class LongestCommonSubsequence {
    /***
     * 暴力解法
     */
    public static int longestCommonSubsequence1(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);
    }

    // str1[0...i]和str2[0...j],这个范围上最长公共子序列长度是多少?
    // 可能性分类:
    // a) 最长公共子序列,一定不以str1[i]字符结尾、也一定不以str2[j]字符结尾
    // b) 最长公共子序列,可能以str1[i]字符结尾、但是一定不以str2[j]字符结尾
    // c) 最长公共子序列,一定不以str1[i]字符结尾、但是可能以str2[j]字符结尾
    // d) 最长公共子序列,必须以str1[i]字符结尾、也必须以str2[j]字符结尾
    // 注意:a)、b)、c)、d)并不是完全互斥的,他们可能会有重叠的情况
    // 但是可以肯定,答案不会超过这四种可能性的范围
    // 那么我们分别来看一下,这几种可能性怎么调用后续的递归。
    // a) 最长公共子序列,一定不以str1[i]字符结尾、也一定不以str2[j]字符结尾
    //    如果是这种情况,那么有没有str1[i]和str2[j]就根本不重要了,因为这两个字符一定没用啊
    //    所以砍掉这两个字符,最长公共子序列 = str1[0...i-1]与str2[0...j-1]的最长公共子序列长度(后续递归)
    // b) 最长公共子序列,可能以str1[i]字符结尾、但是一定不以str2[j]字符结尾
    //    如果是这种情况,那么我们可以确定str2[j]一定没有用,要砍掉;但是str1[i]可能有用,所以要保留
    //    所以,最长公共子序列 = str1[0...i]与str2[0...j-1]的最长公共子序列长度(后续递归)
    // c) 最长公共子序列,一定不以str1[i]字符结尾、但是可能以str2[j]字符结尾
    //    跟上面分析过程类似,最长公共子序列 = str1[0...i-1]与str2[0...j]的最长公共子序列长度(后续递归)
    // d) 最长公共子序列,必须以str1[i]字符结尾、也必须以str2[j]字符结尾
    //    同时可以看到,可能性d)存在的条件,一定是在str1[i] == str2[j]的情况下,才成立的
    //    所以,最长公共子序列总长度 = str1[0...i-1]与str2[0...j-1]的最长公共子序列长度(后续递归) + 1(共同的结尾)
    // 综上,四种情况已经穷尽了所有可能性。四种情况中取最大即可
    // 其中b)、c)一定参与最大值的比较,
    // 当str1[i] == str2[j]时,a)一定比d)小,所以d)参与
    // 当str1[i] != str2[j]时,d)压根不存在,所以a)参与
    // 但是再次注意了!
    // a)是:str1[0...i-1]与str2[0...j-1]的最长公共子序列长度
    // b)是:str1[0...i]与str2[0...j-1]的最长公共子序列长度
    // c)是:str1[0...i-1]与str2[0...j]的最长公共子序列长度
    // a)中str1的范围 < b)中str1的范围,a)中str2的范围 == b)中str2的范围
    // 所以a)不用求也知道,它比不过b)啊,因为有一个样本的范围比b)小啊!
    // a)中str1的范围 == c)中str1的范围,a)中str2的范围 < c)中str2的范围
    // 所以a)不用求也知道,它比不过c)啊,因为有一个样本的范围比c)小啊!
    // 至此,可以知道,a)就是个垃圾,有它没它,都不影响最大值的决策
    // 所以,当str1[i] == str2[j]时,b)、c)、d)中选出最大值
    // 当str1[i] != str2[j]时,b)、c)中选出最大值
    public static int process(char[] str1, char[] str2, int i, int j) {
        //样本对应模型分析样本的尾部


        if (i == 0 && j == 0) {
            return str1[i] == str2[j] ? 1 : 0;
        }

        if (i == 0) {
            if (str1[0] == str2[j]) {
                return 1;
            } else {
                int ans = process(str1, str2, i, j - 1);
                return ans;
            }
        }

        if (j == 0) {
            if (str1[i] == str2[j]) {
                return 1;
            } else {
                int res = process(str1, str2, i - 1, j);
                return res;
            }
        }

        //1:最长公共子序列可能以str1[i]结尾,但是一定不以str2[j-1]结尾。
        //所以str2[j-1]可以直接砍掉。str1[i]因为可能是公共子序列的结尾,所以要保留。
        int res1 = process(str1, str2, i, j - 1);

        //2:最长公共子序列可能以str[j]结尾,但是一定不以str1[i]结尾,所以str1[i]可以直接的砍掉。
        int res2 = process(str1, str2, i - 1, j);

        //3:最长公共子序列可能即以str[j]结尾,也以str1[i]结尾,所以str1[i]与str2[j]相等
        // int res3 = process(str1, str2, i, j); -- 这个写法是严重错误的,导致进入死递归。
        int res3 = str1[i] == str2[j] ? 1 + process(str1, str2, i - 1, j - 1) : 0;

        //这个res4有他没他都可以,因为最后我要求的是最大值,res4的范围比前三个都小,最后得出的结果也是最小的,所以单纯考虑最后
        //结果时,有4没4都可以,为了方便理解,保留。
        int res4 = process(str1, str2, i - 1, j - 1);

        return Math.max(Math.max(res1, res2), Math.max(res3, res4));
    }
}

二:dp迭代法

java 复制代码
	public static int longestCommonSubsequence2(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 C = str1.length;
        int L = str2.length;
        int[][] dp = new int[C][L];

        dp[0][0] = str1[0] == str2[0] ? 1 : 0;
        for (int j = 1; j < L; j++) {
            dp[0][j] = str1[0] == str2[j] ? 1 : dp[0][j - 1];
        }

        for (int i = 1; i < C; i++) {
            dp[i][0] = str1[i] == str2[0] ? 1 : dp[i - 1][0];
        }

        for (int c = 1; c < C; c++) {
            for (int l = 1; l < L; l++) {
                int p1 = dp[c][l - 1];
                int p2 = dp[c - 1][l];
                int p3 = str1[c] == str2[l] ? 1 + dp[c - 1][l - 1] : 0;
                dp[c][l] = Math.max(p1, Math.max(p2, p3));
            }
        }
        return dp[C - 1][L - 1];
    }

三:总结

这是一个样本对应模型,传入两个字符串样本,寻找最长的公共字符个数。

样本对应模型一般都是从尾部进行分析。

本题分析的标准是:假设公共子序列已知,判断最后一个字符是不是子序列的最后一个字符。

str1[0...i]和str2[0...j]

会出现四种情况:最后的结果一定会在这四种情况中的一个。

1:i 是 j 不是

​ 2:i 不是 j 是

​ 3:i 是 j 也是

​ 4:i 不是 j 也不是

通过分析上图,可以看到一直在从后向前进行缩小范围,只要是两个结尾不一样,就分两种情况进行范围的缩小,只要是两个结尾一样就加1,然后进行范围的缩小。会发现,只要是结尾两个结果是一样的就找到一个了,如果两个结果不一样,就分三路来跑,因为结果就在这三路中一个。判断的标准一直都在追踪最后的结尾是不是一样。

相关推荐
ModestCoder_5 分钟前
将一个新的机器人模型导入最新版isaacLab进行训练(以unitree H1_2为例)
android·java·机器人
a1800793108026 分钟前
软件工程面试题(二十二)
java·面试·软件工程
RainbowSea29 分钟前
4. RabbitMQ 发布确认的配置详细说明
java·消息队列·rabbitmq
robin_suli41 分钟前
Spring事务的传播机制
android·java·spring
青云交42 分钟前
Java 大视界 -- Java 大数据在智能电网电力市场交易数据分析与策略制定中的关键作用(162)
java·大数据·数据分析·交易策略·智能电网·java 大数据·电力市场交易
m0Java门徒1 小时前
Java 递归全解析:从原理到优化的实战指南
java·开发语言
云徒川1 小时前
【设计模式】原型模式
java·设计模式·原型模式
张张张3121 小时前
4.2学习总结 Java:list系列集合
java·学习
KATA~1 小时前
解决MyBatis-Plus枚举映射错误:No enum constant问题
java·数据库·mybatis
xyliiiiiL2 小时前
一文总结常见项目排查
java·服务器·数据库