• 动态规划之子序列


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

    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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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;        
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
  • 相关阅读:
    土巴兔上市再折戟,互联网家装没故事
    11. 盛最多水的容器
    C++基础——模板讲解
    LeetCode --- 1266. Minimum Time Visiting All Points 解题报告
    【5GC】5G PDU会话以及会话类型
    灵活多样认证授权,零开发投入保障 IoT 安全
    H5页面如何实现图片懒加载?
    Boolean源码解剖学
    牛客网刷题-(5)
    蓝牙核心规范(V5.4)12.1-深入详解之PAwR
  • 原文地址:https://blog.csdn.net/weixin_68278653/article/details/132817493