动态规划之子序列

首先说明一下子序列和子数组的概念。在数组中,子数组是由连续的元素组成的,而子序列则不一定是连续的。 在字符串中,子串是由连续的字符组成的,而子序列则不一定是连续的。

1. 最长递增子序列

1.题目链接:子序列问题
2.题目描述:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

1 <= nums.length <= 2500
-10^4 <= nums[i] <= 10^4

3.问题分析:这道题是要求递增子序列的最长长度,那么如果是求递增子数组的最长长度,又该如何求解?两者之间又有什么区别?
首先看求递增子数组的最长长度这道问题,然后以 i 位置为结尾进行分析,如果nums的 i 位置的值比nums的 i - 1位置的值大,那么dp表中该位置(i位置)的数值等于前一个位置的数值加1;由上述可知,求递增子数组的最长长度关键是寻找 i 位置前一个位置的数。
那么子序列呢?因为子序列是不一定连续的,以 i 位置进行分析,我们需要找到的是 i 位置前面所有元素中比nums[i]小的数值,然后在这些位置对应的dp表中寻找最大的数值,即最长长度。

  1. 状态表示:dp[i] 表⽰:以 i 位置元素为结尾的所有⼦序列中,最⻓递增⼦序列的⻓度。
  2. 状态转移方程:有上述问题分析知,求dp[i]位置的值,需要遍历 i 位置前面的所有比nums[i]小的值,找到对应的dp表中寻找最大的数值,所以状态转移方程为:dp[i] = max(dp[i], dp[i前面的元素] + 1)
  3. 初始化:求得值子序列的长度,递增子序列的最小长度为1,所以全部初始化为1即可。
  4. 填表顺序:从左往右。
  5. 返回值:返回dp表中最大的数值。

4.代码如下:

class Solution 
{
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        int n = nums.size();
        //创建dp表,并全初始化为1
        vector<int> dp(n, 1);
        int ret = 1;
        for (int i = 1; i < n; ++i)
        {
        	//寻找i位置前面的所有值
            for (int j = 0; j < i; ++j)
            {
                //将递增的最大值保留下来
                if (nums[i] > nums[j])
                    dp[i] = max(dp[i], dp[j] + 1);
            }
            //找出dp表中的最大值
            ret = max(ret, dp[i]);
        }
        return ret;
    }
};

2. 摆动序列

1.题目链接:摆动序列
2.题目描述:
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

示例 1:

输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3)。

示例 2:

输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。

示例 3:

输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2

提示:

1 <= nums.length <= 1000
0 <= nums[i] <= 1000

3.题目分析:这道题可以说是摆动数组(即最长湍流子数组)的升级,如果是摆动数组,那么这道题该如何解决?因为两元素之间的差有两种结果(0不考虑),所以需要两个dp表来将所有结果管理起来;通常一个表f[i]用来表示相减的差为正数的最长子数组的个数,另一个表g[i]用来表示相减的差为负数的最长子数组的个数。如果 i 位置的元素⽐ i - 1 位置的元素⼤,说明接下来应该去找 i -1 位置结尾,并且 i - 1 位置元素⽐前⼀个元素⼩的序列,那就是 g[i - 1] 。更新 f[i] 位置的值: f[i] = g[i - 1] + 1 ; 2.arr[i] < arr[i - 1] :如果 i 位置的元素⽐ i - 1 位置的元素⼩,说明接下来应该去找 i - 1 位置结尾,并且 i - 1 位置元素⽐前⼀个元素⼤的序列,那就是f[i - 1] 。更新 g[i] 位置的值: g[i] = f[i - 1] + 1 ; arr[i] == arr[i - 1] :不构成湍流数组。
那么这道题该如何解决?因为子序列不一定相连,所以需要将 i 位置前面的元素都找一遍,每次保留最大的长度即可。

  1. 状态表示:f[i] 表⽰:以 i 位置元素为结尾的所有的⼦序列中,最后⼀个位置相减为正数的最⻓摆动序列的⻓度;g[i] 表⽰:以 i 位置元素为结尾的所有的⼦序列中,最后⼀个位置相减为负数的最⻓摆动序列的⻓度
  2. 状态转移方程:1.如果子序列长度为1,那么f[i]、g[i]就只能为1;2.如果子序列长度大于1,以i位置进行分析,如果nums[i] > nums[i - 1],即最后⼀个位置相减为正数,此时应该入f表,并且应该寻找i - 1位置为值相减为负数的表,也就是g表,所以f[i] = g[i - 1] + 1,那么当nums[i] < nums[i - 1]时,g[i] = f[i - 1] + 1;因为要求子序列的最长长度,所以将i位置前面的元素都遍历一遍即可。
  3. 初始化:所有的元素单独都能构成⼀个摆动序列,因此可以将f、g表内所有元素初始化为1 。
  4. 填表顺序:从左往右。
  5. 返回值:返回f表、g表中的最大值。

4.代码如下:

class Solution
{
public:
    int wiggleMaxLength(vector<int>& nums)
    {
        int n = nums.size();
        vector<int> f(n, 1), g(n, 1); // 1. 创建 dp 表 + 初始化
        int ret = 1; // 更新最终结果
        for (int i = 1; i < n; i++) // 2. 填写 f[i] 以及 g[i]
        {
            for (int j = i - 1; j >= 0; j--)
            {
                if (nums[j] < nums[i]) //j相当于i - 1,i - 2等
                    f[i] = max(g[j] + 1, f[i]);
                else if (nums[j] > nums[i])
                    g[i] = max(f[j] + 1, g[i]);
            }
            ret = max(ret, max(f[i], g[i]));
        }
        // 3. 返回结果
        return ret;
    }
};

3. 最长递增子序列的个数

1.题目链接:最长递增子序列的个数
2.题目描述:
给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。

示例 1:

输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。

示例 2:

输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

提示:

1 <= nums.length <= 2000
-10^6 <= nums[i] <= 10^6

3.问题分析:这道题就有些意思,由题知有两点是变化的,一个是递增数列的长度,另一个是递增数列的个数,将上述这两个信息能管理起来,那么就可以求出最长递增数列的个数。用一个f表表示以 i 为结尾的最⻓递增⼦序列的⻓度;g表表示:以 i 为结尾的最⻓递增⼦序列的个数;用len表示返回递增子序列的长度,用count表示返回递增子序列的个数。
以 i 位置进行分析:如果nums[i] > nums[j]( j 表示的是i位置前面的元素),1.此时如果f[j] + 1 == f[i],说明有不同序列相同的长度,所以g[i] += g[ j ],因为 j 位置不一定为1个元素,可能为多个,所以+=;2.如果f[j] + 1 > f[i],说明需要更新长度,即更新f表,但f表一更新,g表也要更新,此时f[i] = f[ j ],g[i] = g[ j ],长度更新后,i 位置的递增子序列的个数等于 j 位置的个数。遍历完一个循环后统计要返回的个数,先看要返回的长度是否增加,如果增加那么就更新,更新为f表、g表的最新值,否则就加上遍历后的递增子序列的新个数。

4.代码如下:

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
 int n = nums.size();
        vector<int> f(n, 1), g(n, 1);
        int len = 1, count = 1;
        for (int i = 1; i < n; ++i)
        {
            for (int j = 0; j < i; ++j)
            {
                if (nums[i] > nums[j])
                {
                    if (f[j] + 1 == f[i])
                        g[i] += g[j];
                    else if (f[j] + 1 > f[i])
                    {
                        f[i] = f[j] + 1;
                        g[i] = g[j];
                    }
                }
            }
            if (len == f[i])
                count += g[i];
            else if (len < f[i]) 
            {
                len = f[i];
                count = g[i];
            }
        }
        return count;        
    }
};

4. 最长等差数列

1.题目链接:最长等差数列
2.题目描述:
给你一个整数数组 nums,返回 nums 中最长等差子序列的长度。

回想一下,nums 的子序列是一个列表 nums[i1], nums[i2], …, nums[ik] ,且 0 <= i1 < i2 < … < ik <= nums.length - 1。并且如果 seq[i+1] - seq[i]( 0 <= i < seq.length - 1) 的值都相同,那么序列 seq 是等差的。

示例 1:

输入:nums = [3,6,9,12]
输出:4
解释: 整个数组是公差为 3 的等差数列。

示例 2:

输入:nums = [9,4,7,2,10]
输出:3
解释: 最长的等差子序列是 [4,7,10]。

示例 3:

输入:nums = [20,1,15,3,10,5,8]
输出:4
解释: 最长的等差子序列是 [20,15,10,5]。

提示:

2 <= nums.length <= 1000
0 <= nums[i] <= 500

3.问题分析:前几道所求是递增或递减序列,只需要两个数就可以知道它两是递增还是递减;而这道题所求为等差数列,要知道一个序列是否为等差数列,最少需要知道3个数;所以答题思路就和前几道差不多,一个for循环确定一个数,三个for循环就可以解决这道题;这里有一种思路,就是可以将数组中的元素和其下标存到一个哈希表中,找第三个数时,可以降低时间复杂度。其中填哈希表时应该注意:因为nums数组中可能会有相同的数值,当填到最后一个相同的元素时,下标会被覆盖,所以可以用数组存下标,另一种方式就是一遍寻找,一遍保存,保存距离最近的元素。

  1. 状态表示:dp[i][j] 表⽰:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,最⻓的等差序列的⻓度。
  2. 状态转移方程:以i,j位置进行分析,i,j位置所表示等差序列中第二,第三位数,然后在i位置之前寻找符合条件的数,如果符合,那么dp[i][j]位置的值该如何确定?i, j表示以 i 及 j 位置为结尾 的,那么新找到数的线标(符合条件数的下标)是不是就为新的 i ,而之前的 i 是不是就为新的 j ;如:符合条件的下标 i 、 j ;dp[i][j] = dp[符合条件的下标][i] + 1
  3. 初始化:因为等差数列的长度不是0,就是比3大,所以将所有值初始化为2
  4. 填表顺序:固定倒数第二个数,再由倒数第一个数寻找符合条件的数。
  5. 返回值:返回dp表中最大的元素。

4.代码如下:

class Solution 
{
public:
    int longestArithSeqLength(vector<int>& nums) 
    {
        int n = nums.size();
        unordered_map<int, int> hash;

        hash[nums[0]] = 0;
        int ret = 2;
        vector<vector<int>> dp(n, vector<int>(n, 2));
        for (int i = 1; i < n; ++i) //固定倒数第二个数
        {
            for (int j = i + 1; j < n; ++j)
            {
                int a = 2 * nums[i] - nums[j];
                if (hash.count(a))
                    dp[i][j] = dp[hash[a]][i] + 1;
                ret = max(ret, dp[i][j]);
            }
            hash[nums[i]] = i;
        }
        return ret;
    }
};

5. 等差数列划分 II - 子序列

1.题目链接:等差数列划分 II - 子序列
2.题目描述:
给你一个整数数组 nums ,返回 nums 中所有 等差子序列 的数目。
如果一个序列中 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该序列为等差序列。
例如,[1, 3, 5, 7, 9]、[7, 7, 7, 7] 和 [3, -1, -5, -9] 都是等差序列。
再例如,[1, 1, 2, 5, 7] 不是等差序列。
数组中的子序列是从数组中删除一些元素(也可能不删除)得到的一个序列。

例如,[2,5,10] 是 [1,2,1,2,4,1,5,10] 的一个子序列。
题目数据保证答案是一个 32-bit 整数 。

示例 1:

输入:nums = [2,4,6,8,10]
输出:7
解释:所有的等差子序列为: [2,4,6] [4,6,8] [6,8,10] [2,4,6,8] [4,6,8,10] [2,4,6,8,10] [2,6,10]

示例 2:

输入:nums = [7,7,7,7,7]
输出:16
解释:数组中的任意子序列都是等差子序列。

提示:

1 <= nums.length <= 1000
-2^31 <= nums[i] <= 2^31 - 1

3.题目解析:这道题是在一个数组中寻找等差子序列的总个数,并且等差数列可以相同。对于寻找等差数列还是用和前几道一样的方法,一个for循环固定等差数列倒数第二的元素,另一个固定等差数列倒数第一的元素,然后寻找构成等差数列的数,如果再加一个for循环,那么时间复杂度就太大了,所以用一个hash表将nums中的元素和元素下标管理起来,这样寻找的时间复杂度接近O(1)。需要注意的是nums数组中的元素可能会有重复的,所以可以用1对多的关系来管理下标。 因为用两层循环来确定等差数列,所以定义一个二维dp[i][j]表示:以 i 位置及 j 位置为结尾所有等差数列的个数,因为每个i,j位置是不一样的,所以最后返回的是dp表中所有元素的总和。

  1. 状态表示:dp[i][j] 表示:以 i 位置以及 j 位置的元素为结尾的所有的⼦序列中,等差⼦序列的个数。其中 i < j ,即 i 为等差数列倒数第二个位置,j 为倒数第一个。
  2. 状态转移方程:如果寻找的倒数第三个等差数列,这个位置记为 k,那么k,i,j 构成等差数列,k前面有可能还有元素;这里举个例子,将k,i,j 当做子序列的话不容易想到,那么就k,i,j 当做是连续的等差数列,k前面再记几个元素,如下m,n,k,i,j 这5位数构成等差数列,以k,i 位置来分析,dp表表示的是以 i 位置以及 j 位置的元素为结尾等差⼦序列的个数,那么以i,j 为结尾是比k,i 为结尾的个数多一个,详解如等差数列划分1。所以状态转移方程为:dp[i][j] += dp[k][i] = 1 。
  3. 填表顺序:从上往下,从左往右。
  4. 初始化:dp表全初始化为0。
  5. 返回值:返回的是dp表中所有元素的总和。

4.代码如下:

class Solution 
{
public:
    int numberOfArithmeticSlices(vector<int>& nums)
    {
        int n = nums.size();
        unordered_map<long long, vector<int>> hash; //一对多的关系
        for (int i = 0; i < n; ++i) //将所有nums数组中的元素与其对应的下标管理起来
            hash[nums[i]].push_back(i);

        int ret = 0;
        vector<vector<int>> dp(n, vector<int>(n));
        for (int i = 1; i < n; ++i) //确定倒数第二个数
        {
            for (int j = i + 1; j < n; ++j) //确定倒数第一个数
            {
                long long a = ((long long)nums[i]) * 2 - nums[j]; //相乘可能会越界,所以先强转,再相乘
                if (hash.count(a)) //寻找符合条件的数
                {
                    for (auto& k : hash[a])
                    {
                        if (k < i) //降符合的个数全加起来
                            dp[i][j] += dp[k][i] + 1;
                    }
                }
                ret += dp[i][j];
            }
        }
        return ret;
    }
};
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ly@눈_눈

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

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

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

打赏作者

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

抵扣说明:

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

余额充值