【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();
    }
};
相关推荐
Hical6114 小时前
C++17 实战心得:那些真正改变我写代码方式的特性
c++
Hical6115 小时前
实测:C++20 协程 vs Go Gin vs Rust Actix,谁的 Web 性能更强?
c++
happymaker062615 小时前
简单LRU的实现(基于LinkedHashMap)
算法·leetcode·lru
草莓熊Lotso15 小时前
《告别 “会用不会讲”:C++ string 底层原理拆解 + 手撕实现,面试 / 开发都适用》
开发语言·c++·面试
会编程的土豆15 小时前
【数据结构与算法】空间复杂度从入门到面试:不仅会算,还要会解释
数据结构·c++·算法·面试·职场和发展
普通网友15 小时前
《算法面试必刷:15 个高频 LeetCode 题(附代码)》
算法·leetcode·面试
_深海凉_15 小时前
LeetCode热题100-搜索二维矩阵
算法·leetcode·矩阵
张槊哲15 小时前
C++ 进阶指南:如何丝滑地理解与实践多线程与多进程
开发语言·c++·算法
雪度娃娃16 小时前
Effective Modern C++——型别推导
开发语言·c++
Hello eveybody16 小时前
介绍一下背包DP(C++)
开发语言·c++·动态规划·dp·背包dp