最长不下降子序列问题
最长上升子序列问题是解决很多问题的根本,它能帮助你理解二分的思想。
引言
考虑一下:对于一个序列 n n n ,请你查找 n n n中最长的子序列 a a a,使得任意 i < j i<j i<j 时 a [ i ] < = a [ i ] a[i]<=a[i] a[i]<=a[i].
例如一个长度为
5
5
5的
n
n
n=
5
5
5
3
3
3
1
1
1
2
2
2
4
4
4;
显然,它的最长不下降子序列就是
1
1
1
2
2
2
4
4
4.
我们可以想一下自己是如何看出它的最长不下降子序列的.
首先,第一个数是
5
5
5,前面没有数,所以它可以是子序列的一部分,那么我们可以将它放到考虑的第一位:
5
5
5 ? ? ? ?
因为放了一个数,所以答案要加一:
a
n
s
=
1
ans=1
ans=1
第二个数是
3
3
3,有两种选择,一种是插入到刚插入的数的后面,第二种就是替换掉
5
5
5.因为
5
>
3
5>3
5>3,所以
3
3
3不可以放到
5
5
5的后面去.
5
5
5比
3
3
3大还在答案中,那我要你
5
5
5有什么用?果断替换:
3
3
3 ? ? ? ?
a
n
s
=
1
ans=1
ans=1
第三个数是
1
1
1,与
3
3
3同理,替换:
1
1
1 ? ? ? ?
a
n
s
=
1
ans=1
ans=1
第四个数是
2
2
2,比刚插入的数小,可以插入,那么就变为:
1
1
1
2
2
2 ? ? ?
a
n
s
=
2
ans=2
ans=2
第五个数同理插入:
1
1
1
2
2
2
4
4
4 ? ?
a
n
s
=
3
ans=3
ans=3
至此,我们的大脑
(
?
)
(?)
(?)处理完了这样一个长度为
5
5
5的最长不下降子序列,当长度很小时我们能顺利解决,剩下的就交给计算机啦.
二分求解
在我们模拟的时候,有这样一个操作,当新读入的数字小于答案数列的第
a
n
s
ans
ans个数的时候,我们需要找到要用读入数字替换掉的位置.这个时候,选择从第
1
1
1个数挨个比对到第
a
n
s
ans
ans个数就很睿智 ,于是我们选择二分求解.
看这样一个序列:
1
1
1
2
2
2
4
4
4
6
6
6
8
8
8
10
10
10
假设新读入的数是3.
那么我们取这个元素个数为
6
6
6的序列的中间数:
4
4
4
4
4
4比
3
3
3大,而这个序列是单调递增的,要替换的位置一定在左侧.我们取左侧的序列.
1
1
1
2
2
2
4
4
4
中间数为
2
2
2,
2
<
3
2<3
2<3,那么我们取右侧的序列
4
4
4
可以判断,序列中只有一个元素,要替换的数就决定是它啦!
这样子寻找就省去了
O
(
N
)
O(N)
O(N)的复杂度转为
O
O
O
(
(
(
l
o
g
log
logn
)
)
)了,为我们节省了很多时间.
代码实现
#include <cstdio>
#include <iostream>
#define maxn 100001
#define minn -99999
using namespace std;
int n,a[maxn],f[maxn],ans;//f数组就相当于上面的答案数组,a数组存的是数的值.
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
f[0]=minn;
for(int i=1;i<=n;i++){
if(a[i]>f[ans]){//直接向答案序列后加数
f[ans+1]=a[i];
ans++;
}
else{
int l=0,r=ans;
while(l<r){
int mid=(l+r)>>1;
if(f[mid]>a[i]){
r=mid;//如果中间数比要加的数大,那么就要取左侧序列
}
else{
l=mid+1;//如果中间数比要加的数小,那么就要取右侧序列
}
}
f[l]=a[i];//最后替换
}
}
printf("%d\n",ans);
return 0;
}
更简洁的写法
你可能会问了:有没有更简洁的写法来实现这样高效的二分呢?答案是有的.在我们的c++ STL库中就有这样的函数,来帮助我们实现查找.
在< algorithm > 库中,有个叫lower_bound的函数,先看他的函数定义:
template< class ForwardIt, class T >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T& value );
它的功能是:
返回指向范围 [first, last) 中首个不小于(即大于或等于) value 的元素的迭代器,或若找不到这种元素则返回 last 。
如果想了解更多关于lower_bound的严格说明,请点击传送门
那么我们有了这个工具之后就可以这样改进我们的程序,让它更简洁.
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define maxn 100001
using namespace std;
int n,a[maxn],f[maxn],ans;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
memset(f,10000,sizeof f);//要初始化的大,后面的比较就不会因为值为0出错
f[0]=-1;//vis[0]除外,它要设为-1
for(int i=1;i<=n;i++){
int v=lower_bound(f,f+1+ans,a[i])-f;//在1到ans的区间中寻找第一个比a[i]大的.
ans=max(ans,v);//更新答案
f[v]=min(f[v],a[i]);//替换掉比a[i]大的
}
printf("%d\n",ans);
return 0;
}
这样就将二分的过程运用STL函数省去了,提高了我们的编程效率.