最长上升子序列 LIS【DP的O(n^2)及贪心+二分的O(nlogn)解法】

最长上升子序列

给你一个长度为N的序列,求其最长上升子序列的长度。

样例输入:
       6
       1 6 2 4 3 5

样例输出:
       4

解释:其最长上升子序列的长度为4。可以是{1 2 4 5 }或者{1 2 3 5}

注意:子序列是不连续的。

 

假设我们把序列储存在a数组中,并且从下标1开始存储。

 

动态规划解法

定义 f[i] 表示到 a[i] 为止的最长上升子序列的长度。(其中a[i]必须被选中,必须以a[i]结尾)

所以动态转移方程是 f [ i ] = max( f [ j ] ) + 1. ( 1 <= j < i 并且 a [ j ] > a [ i ] )

也就是说 对于位置i来说,只要前面的a[ j ]小于a[ i ],那么就可以转移过来,从中选择一个最大的即可。

代码:

#include<iostream>
using namespace std;

const int maxn=10000;
int a[maxn],f[maxn];
 
int main() {
	int n;
	cin>>n;
	for (int i=1; i<=n; i++)
		cin>>a[i];
		
    for (int i=1; i<=n; i++){  //n个数都得算f[i]
        f[i]=1; //最起码是个1
        for (int j=1; j<i; j++){ //前面的所有j
            if (a[j]<a[i])  //满足小于
                f[i]=max(f[i],f[j]+1);  //选最大的
        }
    }
    int ans=0;
    for (int i=1; i<=n; i++) //统计答案
        if (f[i]>ans) ans=f[i];
    cout<<ans<<endl;
}

时间复杂度:双重循环,所以复杂度是O(n^2).

 

贪心+二分解法

对上述的动态规划算法考虑优化,外层循环,也就是每个数都得计算f[i],这个肯定没有办法再优化了。
所以优化的重点就在于内层循环找 i 前面的比 a[ i ] 小的最大的 f [ j ]。

说到查找,大家第一时间都会想到二分,但是二分要求数组得是有序的,f 数组不满足,那么我们可以使得 f 数组有序吗?

我们先把样例的 f 数组写出来,如下:

我们观察 f 数组划线的两个1,前一个表示的是以a[1]也就是1结尾的最长上升子序列长度是1,后一个表示以a[2]也就是6为结尾的最长上升子序列长度也是1。

这两个1对于后面的转移是有影响的,但是我们发现,只要是6能转移的,1肯定都能转移,所以这个6我们没有必要保存下来。

到这里我们可以总结出,在相同的长度下,我们想尽量让a[i]更小,这样状态更能转移到后面去,这种优化就是贪心思想的体现。比如刚刚说的样例,同样是长度为1,以1结尾比以6结尾好多了,所以我们选择1,而不用6,划线的4和3也是同样的情况。

所以,对于每个长度,我们只需要记录其最小的那个a[i]。 假设我们把这个数组称为g。
g[i]表示长度为 i 的最长上升子序列的最小结尾是 g[ i ]。g数组的长度就是截止当前的最长上升子序列的长度。

比如当前 g 数组有m个。
  如果 a[i] >g[m]  说明可以用 a[i] 继续扩展.
  如果 a[i]<g[m] 对于当前答案没有影响,但是 a[i] 可能会优化g数组,所以我们要在 g 数组中找到第一个小于 a[i] 的数,去优化g数组。因为g数组单调的,可以使用二分查找。

代码:

#include<iostream>
using namespace std;

const int maxn=10000;
int a[maxn],g[maxn];
 
int main() {
	int n;
	cin>>n;
	for (int i=1; i<=n; i++)
		cin>>a[i];
	
	int m=0;
	g[0]=-100000000; //初始化	
    for (int i=1; i<=n; i++){
        if (a[i]>g[m]){ //满足就扩展
        	m++;
        	g[m]=a[i];
		}else{ //否则二分
			int l=0,r=m;
			while (l!=r){
				int mid=(l+r+1)/2;
				if (g[mid]<=a[i]) l=mid;
					else r=mid-1;		
			}
			if (g[l]!=a[i]) g[l+1]=a[i]; //特判下相等的情况
		}
    }
   	cout<<m<<endl; //最长上升子序列的长度就是g数组的长度
}

其中,初始化g[0]为负值,目的是让a[1]一定可以选入。当然你可以选择先在g数组中加入a[1].
二分完毕后,有一个判断语句, if (g[l]!=a[i]) g[l+1]=a[i];  因为我们求得是严格上升的,所以相同的情况下是不能更新g的。
答案就是g数组的长度。

时间复杂度:O(nlogn)。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值