题解:AcWing 896 最长上升子序列II
【题目来源】
AcWing:896 最长上升子序列 II - AcWing题库
【题目描述】
给定一个长度为 \(N\) 的数列,求数值严格单调递增的子序列的长度最长是多少。
【输入】
第一行包含整数 \(N\)。
第二行包含 \(N\) 个整数,表示完整序列。
【输出】
输出一个整数,表示最大长度。
【输入样例】
7
3 1 2 1 8 5 6
【输出样例】
4
【核心思想】
-
问题分析:给定长度为 \(N\) 的数列,求数值严格单调递增的子序列的最大长度。与 895 题相同,但数据范围更大(\(N \leq 10^5\)),需要使用二分优化将时间复杂度从 \(O(N^2)\) 优化到 \(O(N \log N)\)。
-
算法选择:
- 贪心 + 二分查找:维护数组 \(q\),\(q[i]\) 表示长度为 \(i\) 的上升子序列的最小结尾元素
- 关键性质:\(q\) 数组单调递增,可以使用二分查找
-
关键步骤:
- 初始化:\(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 长度
-
时间/空间复杂度:
- 时间复杂度:\(O(N \log N)\),每个元素二分查找
- 空间复杂度:\(O(N)\),数组 \(q\)
-
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
浙公网安备 33010602011771号