最长上升子序列/子串和最长公共子序列/子串

经典序列问题解析

子序列(不连续)

<span id = “最长上升子序列">最长上升子序列

LIS(longest increasing subsequence)是最经典的动态规划题目之一,因为它的递推公式很有条理,可以很清楚的告诉我们什么状态可以递推出当前的状态。

首先搞清楚几个概念:

上升:本题中的上升指的是严格的上升,即{1, 2, 3}而不是{1, 2, 2}

子序列和子串:子序列是数组中可以不连续的子段序列,而子串是数组中一个连续的一段序列。

(暴力回溯)

可以将所有的数组中的所有子集(所有组合)求出来,然后判断是否为严格上升的序列,最后取最长的序列的长度。

有时间复杂度太高,所以就不写了。

(动规)

1.dp数组的含义

要求出最长上升子序列,只有一个限制条件,即要满足是上升的序列,所以只需要一个一维数组来控制即可。

定义dp[i]表示:以第i个数字结尾的最长上升子序列的长度为dp[i]

注意:答案不一定是dp[nums.size() - 1],因为不一定是以最后一个数字结尾就是最长的上升子序列,例如nums = {1, 2, 3, 1, 1},所以最后遍历一遍以nums[i]结尾的所有数dp[i],取一个最大值。(也可以在算出dp[i]的时候就更新ans,见下面的代码)

2.递推公式

一般递推公式都是推出最后一个最具有一般性的dp数组,即dp[i]。所以要考虑什么状态可以推出dp[i],即dp[i]前一个满足要求的dp数组,也就是说哪一个dp数组满足nums[i]是它的最长上升子序列中的数。

可以发现想要推出nums[i]是否为最长上升子序列中的数,就要看nums[j] j = 1,2 ... i - 1是否小于nums[i]if (nums[i] > nums[j])这时dp[i] = dp[j] + 1,但是j = 1,2,...i - 1,所以dp[i]是其中最大的一个,所以dp[i] = max(dp[i], dp[j] + 1)

3.初始化

因为每一个子序列本身都是一个上升的子序列,所以dp数组中的所有数初始化为1。

根据递推公式中dp[i] = max(dp[i], dp[j] + 1),可知dp[i]dp[j]也就是dp[0 ... i - 1]推出,所以不用特判初始化。

4.遍历顺序

有递推公式可以知道,前面状态推出后面的状态,所以从前往后遍历即可。

5.举例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7T4GVkpf-1626530905996)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210717085009149.png)]

class Solution {
   
   
public:
    int lengthOfLIS(vector<int>& nums) {
   
   
        if (nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1);
        
        int ans = 0;
        for (int i = 1; i < nums.size(); i ++) {
   
   
            for (int j = 0; j < i; j ++) {
   
   
                if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
            }
            if (ans < dp[i]) ans = dp[i];
        }
        return ans;
    }
};
(动规 + 二分 + 贪心)

tail[i]表示:长度为i + 1的所上升子序列的结尾的最小值。所以tail数组的长度就是上升子序列的长度。

如何更新tail数组:

1.如果从前往后遍历数组,nums[i] > tail[end]的话,就将nums[i]插入到tail数组的最后。

2.如果从前往后遍历数组,nums[i] <= tail[end]的话,就将tail数组中>=nums[i]的第一个元素,替换成nums[i],这个过程可以用二分来解决。

为什么要用nums[i]替换tail数组中>= nums[i]的第一个元素?

数组是从前往后遍历的去求出上升子序列的最后一个最小元素的。

如果nums[i]>tail[end]的话就不说明了,因为要满足上升子序列的要求,所以只能将nums[i]插入到tail的后面。

但是如果nums[i]<=tail[end],就说明tail数组中一定存在一个数>=nums[i],而>=nums[i]的第一个数,可能是tail[end],也可能是tail数组中前面的数。这里就要用到贪心的思想了,就是不论如何,要想求出最长的上升子序列,就要使得if(nums[i] <= tail[end])中的nums[i]一定是tail数组中相应位置上最小的数,因为小数字后面接的大数字的个数一定要比小数字大的数后面接的大数字的个数要多

这样说太抽象了,举个栗子:

tail = {1, 3, 6}nums[i] = 5nums[i + 1] = 6

如果不替换nums[i]的话,因为nums[i] 不大于tail[end] == 6 nums[i + 1] 也不大于tail[end] == 6,所以tail的长度就是3.

但是因为nums[i] < tail[end],所有nums[i]替换>= nums[i]的第一个元素tail[2] == 6,所以tail = {1, 3, 5}, 因为nums[i + 1] > tail[end],所以nums[i + 1]tail数组后面,所以tail = {1, 3, 5, 6},这时tail数组的长度为4,所以最长上升子序列的长度为4。这显然比上面一种情况的长度要大。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPlFpLXJ-1626530905997)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210717104321423.png)]

class Solution {
   
   
public:
    int lengthOfLIS(vector<int>& nums) {
   
   
        if (nums.size(
### 最长公共子序列最长公共子串的概念及区别 #### 定义 - **最长公共子序列 (LCS)** 是指在两个给定的字符串中,找到一个共同的子序列,其字符顺序与原字符串中的相对位置相同,但不一定是连续的。例如,“blog”是从“cnblogs”“belong”提取出来的最长公共子序列[^1]。 - **最长公共子串** 则是一个更加严格的定义,指的是两个字符串中共有的、连续的一段字符组成的最大长度子串。例如,“lo”是从“cnblogs”“belong”提取出来的最长公共子串[^2]。 #### 区别 两者的根本差异在于是否要求字符在原始字符串中是连续的: - 子序列允许跳过某些字符; - 而子串则不允许有任何间断,必须是一整块连续的部分[^3]。 --- ### 动态规划算法实现 以下是基于Java语言分别针对最长公共子序列(LCS)以及最长公共子串的具体实现方法: #### Java 实现 - 最长公共子序列 (LCS) ```java public class LongestCommonSubsequence { public static int lcs(String X, String Y){ int m = X.length(); int n = Y.length(); // 创建二维数组存储中间状态 int[][] dp = new int[m+1][n+1]; for(int i=0;i<=m;i++){ for(int j=0;j<=n;j++) { if(i==0 || j==0){ dp[i][j]=0; } else if(X.charAt(i-1)==Y.charAt(i-1)){ dp[i][j] = dp[i-1][j-1]+1; }else{ dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]); } } } return dp[m][n]; } } ``` 这段代码通过构建`dp`表来记录不同阶段的最大匹配情况,并最终返回整个过程中的最优解即为最长公共子序列长度[^4]。 #### Java 实现 - 最长公共子串 ```java public class LongestCommonSubstring { public static String longestCommonSubstring(String s1, String s2) { int maxLen = 0; int endPos = 0; int[][] dp = new int[s1.length()+1][s2.length()+1]; for(int i=1;i<=s1.length();i++){ for(int j=1;j<=s2.length();j++){ if(s1.charAt(i-1)==s2.charAt(j-1)){ dp[i][j] = dp[i-1][j-1]+1; if(maxLen<dp[i][j]){ maxLen = dp[i][j]; endPos = i; } } else{ dp[i][j]=0; } } } return s1.substring(endPos-maxLen,endPos); } } ``` 在这个版本里同样采用了动态规划的思想,不过这里关注的是连续相同的片段,因此一旦遇到不同的字符就需要重新计数起点。 --- ### 总结 无论是处理最常公共子序列还是最长公共子串问题,都可以采用动态规划策略有效解决。然而由于两者对于"公共部分"的要求有所不同——前者强调逻辑上的次序关系即可满足条件;后者除了要遵循同样的排列外还额外附加了物理上邻接性的约束—所以它们各自对应的解决方案也会有所调整适应这些特定需求特点.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

hyzhang_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值