LeetCode_C++线性动态规划(线性dp)_单串LIS(最长递增子序列,最长递增子序列的个数)

本文详细介绍了线性动态规划的概念,以最长递增子序列为例,解析了动态规划问题的求解步骤和状态转移方程。通过两个LeetCode题目,展示了如何找出一个数列的最长递增子序列以及其个数。状态定义、BaseCase、状态转移函数和状态更新策略是解决问题的关键。

1.线性动态规划

线性动态规划的特点是:状态推导按照i从小到大进行推导。
大规模的解答依靠小规模的解答。

解决动态规划问题大体上分为:

  • 问题的Base Case情况(最简单的情况)
  • 问题的状态有哪些
  • 什么选择问题的状态可能会改变
  • 如何定义dp数组表达状态和选择(状态转移)

线性动态规划状态定义一般为:

dp[n]:0~n符合题干的答案

状态转移一般有两种情况:

1. 依赖比 i 小的 O(1) 个子问题

dp[n] = f(dp[n-1], ..., dp[0]);
其中f一般为最大值和最小值函数。具体情况具体分析

2. 依赖比 i 小的 O(n) 个子问题
此时dp[n] 与此前的更小规模的所有子问题 dp[n - 1], dp[n - 2], …, dp[1] 都可能有关系

for(int i=1;i<n;i++){
	for(int j=1;j<i-1;j++){
		dp[i]=f(dp[i],f(dp[j]))
	}
}
其中f一般为最大值和最小值函数。具体情况具体分析

线性动态规划分为:单串,双串,矩阵问题等,这里先练习单串最经典的LIS问题。

2.例题练习

最长递增子序列

LeetCode原题链接
在这里插入图片描述
思路:输入是一个单串,首先思考单串问题中设计状态

  1. dp[i]数组定义:代表数组下标为i时最长子序列。包括数组下标为i的元素。
  2. 初始状态:当i=0时,数组只有一个元素,所以dp[0]=1。
  3. 每次选择是将数组的下标+1,代表检测过这个元素了。这个选择会影响最长子序列。
  4. 状态转化:因为dp[i]是数组下标为i时最长子序列,但不一定dp[i-1]一定小于dp[i]。所以这个状态转化为dp[i]=max(dp[i],(dp[0]…dp[i-1])+1)。
  5. 需要注意,更新dp数组的时机,当num[0]…num[i-1]之间有元素<num[i]才需要更新dp[i]。
  6. 最后的值是dp数组中最大的值,所以需要记录最大值作为返回值。

eg:
nums[j]<nums[i](0<= j <=i-1)
此时的dp[i]=max(dp[i],dp[j]+1);
需要遍历0~i-1次dp数组才可以判断dp[i]的值。
这个状态转移是第二类。依赖比 i 小的 O(n) 个子问题

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        //dp[i]代表数组下标为i时最长子序列
        vector<int>dp(nums.size(),1);
        dp[0]=1;
        int ret=1;
        for(int i=1;i<nums.size();i++){
            for(int j=0;j<i;j++){
                //因为dp[i]最大值不一定是最后值,所以每次都需要遍历一遍dp数组找最大值
                if(nums[j]<nums[i]){
                    dp[i]=max(dp[j]+1,dp[i]);
                }
            }
            ret=max(ret,dp[i]);
        }
        return ret;
    }
};

最长递增子序列的个数

LeetCode原题链接
在这里插入图片描述
这个题的思路与上题相同,但是需要返回最长递增的子序列个数。

首先,我们是按照递增的方式找的子序列,所以数列一定是递增的。

我们定义一个数组count,count[i]代表nums[i]元素之前的递增序列长度有多少个。
最后答案为count数组中长度是最长子序列的所有数组元素和。

int longest;//最长递增子序列
int ans;//最后答案
for(int i=0;i<count.size();i++){
	if (dp[i] == longest) {
    	ans += counts[i];
    }
}

之所以这么设,是因为最长递增子序列可能在dp数组的任何一个位置。

其次在一问中我们可以知道

当dp[i]>dp[j]需要更新dp数组
dp[i]=max(dp[i],(dp[0]…dp[i-1])+1)

for(int i=1;i<nums.size();i++){
    for(int j=0;j<i;j++){
        //因为dp[i]最大值不一定是最后值,所以每次都需要遍历一遍dp数组找最大值
        if(nums[j]<nums[i]){
           //dp[i]=max(dp[j]+1,dp[i]);
           if(dp[j]>=dp[i]){
          	  dp[i]=dp[j]+1	;
          	  count[i]=count[j];//最长递增子序列发生改变,此时count[i]为最长递增子序列,其值和发生改变的上一个位置相同。
           }
           else if(dp[j] + 1 == dp[i]){
           	  //最长递增子序列长度没变化,但是序列发生变化,有count[j] 个额外的序列
           	  count[i]+=count[j];
           }
        }
    }
    ret=max(ret,dp[i]);
}
  • 如果这些序列比 length[i] 长,那么我们就知道我们有count[j] 个长度为 length 的序列。
  • length[j] + 1 = length[i]说明在某个最长递增序列中,num[j]正好是num[i]的前一个元素。那么num[j]所对应的count[j]应该要累加到num[i]所对应的count[i]上
class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int size=nums.size();
        if(size==0||size==1){
            return size;
        }
        vector<int>dp(size,1);
        vector<int>count(size,1);
        dp[0]=1;
        count[0]=1;
        int maxlen=1;
        for(int i=0;i<size;i++){
            for(int j=0;j<i;j++){
                if(nums[i]>nums[j]){
                    //dp[i]=max(dp[j]+1,dp[i]);
                    if(dp[j]+1==dp[i]){
                        count[i]+=count[j];
                    }
                    else if(dp[j]+1>=dp[i]){
                        count[i]=count[j];
                        dp[i]=dp[j]+1;                        
                    }
                }
            }
            maxlen=max(maxlen,dp[i]);
        }
        int ans=0;
        for(int i=0;i<count.size();i++){
            if(dp[i]==maxlen){
                ans+=count[i];
            }
        }
        return ans;
    }
};
LeetCode 673 题“最长递增子序列个数”的动态规划解法及分析如下: ### 分析 - **定义状态**: - `dp[i]`:到`nums[i]`为止的最长递增子序列长度。 - `count[i]`:到`nums[i]`为止的最长递增子序列个数 [^2]。 - **初始化状态**: - `dp = [1] * n`:代表最长递增子序列的长度至少为 1。 - `count = [1] * n`:代表最长递增子序列个数至少为 1 [^2]。 - **状态转移**: - 对于每个`i`,从 0 到`i - 1`遍历`j`,如果`nums[i] > nums[j]`,则有以下两种情况: - 若`dp[i] < dp[j] + 1`,说明找到了更长的递增子序列,更新`dp[i] = dp[j] + 1`,同时`count[i] = count[j]`。 - 若`dp[i] == dp[j] + 1`,说明找到了长度相同的递增子序列,更新`count[i] += count[j]` [^2]。 ### 代码实现 以下是用 C++ 实现的代码: ```cpp class Solution { public: int findNumberOfLIS(vector<int>& nums) { int n = nums.size(); if(n < 2) return n; vector<int> cnts(n, 1); vector<int> lens(n, 1); for(int i = 1; i < n; ++i) { for(int j = 0; j < i; ++j) { if(nums[i] > nums[j]) { // 代表第一次遇到最长子序列 if(lens[i] < lens[j] + 1) { lens[i] = lens[j] + 1; cnts[i] = cnts[j]; } // 代表已经遇到过最长子序列 else if(lens[i] == lens[j] + 1) { cnts[i] += cnts[j]; } } } } // 求最长递增子序列的长度 int maxLen = *max_element(lens.begin(), lens.end()); int maxCnt = 0; // 找到有最长长度的次数,将它们加起来 for(int i = 0; i < n; ++i) { if(maxLen == lens[i]) maxCnt += cnts[i]; } return maxCnt; } }; ``` 以下是用 C 语言实现的代码: ```c int findNumberOfLIS(int* nums, int numsSize) { if(numsSize == 0 || numsSize == 1) return numsSize; int *length = (int*)malloc(sizeof(int) * numsSize); int *count = (int*)malloc(sizeof(int) * numsSize); for(int i = 0; i < numsSize; i++) { count[i] = 1; // 以nums[i] 结尾得最长序列的个数 length[i] = 1; // 以nums[i]结尾的最长序列的长度 } int max = 1; for(int i = 1; i < numsSize; i++) { for(int j = 0; j < i; j++) { if(nums[i] > nums[j]) { if(length[i] < length[j] + 1) { length[i] = length[j] + 1; count[i] = count[j]; } else if(length[i] == length[j] + 1) { count[i] += count[j]; } } } max = max > length[i] ? max : length[i]; } int res = 0; for(int i = 0; i < numsSize; i++) { if(length[i] == max) res += count[i]; } return res; } ``` ### 复杂度分析 - **时间复杂度**:$O(n^2)$,其中`n`是数组的长度,因为有两层嵌套循环 [^3]。 - **空间复杂度**:$O(n)$,主要用于存储`dp`和`count`数组 [^3]。 ### 总结 动态规划是解决最长递增子序列个数问题的一种有效方法。通过定义合适的状态和状态转移方程,可以逐步计算出到每个位置为止的最长递增子序列长度和个数。最后,找出最长递增子序列的长度,并统计拥有该长度的子序列个数
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

BaiRong-NUC

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

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

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

打赏作者

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

抵扣说明:

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

余额充值