【Hot 100 刷题计划】 LeetCode 300. 最长递增子序列 | C++ 动态规划 & 贪心二分

LeetCode 300. 最长递增子序列

📌 题目描述

题目级别:中等

给你一个整数数组 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。

💡 解法一:动态规划 (回头找垫脚石)

面对子序列问题,动态规划是最正统的解法。

状态定义:

定义 dp[i] 表示:nums[i] 这个数结尾 的最长递增子序列的长度。
(注意:这里必须是以它结尾,这样我们才能确切知道下一个数能不能接在它后面。)

状态转移方程:

假设我们正在考察第 i 个数字,我们如何求它的 dp[i]

我们只需要回头看它前面的所有数字(假设索引为 j0 <= j < i):

只要 nums[i] > nums[j],说明 nums[i] 可以完美地接在 nums[j] 的后面,形成一个更长的递增子序列。

所以我们在所有符合条件的 j 中,挑一个 dp[j] 最大的接上去,再加上自己本身的长度 1 即可:

dp[i] = max(dp[i], dp[j] + 1)

初始化:

每个数字自己本身就可以构成一个长度为 1 的子序列,所以 dp 数组初始全部赋值为 1


💻 C++ 代码实现 (DP 法)

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

        // 规范写法:使用 vector 开辟状态数组,并全部初始化为 1
        vector<int> dp(n, 1);
        int res = 1;

        for (int i = 0; i < n; i ++ )
        {
            // 内层循环:回头寻找可以接上去的"垫脚石"
            for (int j = 0; j < i; j ++ )
            {
                if (nums[i] > nums[j]) {
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            // 记录整个过程中出现的最大长度
            res = max(res, dp[i]);
        }

        return res;
    }
};

进阶挑战:你能将算法的时间复杂度降低到 O(nlog⁡n)O(n \log n)O(nlogn) 吗?


💡 解法二:贪心策略 + 二分查找

为了让递增子序列尽可能的"长",我们需要秉持一个贪心 的原则:让子序列的增长速度尽可能的慢!

换句话说,每次加进来的数字越小,后面能接上的数字就越多。

我们可以维护一个名为 tails 的数组:

  • tails[k] 表示:长度为 k+1 的递增子序列中,末尾最小的那个数字。

核心运作机制:

遍历原数组 nums,对于当前数字 num

  1. 如果 numtails 数组的最后一个元素还要大
    简直完美!说明它可以直接接在当前最长的子序列后面,让最大长度加 1。我们直接把它 push_backtails 末尾。
  2. 如果 num 没有比最后一个元素大
    它虽然不能增加最长子序列的长度,但它是一个"潜力股"。我们要在 tails 数组中找到第一个大于等于 num 的元素 ,并用 num替换 它!
    为什么?因为 tails 数组天然是严格递增的!把较大的末尾元素换成较小的 num,不会改变当前子序列的长度,但会让末尾数字变小,为后续接上更多的数字创造了有利条件。

由于 tails 数组是严格递增的,在其中寻找"第一个大于等于 num 的元素"这一步,我们可以直接使用二分查找 ,将这部分的时间从 O(N)O(N)O(N) 降到 O(log⁡N)O(\log N)O(logN)。


💻 C++ 代码实现 (贪心+二分法)

cpp 复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        // tails 数组存储当前各个长度的递增子序列的最小末尾元素
        vector<int> tails;

        for (int num : nums) {
            // 如果 tails 为空,或者当前数字大于 tails 的最后一个数字,直接追加
            if (tails.empty() || num > tails.back()) {
                tails.push_back(num);
            } 
            else {
                // 否则,使用二分查找找到 tails 中第一个 >= num 的元素
                int l = 0, r = tails.size() - 1;
                while (l < r) {
                    int mid = l + (r - l) / 2;
                    if (tails[mid] >= num) {
                        r = mid;     // 目标在左侧或就是 mid
                    } else {
                        l = mid + 1; // 目标在右侧
                    }
                }
                // 用更小的 num 替换掉原来的较大元素,培养潜力
                tails[l] = num;
                
                // 也可以一行代码搞定:
                // *lower_bound(tails.begin(), tails.end(), num) = num;
            }
        }

        // tails 数组的最终长度,就是最长递增子序列的长度
        return tails.size();
    }
};
相关推荐
CHANG_THE_WORLD1 小时前
C++ 文件读取函数完全指南
开发语言·c++
阿正的梦工坊1 小时前
JavaScript 闭包 × C++ 类比:彻底搞懂闭包
开发语言·javascript·c++
6Hzlia1 小时前
【Hot 100 刷题计划】 LeetCode 72. 编辑距离 | C++ 经典 DP 增删改状态转移
c++·算法·leetcode
穿条秋裤到处跑2 小时前
每日一道leetcode(2026.04.16):距离最小相等元素查询
算法·leetcode·职场和发展
无限进步_2 小时前
【C++】寻找字符串中第一个只出现一次的字符
开发语言·c++·ide·windows·git·github·visual studio
楼田莉子2 小时前
Linux网络:IP协议
linux·服务器·网络·c++·学习·tcp/ip
wuminyu2 小时前
专家视角看JVM_StartThread
java·linux·c语言·jvm·c++
敲上瘾2 小时前
高并发内存池(三):PageCache(页缓存)的实现
linux·c++·缓存·高并发内存池·池化技术
小雅痞3 小时前
[Java][Leetcode simple] 1. 两数之和
java·算法·leetcode