AcWing. 896 最长上升子序列 II
题目描述
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
样例
输入:
7
3 1 2 1 8 5 6
输出:
4
解释:找到最长的上升子序列1 2 5 6, 长度为4
限制
- $1 ≤ N ≤ 100000$
- $−10^9 ≤ 数列中的数 ≤ 10^9$
算法1(TLE)
(贪心 + 暴力枚举) $O(n^2)$
- 贪心思路:较小的数开头的数作为的子序列 比 较大的数作为开头的子序列 更好
- 贪心步骤:
- 开一个数组
q[i]
,存的是以长度为i
的上升子序列中末尾元素最小的数, q[]
一开始是空集,长度为0
- 遍历每个数
x
,对于当前数x
, 先找到一个最大的小于x
的数c
感谢@sort:- 情况
1
:若找不到c
, 扩大q[]
的长度并记录当前数x
- 情况
2
:若找到c
, 就存在一个不等式c < x ≤ a < b
, 则将x
覆盖a
的位置
- 情况
- 开一个数组
时间复杂度
- 遍历每个数是 $O(n)$
- 遍历
q
数组每个数, 找到一个最大的小于当前数x
的数c
,最坏情况需要枚举所有数 $O(n)$ - 因此时间复杂度是 $O(n^2)$
- 由于本题的
N = 100000, (10^5)^2 = 10^10
会TLE
C++ 代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N], q[N];
int main(){
cin >> n;
for(int i = 0; i < n; i ++) scanf("%d", &a[i]);
int len = 0;
for(int i = 0; i < n; i ++){
int k = 0;
while(k < len && q[k] < a[i]) k ++;
q[k] = a[i];
if (k >= len) len ++;
}
cout << len << endl;
return 0;
}
算法2
(贪心 + 二分) $O(nlog{n})$
- 基于
算法1
的单调性:找到一个最大的小于等于当前数的数, 我们可以用 二分 来优化 - 二分的思路:
- 先定义边界,
l = 0, r = len
, 其中len
是q
数组的长度 - 然后确定
check
函数, 可以先找到不等式中c < x ≤ a ≤ b
的c
- 通过
q[r + 1] = a[i]
来将x
覆盖a
的值 - 同时也要考虑
算法1
的情况1
, 需要扩大q
数组的长度- 即
r + 1 > len
时, 表示超出了二分边界,这时就要len ++
更新q
的长度
- 即
- 通过
- 先定义边界,
时间复杂度
- 遍历每个数是 $O(n)$
- 遍历
q
数组每个数, 找到一个最大的小于当前数x
的数c
$O(log{n})$ - 因此时间复杂度是 $O(nlog{n})$
C++ 代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N], q[N];
int main(){
cin >> n;
for(int i = 0; i < n; i ++) cin >> a[i];
int len = 0;
for(int i = 0; i < n; i ++){
int l = 0, r = len; // 二分找到最大的小于当前数x的数c
while(l < r){
int mid = l + r + 1 >> 1;
if(q[mid] < a[i]) l = mid;
else r = mid - 1;
}
q[r + 1] = a[i];
if(r + 1 > len) len ++;
}
cout << len << endl;
return 0;
}
最关键的就是 开一个数组q[i],存的是以长度为i的上升子序列中末尾元素最小的数
k=0 q[0]=a[0]=3 len=1
k=0 q[0]=a[1]=1 len=1
k=1 q[1]=a[2]=2 len=2
k=0 q[0]=a[3]=1 len=2
k=2 q[2]=a[4]=8 len=3
k=2 q[2]=a[5]=5 len=3
k=3 q[3]=a[6]=6 len=4
感谢!
%%
我也没有初始化$q[0]$,但是y总初始化了,有点没想明白那样初始化的意义在哪
我认为是由于一开始二分过程的
l = 0,r = len
,因此即使最终二分没有找到元素,结果最终也是r = 0
,然后q[r+1] = a[i]
这样就已经形成了一个哨兵的概念,因此初始化和不初始化的意义是相同的请问为什么不能用r=mid这个模板
为什么二分找的是最大小于当前是数x的数c 而不是直接找到数x
麻烦问一下二分的时候为什么l=0而不是1
如果没找到x,不应该是扩大q数组的长度并记录,而是替换q[1]=x,因为没找到的情况只有前面都大于x,如果新开再记录x不就矛盾了吗
是不是代码没找到的时候 出来的结果都是r=0 然后q【r+1】 就是q 1=x 也不扩宽数组。
不对吧,没找到的情况分两种,一种是前面都大于x,还有一种就是前面都小于x。前面都大于x的话,就把x放在q[1],此时r = 0。前面都小于x的话会不断更新左边界l,最后到l = r = len,所以先更新len的长度,然后把x放在最后
我想问一下为什么输出存储的答案q序列为什么有些不是正确的序列呢?QAQ
比如 输入
7
3 2 3 1 8 5 6
q从1到len遍历输出就是
1 3 5 6
求大佬
答案不应该是2 3 5 6吗
长度为1的递增子序列 贪心选最小值是1
算法一并没有用到二分,为什么配的图中第二步写的是用二分呢
算法1和2的基本思路是一样的,只是有一步不一样,二分只是实现代码的细节。
当然也可以说图放在算法一是放错了,不过我不太想放2张差不多的图解释同样的思路。
嗷嗷嗷,谢谢大佬回复!
可以加一下怎么输出序列嘛大佬
TQL,比y总解释的通透!
好像有一点不太对,应该找到一个<x最大的数,不是<=
吧
谢谢指出,确实有问题,发现文字描述和图没对上,我把图和描述都更改了。
分析的很好啊!
如果你是把二分拆开两部分看,也就是
while
结束完就结束了二分过程的话,然后更新结果是二分之后的部分,就是你说的“二分找到一个最大的小于当前数的数”
。其实应该看作一个整体,就是最后二分结果会返回 r+1。
描述之前有问题,已更正。
算法1TLE了
是的,那个确实会
TLE
。之前跟y总反馈过这个写法hh,然后就卡掉了这也太逗了哈哈哈