动态规划算法(子序列专题1)

动态规划算法专辑之子序列问题(1)

本专栏将从状态定义、状态转移方程、初始化、填表顺序、返回值这五大细节来详细讲述动态规划的算法的解题思路及代码实现

一、什么是子序列

子数列,又称子序列,在数学中,某个序列的子序列是从最初序列通过去除某些元素但不破坏余下元素的相对位置(在前或在后)而形成的新序列。

例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

下面我们将通过几道经典例题来进行讲述

二、最长递增子序列

image-20230614000242723

1.题目解析

严格递增:第i个元素一定比i-1大

非严格递增:第i个元素可能比i-1大,也可能相等

要从数组中的n个子序列中,找出所有递增的子序列,并在里面找到最长的,最后返回它的长度

2.状态定义

根据经验+题目要求,我们可以得到如下定义:

dp[i]表示,以下标i元素为结尾的递增子序列的最长长度

3.状态转移方程

image-20230614161226883

通过上图,我们可以发现,dp[i]有两种状态,一种是它自己就是递增子序列,一种是和前面的任意个元素组成递增子序列,对于第二种情况,我们首先要保证前面0到i-1的元素是小于nums[i]的,同时又要dp[i]是最长的长度,所以需要在0到i-1中找到最长子序列,综上,状态转移可以得到下图:

image-20230614162408634

根据上图,我们可以得到如下的状态转移方程:

image-20230614162630611

4.初始化

初始化也是根据题目要求+经验来进行的,并不是墨守成规的初始化为0

根据上述的状态转移方程和状态定义,我们可以将dp表里的值初始化为1,这样就对于长度为1的递增子序列,我们就不用再进行判断和计算了,使得状态只有一个了,大大简化了编码的实现

5.填表顺序

填表的顺序主要看的是状态转移方程,在方程中,dp[i]受dp[j]影响,而j又是小于i的,所以应该从左往右进行填表

6.返回值

本地不能简单的返回dp[n-1],因为他不一定是最长的递增子序列,他只是以n-1为结尾的最长递增子序列的长度,所以我们需要遍历dp表,返回里面的最大值

7.代码实现

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n,1);
        int res = 1;
        for(int i=1;i<n;i++)
        {
            for(int j=0;j<i;j++)
            {
                if(nums[j] < nums[i])
                {
                    dp[i] = max(dp[i],dp[j]+1);
                }
            }
            //填表的同时更新最大值,减少了一次for循环的遍历
            res = max(dp[i],res);
        }

        return res;
    }
};

三、最长斐波那契数列

1.题目解析

image-20230614164245691

在n个子序列中,找到所有符合斐波那契数列形式的子序列,并返回最长的长度

image-20230614164946801

2.状态定义

根据题目+经验,dp[i]表示:以i为结尾的斐波那契式的子序列的最长的长度

状态分析:

image-20230614165853650

新的状态定义:

dp[i] [j]表示:以i及j位置为结尾的斐波那契式子序列的最长的长度(i<j)

3.状态转移方程

image-20230614170813591

优化:由于数组中的元素是严格递增的,所以a的值是唯一确定的,所以a的下标也是唯一确定的,因此可以建立元素和元素下标的映射,并存入哈希表中,这样对于a的查找就大大简化了

image-20230614171511712

4.初始化

根据状态定义,表里的所有值应该初始化为2,对于dp表最少有两个元素i和j

5.填表顺序

从状态转方程中看出,k<i<j,,所以应该从左到右从上到下进行填表

6.返回值

和最长递增子序列一样,应该返回的是dp表里的最大值,注意,斐波那契式的子序列的最小长度应该为3,当结果小于3时,应该返回0

7.代码实现

class Solution {
public:
    int lenLongestFibSubseq(vector<int>& arr) {
        int n = arr.size();
        unordered_map<int,int> hash;
        for(int i=0;i<n;i++)
            hash[arr[i]] = i;
        vector<vector<int>> dp(n,vector<int>(n,2));
        int res = 2;
        for(int j = 2;j<n;j++)
        {
            for(int i=0;i<j;i++)
            {
                int a = arr[j] - arr[i];
                if(hash.count(a) && a < arr[i])
                {
                    int k = hash[a];
                    dp[i][j] = dp[k][i]+1;
                }
                res = max(dp[i][j],res);
            }
        }

        return res < 3 ? 0 : res;
    }
};

四、总结

本文所涉及的知识点:一维dp和二维dp,初始化的细节,状态定义(要大胆进行状态定义,错了,再改就行,当状态转移方程定义不出来的时候),返回值的细节,如何判断填表顺序

相信通过上述的两个例题,你对动态规划及子序列问题有了一定的感觉了,但对于上述问题,本文给的不一定是最优解,只是基于动态规划算法给出的解法,对于子序列问题,分析状态转移大差不差,你也可以尝试去做一些其他题练练手

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

贩梦先生007

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

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

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

打赏作者

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

抵扣说明:

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

余额充值