动态规划四——子序列系列

目录

题目一——300. 最长递增子序列 - 力扣(LeetCode)

题目二——376. 摆动序列 - 力扣(LeetCode)

题目三—— 673. 最长递增子序列的个数 - 力扣(LeetCode)

题目四——646. 最长数对链 - 力扣(LeetCode)

题目五——1218. 最长定差子序列 - 力扣(LeetCode)

题目六——873. 最长的斐波那契子序列的长度 - 力扣(LeetCode)

题目七——1027. 最长等差数列 - 力扣(LeetCode)

题目八——446. 等差数列划分 II - 子序列 - 力扣(LeetCode) 

题目九——P2285 [HNOI2004] 打鼹鼠 - 洛谷

题目十——P2196 [NOIP 1996 提高组] 挖地雷 - 洛谷


题目一——300. 最长递增子序列 - 力扣(LeetCode)

首先子序列不是子数组!!

子序列 是可以通过从另一个数组删除或不删除某些元素,但不更改其余元素的顺序得到的数组。

说白了就是按从左往右的顺序挑选元素,组成的新数组就是子序列。每个元素的相对顺序是保持不变的!!! 

我们下面看一个例子:

原数组:[1, 2, 3, 4, 5]

可能的子序列:

  • [1, 2, 3] (保留了前三个元素)
  • [1, 3, 5] (跳过了第二个和第四个元素)
  • [1, 2, 4, 5] (跳过了第三个元素)
  • [5] (只保留了最后一个元素)
  • [1, 2, 3, 4, 5] (没有删除任何元素)
  • [] (空数组也是任何数组的子序列,表示删除所有元素)

有没有发现,子序列就是包含了子数组。所以子序列的问题是比子数组的题目难的!!


1.状态表示

对于线性 dp ,我们可以⽤「经验+题⽬要求」来定义状态表⽰:

  • i. 以某个位置为结尾,巴拉巴拉;
  • ii. 以某个位置为起点,巴拉巴拉。

这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

使用动态规划(DP)数组 dp,其中 dp[i] 表示以 nums[i] 结尾的所有递增子序列中的最长长度。

2.状态转移方程

像这种子数组或者子序列的状态转移方程的推导,都是从如何构成子序列或者子数组来解决的!!

我们看dp[i] 表示以 nums[i] 结尾的所有递增子序列中的最长长度

那以nums[i]为结尾的子序列可以分为下面几类:

  1. 单独一个元素组成子序列:
  2. 和前面的元素一起组成子序列(比如说跟在nums[i-1]后面形成子序列,或者跟在nums[i-2]后面,再或者跟在nums[i-3]后面……跟在nums[0]都是可以的

所以我们很容易就能推导出下面这个状态转移方程

  1. 单独一个元素组成子序列:dp[i]=1;
  2. 和前面的元素一起组成子序列:这个时候我们不能随便乱填dp[i],我们可以回去看一下状态表示:一定要是最长子序列的长度,所以我们完全可以定义一个变量 j 从0开始往n-1进行遍历,看看哪个的dp[j]最大,哪个的最大就让nums[i]跟在它后面。所以就是最大的dp[j] + 1

由于我们要找最长的,所以最终的状态转移方程:

if (nums[i] > nums[j]) {

        dp[i] = max(dp[i],dp[j] + 1);

}

注意:我们回到这个题目,题目说找一个严格递增的子序列,所谓严格递增,就是子序列里面各个元素不能存在相等的情况,只能是nums[i]>nums[j]。 

3.初始化

初始化我们是将它初始化为最差的情况。

每个元素单独都能构成一个递增子序列,因此初始时,dp[i] 应该被设置为 1(对于所有 i)。

4.填表顺序

由于我们需要用到前面的状态来计算当前状态,因此填表顺序应该是从左往右(即从小到大的索引)。

5.返回值

最长递增子序列的长度就是 dp 数组中的最大值。


class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n,1);

        int ret=1;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j <= i-1; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = max(dp[i],dp[j] + 1);
                }
                ret=max(ret,dp[i]);
            }
        }
        return ret;
    }
};

题目二——376. 摆动序列 - 力扣(LeetCode)

 

 这个题目很像多状态dp啊

1. 状态表⽰:

对于线性 dp ,我们可以⽤「经验+题⽬要求」来定义状态表⽰:

  • i. 以某个位置为结尾,巴拉巴拉;
  • ii. 以某个位置为起点,巴拉巴拉。

这⾥我们选择⽐较常⽤的⽅式,以某个位置为结尾,结合题⽬要求,定义⼀个状态表⽰:

  • dp[i] 表⽰「以 i 位置为结尾的最⻓摆动序列的⻓度」。

但是,问题来了,如果状态表⽰这样定义的话,以 i 位置为结尾的最⻓摆动序列的⻓度我们没法 从之前的状态推导出来。因为我们不知道前⼀个最⻓摆动序列的结尾处是递增的,还是递减的。因 此,我们需要状态表⽰能表⽰多⼀点的信息:要能让我们知道这⼀个最⻓摆动序列的结尾是递增的 还是递减的。

解决的⽅式很简单:搞两个 dp 表就好了。

  1. f[i] 表⽰:以 i 位置元素为结尾的所有的⼦序列中,最后⼀个位置呈现「上升趋势」的最⻓摆 动序列的⻓度;
  2. g[i] 表⽰:以 i 位置元素为结尾的所有的⼦序列中,最后⼀个位置呈现「下降趋势」的最⻓摆 动序列的⻓度。

2.状态转移方程

像这种子数组或者子序列的状态转移方程的推导,都是从如何构成子序列或者子数组来解决的!!

那以nums[i]为结尾的子序列可以分为下面几类:

  1. 单独一个元素组成子序列:
  2. 和前面的元素一起组成子序列(比如说跟在nums[i-1]后面形成子序列,或者跟在nums[i-2]后面,再或者跟在nums[i-3]后面……跟在nums[0]都是可以的

由于子序列的构成比较特殊,对于 f[i](即以 i 位置为结尾的子序列),其前一个位置 j 可以是 [0, i - 1] 区间内的某一个位置。我们可以根据子序列的构成方式进行分类讨论:

  1. 单独一个元素组成子序列:此时,只能由单个元素自身构成,因此 [0, i - 1] 的任意 f[i] 初始化为 1。

  2. 和前面的元素一起组成子序列:因为结尾要呈现上升趋势,所以需要 nums[j] < nums[i]。在满足这个条件下,j 结尾的子序列需要呈现下降状态,因此最长的摆动序列就是 g[j] + 1。我们需要找出所有满足条件下的最大 g[j] + 1,并更新 f[i]

综上,对于 f[i] 的更新公式为:

f[i] = max(g[j] + 1, f[i]),其中 j 满足 0 <= j < i 且 nums[j] < nums[i]

同理,对于 g[i](即以 i 位置为结尾的子序列),我们也可以进行分类讨论:

  1. 单独一个元素组成子序列:此时,只能由单个元素自身构成,因此 g[i] 初始化为 1。

  2. 和前面的元素一起组成子序列:因为结尾要呈现下降趋势,所以需要 nums[j] > nums[i]。在满足这个条件下,j 结尾的子序列需要呈现上升状态,因此最长的摆动序列就是 f[j] + 1。我们需要找出所有满足条件下的最大 f[j] + 1,并更新 g[i]

综上,对于 g[i] 的更新公式为:

g[i] = max(f[j] + 1, g[i]),其中 j 满足 0 <= j < i 且 nums[j] > nums[i]

3.初始化

由于每个元素单独都能构成一个摆动序列(长度为 1),因此我们可以将 f 和 g 两个 DP 表内的所有元素初始化为 1。

4.填表顺序

毫无疑问,我们应该从左往右依次填充 DP 表,即按照数组 nums 的顺序进行。

5.返回值

最终,我们应该返回两个 DP 表里面的最大值,这代表了最长的上升摆动子序列和下降摆动子序列中的最长者。我们可以在填表的过程中,顺便更新一个全局的最大值变量来记录这个结果。


所以我们很容易就想到下面这个代码

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n=nums.size();
        vector<int>f(n,1);
        vector<int>g(n,1);

        int ret=1;
        for(int i=1;i<n;i++)
        {
            for(int j=0;j<=i-1;j++)
            {
                if(nums[j]<nums[i])
                {
                    f[i]=max(f[i],g[j]+1);
                }
     
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值