一、算法概述

最长递增子序列(Longest Increasing Subsequence, LIS)是指在一个序列中,找到一个最长的子序列,使得子序列中的元素严格递增。子序列不要求连续,但要求保持原序列中的相对顺序。

例如:

  • 序列 [10, 9, 2, 5, 3, 7, 101, 18] 的 LIS 是 [2, 3, 7, 101][2, 5, 7, 101],长度为 4。
  • 序列 [0, 1, 0, 3, 2, 3] 的 LIS 是 [0, 1, 2, 3],长度为 4。

LIS 是动态规划的经典问题,有 O(n²) 的 DP 解法和 O(n log n) 的贪心+二分解法。


二、核心思想

方法一:动态规划 O(n²)

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

状态转移: 对于每个 i,向前扫描 j ∈ [0, i-1]

  • nums[j] < nums[i],则 dp[i] = max(dp[i], dp[j] + 1)

初始 dp[i] = 1(每个元素自身构成长度为 1 的子序列)。最终答案为 max(dp[i])

方法二:贪心 + 二分查找 O(n log n)

维护一个数组 tailstails[k] 表示长度为 k+1 的递增子序列的最小可能末尾元素。

核心思想: 对于每个元素 x

  • x 大于 tails 中所有元素,则 x 可以扩展当前最长递增子序列,将 x 追加到 tails 末尾。
  • 否则,在 tails 中找到第一个 ≥ x 的元素并替换为 x(这不会改变当前 LIS 长度,但为后续更小的元素创造了机会)。

tails 的长度即为 LIS 长度。


三、算法步骤 / 伪代码

O(n²) DP 解法:

dp[0..n-1] = 1
for i = 0 to n-1:
    for j = 0 to i-1:
        if nums[j] < nums[i]:
            dp[i] = max(dp[i], dp[j] + 1)
return max(dp)

O(n log n) 贪心+二分:

tails = []
for each x in nums:
    if tails 为空 或 x > tails.back():
        tails.push_back(x)
    else:
        在 tails 中二分查找第一个 ≥ x 的位置 pos
        tails[pos] = x
return tails.size()

四、C++ 代码实现

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// ==================== O(n²) DP 解法 ====================
int lengthOfLIS_n2(const vector<int>& nums) {
    int n = nums.size();
    vector<int> dp(n, 1);    // dp[i] = 以 nums[i] 结尾的 LIS 长度
    int maxLen = 1;

    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);
            }
        }
        maxLen = max(maxLen, dp[i]);
    }

    cout << "DP 数组: ";
    for (int v : dp) cout << v << " ";
    cout << endl;

    return maxLen;
}

// ==================== O(n log n) 贪心 + 二分 ====================
int lengthOfLIS_nlogn(const vector<int>& nums) {
    vector<int> tails;  // tails[k] = 长度为 k+1 的 IS 的最小末尾

    for (int x : nums) {
        // 在 tails 中找第一个 >= x 的位置
        auto it = lower_bound(tails.begin(), tails.end(), x);

        if (it == tails.end()) {
            // x 比所有 tails 中的元素都大,扩展 LIS
            tails.push_back(x);
        } else {
            // 替换第一个 >= x 的元素
            *it = x;
        }

        // 调试输出 tails 的变化过程
        cout << "处理 " << x << " 后 tails: ";
        for (int v : tails) cout << v << " ";
        cout << endl;
    }

    return tails.size();
}

int main() {
    cout << "请输入数组元素个数: ";
    int n;
    cin >> n;

    vector<int> nums(n);
    cout << "请输入 " << n << " 个整数: ";
    for (int i = 0; i < n; i++) cin >> nums[i];

    cout << "\n===== O(n²) DP 解法 =====" << endl;
    int lis1 = lengthOfLIS_n2(nums);
    cout << "LIS 长度 (DP): " << lis1 << endl;

    cout << "\n===== O(n log n) 贪心+二分 =====" << endl;
    int lis2 = lengthOfLIS_nlogn(nums);
    cout << "LIS 长度 (贪心+二分): " << lis2 << endl;

    return 0;
}

五、复杂度分析

解法 时间复杂度 空间复杂度 说明
动态规划 O(n²) O(n) 两重循环,简单直观
贪心 + 二分 O(n log n) 每个元素二分一次,tails 数组

注意: 贪心+二分法只能求出 LIS 的长度,不能直接输出 LIS 的具体序列。若需要输出具体序列,可用 O(n²) DP + 回溯,或在贪心法中维护额外的"前驱"信息。


六、应用场景

  1. 最长递增子序列问题本身: 直接求序列中最长递增部分
  2. 信封嵌套问题: 按宽度排序后对高度求 LIS(LeetCode 354 俄罗斯套娃信封)
  3. 任务调度 / 活动安排: 选择最多不冲突的任务
  4. 求最长不降子序列:lower_bound 改为 upper_bound
  5. 数组最少递减子序列划分数: Dilworth 定理相关
  6. 建桥问题 / 布线问题: 实际为二维 LIS

七、经典例题

LeetCode 300. 最长递增子序列

题目描述: 给你一个整数数组 nums,找到其中最长严格递增子序列的长度。

O(n log n) 解法代码:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> tails;
        for (int x : nums) {
            auto it = lower_bound(tails.begin(), tails.end(), x);
            if (it == tails.end())
                tails.push_back(x);
            else
                *it = x;
        }
        return tails.size();
    }
};

扩展:LeetCode 354. 俄罗斯套娃信封问题

题目描述: 给定一些信封的宽度和高度 (w, h),当另一个信封的宽度和高度都大于这个信封时,这个信封可以放进另一个信封里(类似俄罗斯套娃)。求最多能嵌套多少层。

思路: 按宽度升序排序(宽度相同时高度降序),然后对高度求 LIS(O(n log n))。

class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        // 宽度升序,宽度相同时高度降序(确保同宽信封不能嵌套)
        sort(envelopes.begin(), envelopes.end(),
             [](auto& a, auto& b) {
                 return a[0] < b[0] || (a[0] == b[0] && a[1] > b[1]);
             });

        vector<int> tails;
        for (auto& e : envelopes) {
            int h = e[1];
            auto it = lower_bound(tails.begin(), tails.end(), h);
            if (it == tails.end())
                tails.push_back(h);
            else
                *it = h;
        }
        return tails.size();
    }
};