二分法求最长上升子序列

例题:导弹拦截

这题目太经典了,我就不放了。

这里就是介绍一下如何求o(nlogn)级别的最长上升子序列长度。本来傻乎乎dp的话,应该是o(n^2)级别的。(思路来自牛客大佬)

贪心策略:如果两个子序列中一个的最后的一个元素的值越小
那么我们越有可能在后面的遍历中给它加上其他元素

用一个数组low[i]表示:长度为i的子序列的末端最小值。
遍历每一个a[i]时找到一个j,如果low[j]<a[i]
那么a[i]可以放在长度为j的子序列的后面,所以low[i+1]可能更新
我们看low[i+1]是否比a[i]大如果low[i+1]>a[i]那么low[i+1]=a[i](显然)

同时注意到,对于low的所有元素,low[j+1]>=low[j]恒成立
就是说长度为j+1的子序列的最后一个元素值一定大于j的子序列的最后一个元素值
这是因为长度为j+1的子序列就包括长度为j的子序列

那么什么时候low[j+1]会更新:当a[i]>a[j]且a[i]<a[j+1]时
结合low是个从小到大的有序序列,我们可以轻易地发现这是二分查找!!
即C++里的lower_bound(序列中的第一个大于等于target的位置)
如果求的不是最长上升子序列而是最长不下降子序列的话就用upper_bound了
因为此时的条件是a[i]>=a[j]且a[i]<a[j]了

如果是下降子序列,只需将原数组到换过来就好了

AC代码:
 

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
ll n;
ll mas[100010];
ll low[100010];
ll ans1,ans2;
const int inf=30010;
int main()
{
	n=1; 
	while(cin>>mas[n]) n++;
	n--;
	memset(low,0x3f,sizeof low);
    //fill(low+1, low+n+1, inf);
	for(int i=1;i<=n;++i){
		low[lower_bound(low+1,low+1+n,mas[i])-low]=mas[i];
	}
	//寻找j使得low【j】<=low【i】&&low【j+1】>low【i】 
	for(int i=n;i;--i){
        if(low[i]!=0x3f3f3f3f3f3f3f3f){
            ans2=i;
            break;
        }
    }
	//reverse(mas.begin(),mas.end());
	reverse(mas+1,mas+1+n);
	//memset(low,0x3f,sizeof low);
    fill(low+1, low+n+1, inf);//重新初始化不要忘了!!! 
	for(int i=1;i<=n;++i){
		low[upper_bound(low+1,low+1+n,mas[i])-low]=mas[i];
	}
	for(int i=1;i<=n;++i){
		if(low[i]!=inf) ans1=i;
	}
    //题目,那就完蛋了。
	cout<<ans1<<endl<<ans2;
	return 0;
}

两次求ans的过程,我用了不同的初始化方法,是因为自己第一次没有注意到,自己开的是long long,所以memset初始化0x3f时,是会变成0x3f3f3f3f3f3f3f3f(不用数了,八个) ,但我一开始只

开了0x3f3f3f3f.....当然,如果用fill的话,就没什么问题,只要保证low的初始化结果是:每一个元素都大于mas的值就可以了,这样就可以保证它能不断更新。找到最后一个被更新的值,它就是最长上升子序列的长度了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值