题解:AcWing 896 最长上升子序列II

【题目来源】

AcWing:896 最长上升子序列 II - AcWing题库

【题目描述】

给定一个长度为 \(N\) 的数列,求数值严格单调递增的子序列的长度最长是多少。

【输入】

第一行包含整数 \(N\)

第二行包含 \(N\) 个整数,表示完整序列。

【输出】

输出一个整数,表示最大长度。

【输入样例】

7
3 1 2 1 8 5 6

【输出样例】

4

【核心思想】

  1. 问题分析:给定长度为 \(N\) 的数列,求数值严格单调递增的子序列的最大长度。与 895 题相同,但数据范围更大(\(N \leq 10^5\)),需要使用二分优化将时间复杂度从 \(O(N^2)\) 优化到 \(O(N \log N)\)

  2. 算法选择

    • 贪心 + 二分查找:维护数组 \(q\)\(q[i]\) 表示长度为 \(i\) 的上升子序列的最小结尾元素
    • 关键性质\(q\) 数组单调递增,可以使用二分查找
  3. 关键步骤

    • 初始化\(q[0] = -\infty\)\(len = 0\)
    • 对于每个元素 \(a[i]\)
      • 二分查找最大的 \(r\) 使得 \(q[r] < a[i]\)\(a[i]\) 可以接在长度为 \(r\) 的子序列后面)
      • 更新 \(q[r+1] = a[i]\)(用更小的值替换,有利于后续扩展)
      • 更新 \(len = \max(len, r+1)\)
    • 答案\(len\) 即为 LIS 长度
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log N)\),每个元素二分查找
    • 空间复杂度:\(O(N)\),数组 \(q\)
  5. LIS 二分优化的核心思想

    • 贪心策略:同样长度的子序列,结尾越小越有利于后续扩展
    • 数组 \(q\) 的单调性\(q[i]\) 表示长度为 \(i\) 的 LIS 的最小结尾,\(q\) 数组单调递增
    • 二分查找:对于每个 \(a[i]\),二分查找它可以接在哪个位置后面
    • 更新策略:用 \(a[i]\) 替换 \(q[r+1]\),保持 \(q\) 数组的最小性
    • 适用于数据范围大的 LIS 问题,\(N\) 可达 \(10^5\) 级别

【解题思路】

【算法标签】

线性DP-一维

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 100005; // 定义常量N,表示数组的最大长度
int n; // 输入的数组长度
int a[N]; // 存储输入的数组
int q[N]; // q数组用于存储当前的最长上升子序列

int main()
{
    cin >> n; // 输入数组的长度
    for (int i = 0; i < n; i++) cin >> a[i]; // 输入数组元素

    int len = 0; // 初始化最长上升子序列的长度为0
    q[0] = -2e9; // 初始化q[0]为一个极小值,方便后续二分查找

    // 遍历数组中的每个元素
    for (int i = 0; i < n; i++)
    {
        int l = 0, r = len; // 二分查找的左右边界
        while (l < r) // 二分查找
        {
            int mid = (l + r + 1) >> 1; // 取中间值,+1是为了避免死循环
            if (q[mid] < a[i]) // 如果q[mid] < a[i],说明a[i]可以接在q[mid]后面
                l = mid; // 更新左边界
            else
                r = mid - 1; // 否则更新右边界
        }

        len = max(len, r + 1); // 更新最长上升子序列的长度
        q[r + 1] = a[i]; // 将a[i]放入q[r+1]的位置,更新当前的最长上升子序列
    }

    cout << len << endl; // 输出最长上升子序列的长度
    return 0;
}
// 引入所有标准库头文件,方便使用各种标准库功能
#include <bits/stdc++.h>

// 使用标准命名空间,避免每次使用标准库函数时都需要加 std:: 前缀
using namespace std;

// 定义常量
const int N = 100010;  // 数组最大长度为100010

// 定义全局变量
int n;                 // n: 输入序列的长度
int a[N];              // a[N]: 存储输入的整数序列
int q[N];              // q[i]: 表示上升子序列长度为 i 时,序列末端的最小值

// 主函数,程序的入口点
int main()
{
    // 读取序列的长度 n
    cin >> n;

    // 循环读取序列中的每个元素,并存储到数组 a[i] 中
    for (int i = 0; i < n; i++)
        cin >> a[i];  // 读入序列

    // 初始化已经求出的最大上升子序列长度为 0
    int len = 0;  // 已经求出的最大上升子序列长度

    // 动态规划结合二分查找,计算最长上升子序列的长度
    for (int i = 0; i < n; i++)
    {
        // 初始化二分查找的左右边界
        int l = 0, r = len;

        // 使用二分查找确定当前元素 a[i] 可以放置的上升子序列位置
        while (l < r)
        {
            // 计算中间位置,防止漏掉 mid,需要 +1
            int mid = (l + r + 1) / 2;

            // 如果 q[mid] 小于 a[i],说明 a[i] 可以接在长度为 mid 的子序列后面
            if (q[mid] < a[i])
                l = mid;  // 更新左边界为 mid
            else
                r = mid - 1;  // 否则,更新右边界为 mid - 1
        }

        // 将当前元素 a[i] 放置在上升子序列长度为 r + 1 的位置,更新 q[r + 1]
        q[r + 1] = a[i];

        // 更新已经求出的最大上升子序列长度
        len = max(len, r + 1);
    }

    // 输出最长上升子序列的长度
    cout << len << endl;

    // 程序正常结束,返回 0
    return 0;
}

【运行结果】

7
3 1 2 1 8 5 6
4
posted @ 2026-06-14 10:37  团爸讲算法  阅读(4)  评论(0)    收藏  举报