leetcode 300最长递增子序列

LeetCode 300. 最长递增子序列(动态规划)

一、完整代码(精简注释)

复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        if(n == 0) return 0;

        // dp[i]表示以nums[i]结尾的最长递增子序列长度
        vector<int> dp(n,1);

        for(int i=0;i<n;i++){
            for(int j=0;j<i;j++){
                if(nums[j]<nums[i]){ // 满足严格递增
                    // 将nums[i]接到以nums[j]结尾的子序列后面
                    dp[i] = max(dp[i],dp[j]+1);
                }
            }
        }

        // 返回所有递增子序列中的最大长度
        return *max_element(dp.begin(),dp.end());
    }
};

二、动态规划五步分析

1. 确定 dp 数组含义

dp[i] 表示以 nums[i] 结尾的最长严格递增子序列长度。

注意:这里强调的是以 nums[i] 结尾,而不是前 i 个元素中的最长递增子序列长度。

例如:

复制代码
nums = {1,3,2,4};

对应的 DP 数组:

i numsi dpi 对应的递增子序列
0 1 1 1
1 3 2 13
2 2 2 12
3 4 3 134

最终:

复制代码
dp = {1,2,2,3};

最长递增子序列长度为 3。

2. 确定状态转移方程(核心)

复制代码
if(nums[j]<nums[i]){
    dp[i] = max(dp[i],dp[j]+1);
}

计算 dp[i] 时,需要遍历 i 前面的所有元素 nums[j],寻找能够连接到当前元素的递增子序列。

条件:

复制代码
nums[j] < nums[i]

只有当前面的元素小于当前元素时,才能把 nums[i] 接在以 nums[j] 结尾的递增子序列后面。

状态转移:

\ dp\[i=\max(dpi,dpj+1) \]

其中:

  • dp[i]:当前已经找到的、以 nums[i] 结尾的最长递增子序列长度。

  • dp[j]:以 nums[j] 结尾的最长递增子序列长度。

  • +1:把当前元素 nums[i] 接到原来的递增子序列后面。

  • max():比较不同方案,保留最长长度。

例如:

复制代码
nums = {1,3,2,4};

当 i=3,即 nums[i]=4 时:

复制代码
j=0:nums[0]=1 < 4
dp[3] = max(1,dp[0]+1) = 2

j=1:nums[1]=3 < 4
dp[3] = max(2,dp[1]+1) = 3

j=2:nums[2]=2 < 4
dp[3] = max(3,dp[2]+1) = 3

最终:

复制代码
dp[3] = 3;

说明以数字 4 结尾的最长递增子序列长度为 3。

3. 确定 dp 数组初始化

复制代码
vector<int> dp(n,1);

将所有 dp[i] 初始化为 1。

原因是:每个元素本身都可以构成长度为 1 的递增子序列。

例如:

复制代码
nums = {5,4,3,2};

由于数组严格递减,无法找到长度大于 1 的递增子序列。

所以:

复制代码
dp = {1,1,1,1};

因此,dp[i] 的初始值应该为 1,而不是 0。

4. 确定遍历顺序

复制代码
for(int i=0;i<n;i++){
    for(int j=0;j<i;j++){
        if(nums[j]<nums[i]){
            dp[i] = max(dp[i],dp[j]+1);
        }
    }
}

外层循环 i:

从左到右遍历数组,依次计算以每个元素结尾的最长递增子序列长度。

内层循环 j:

遍历当前元素前面的所有元素,寻找能够连接到 nums[i] 的递增子序列。

为什么 j<i?

因为子序列必须保持原数组中的相对顺序,只能将当前元素接到它前面的元素之后。

而且 dp[i] 依赖之前已经计算完成的 dp[j],因此需要从左到右遍历。

5. 确定返回值

复制代码
return *max_element(dp.begin(),dp.end());

max_element() 是 C++ STL 中已经封装好的函数,用于寻找指定范围内的最大元素。

  • dp.begin():指向数组第一个元素。

  • dp.end():指向数组最后一个元素的下一个位置。

  • max_element():返回最大元素的迭代器。

  • *:解引用,获得最大元素的值。

为什么不能直接返回 dp[n-1]?

因为最长递增子序列不一定以数组最后一个元素结尾。

例如:

复制代码
nums = {1,2,3,0};
dp   = {1,2,3,1};

最长递增子序列是 {1,2,3},长度为 3。

但是:

复制代码
dp[n-1] = 1;

因此必须取整个 dp 数组中的最大值。

三、复杂度分析

时间复杂度:\(O(n^2)\)

外层循环遍历 n 个元素,内层循环遍历当前元素之前的所有元素,因此总体时间复杂度为 \(O(n^2)\)。

空间复杂度:\(O(n)\)

使用一个长度为 n 的 DP 数组保存每个位置的最长递增子序列长度。

四、核心思路总结

本题采用动态规划求解最长严格递增子序列的长度。

核心思想是:固定当前元素 nums[i] 作为递增子序列的结尾,向前寻找能够连接的元素 nums[j],并利用已经计算好的 dp[j] 更新 dp[i]。

具体过程:

  1. 定义 dp[i],表示以 nums[i] 结尾的最长递增子序列长度。

  2. 将所有 dp[i] 初始化为 1,因为每个元素自身都可以构成一个长度为 1 的子序列。

  3. 外层循环遍历当前元素 i,内层循环遍历它前面的元素 j。

  4. 如果 nums[j]<nums[i],说明可以将当前元素接到以 nums[j] 结尾的递增子序列后面。

  5. 使用 dp[i]=max(dp[i],dp[j]+1),不断更新以当前元素结尾的最长长度。

  6. 最后使用 max_element() 获取整个 DP 数组中的最大值。

核心状态转移方程:

\ \\boxed{dp\[i=\max(dpi,dpj+1),\quad numsj<numsi} \]

需要特别注意的三个地方:

  • dp[i] 表示的是以当前元素结尾的最长递增子序列,而不是整个数组的最长递增子序列。

  • nums[j]<nums[i] 必须是严格小于,不能写成 <=,因为题目要求严格递增。

  • 最终返回 max(dp),而不是 dp[n-1],因为最长递增子序列不一定以最后一个元素结尾。

记忆模板:固定当前结尾 i,向前枚举 j,满足递增条件就通过 dp[j]+1 更新 dp[i],最后取所有 dp[i] 的最大值。

相关推荐
dialect_2 小时前
KMP算法
开发语言·c++·算法
reikocao2 小时前
卡尔曼滤波的动态机制:P如何改变融合权重
算法
漂流瓶jz3 小时前
UVA-1612 猜名次 题解答案代码 算法竞赛入门经典第二版
数据结构·算法·题解·排序·aoapc·算法竞赛入门经典
吞下星星的少年·-·3 小时前
Div.2 CF 1130B题(构造,贪心)
算法·贪心·构造
文丑颜不良啊3 小时前
LeetCode1481~1500题目解析
java·leetcode
文丑颜不良啊4 小时前
LeetCode1381~1400题目解析
java·leetcode
是main不是漫4 小时前
【双指针——移动零】数组划分类题目的入门基石
c++·算法
深圳市快瞳科技有限公司5 小时前
野外鸟类AI识别落地实录:输入适配、结构化输出、去重聚合与置信度分流的工程实践
算法·计算机视觉·鸟类识别·鸟类ai识别·野外鸟类观测·林业调查
专注仿真7 小时前
梯度提升类(XFBoost)算法详解与代码实例
开发语言·人工智能·python·算法·大模型训练·控制算法
小豆庄7 小时前
视频遮罩追踪退场后ID撞号的排查—多目标场景下的失效
opencv·算法·bug·视频·python3.11