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] 的最大值。

相关推荐
垆边人似月.1 小时前
华为机试题 :最大数
c++·算法·华为
考研保研资料分享2 小时前
通信工程保研经验合辑:北航、北理工申请与夏令营、预推免面试(2025)
面试·职场和发展
zhenaibo5212 小时前
如何与导师有效沟通选题?
算法
熊猫钓鱼>_>2 小时前
大学图论第 6 讲:网络流与二分图匹配——对偶性让瓶颈自己现形
开发语言·算法·面试·职场和发展·图论·校招·面经
没有钱的钱仔2 小时前
最大似然估计(MLE)通俗理解
算法·机器学习·概率论
yi0112 小时前
LeetCode 424. 替换后的最长重复字符|不定长滑动窗口:如何判断窗口是否合法?
笔记·python·算法·leetcode·双指针·滑动窗口
Nebula_g2 小时前
JavaSE高级:动态代理
java·开发语言·算法·javase
垆边人似月.3 小时前
华为题:盛最多水的容器
数据结构·算法
杨逢昌工厂6S管理3 小时前
116——杨逢昌:归位三定闭环法的旺季轻量化执行方案与工具清单
经验分享·笔记·职场和发展·学习方法