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,然后进行范围的缩小。会发现,只要是结尾两个结果是一样的就找到一个了,如果两个结果不一样,就分三路来跑,因为结果就在这三路中一个。判断的标准一直都在追踪最后的结尾是不是一样。

相关推荐
《源码好优多》19 分钟前
基于Java Springboot校园共享单车
java·开发语言·spring boot
HenCoder30 分钟前
【泛型 Plus】Kotlin 的加强版类型推断:@BuilderInference
android·java·开发语言·kotlin
八宝袋1 小时前
设计模式之工厂模式,但是宝可梦
java·设计模式·工厂模式
一支榴莲挞1 小时前
前后端分离练习(云客项目)
java·vue.js·spring boot·前端框架
oioihoii1 小时前
设计模式概述
java·c++·设计模式·面试·c#·大学必学
神仙别闹1 小时前
基于JAVA实现的(GUI)坦克大战游戏
java·开发语言·游戏
R-sz2 小时前
java如何利用流式计算筛选出同一天时间最新的一条数据
java·windows·python
程序员阿龙2 小时前
基于Spring Boot的在线性格测试系统设计与实现(源码+定制+开发)智能性格测试与用户个性分析平台、在线心理测评系统的开发、性格测试与个性数据管理系统
java·spring boot·后端·性格测评平台开发·用户性格测评系统·个性分析与推荐系统·心理测评与数据分析
robin_suli2 小时前
Java多线程八股(一), 锁策略,synchronized锁策略详解
java·开发语言·八股
手握风云-2 小时前
零基础Java第十八期:图书管理系统
java·开发语言