最长不下降子序列的O(nlogn)算法

0.总结

  • Get to the key point firstly, the article comes from LawsonAbs!
    本文主要讲的知识点有:
  • 什么是 LNDS
  • 如何以 O(n^2^)的复杂度求出 LNDS【longest non descending subsequence】?
  • 如何以O(NlogN)的复杂度求出 LNDS

1.问题

给出一串序列,求出该序列中最长的不下降(即非严格递增顺序)的子序列长度。

2.分析

在使用DP之前,需要记住的是,是否满足如下两个特性:

  • 最优子结构
  • 重叠子问题

2.1 最优子结构

如果一个数列A是数列B的最长上升子序列,那么相应的在数列A和数列B中去掉数列A中的某个数字之后,A剩下的序列肯定也是B剩下序列中最长的子序列 (或之一)

举例如下:

5
B:1 4 3 5 7
A: 1 4 5 7(最长上升子序列)

去掉数字7之后,数列A,B相应的变成如下的样子:

B: 1 4 3 5
A: 1 4 5

可以看到子序列A仍然是序列B的最长子序列
或者如下例:

5
1 4 3 5 2
(A: 1 4 5)
(B: 1 4 3 5 2) 

去掉数字5之后,数列A和数列B相应的变成如下样子:

(A: 1 4 )
(B: 1 4 3 2) 

可以看到子序列A仍然是序列B的最长子序列。

2.2 重叠子问题

(将上面的数列A和数列B去掉某一个数之后与未去之前具有相同的问题性质,也就是子问题。)
综上所述,可以使用动态规划算法。那么该怎么实现呢?
我们自底向上更新数组 dp[maxn] 即可。主要使用到的数组介绍如下:

  • arr[maxn]用以存储待检测数列的值
  • dp[maxn],其中dp[i] 表示的就是arr[i]这个数之前(包括该数)的最大上升子序列长度 。

3.复杂度为O(N*N)的算法

  • step1:初始化 dp[maxn] 的值为1;表示的是 每个数的初始情况下的最大上升子序列长度为1 【这个是毫无疑问的】
  • step2:双层for循环,一遍遍的更新 dp[i] 的值即可。如下所示:
for(i = 1;i< N;i++){
	for(j = 0;j < i;j++){//从i的下一个数开始 
		if(arr[i] > arr[j]) {
			if(dp[i] < dp[j] + 1){//如果 
				dp[i] = dp[j] + 1;//更新 
			}
		}
	}
} 

i1N,表示的是:需要更新arr[1]arr[N-1]N-1个数下的最长上升子序列长度。
j0i-1表示的就是:每次循环都从0i-1j个树中找出一个最大的值更新,从而能够得以保证得到的值最终是最大的。这个是没啥难度的,对吧!

但是看看这个算法的时间复杂度是 O(n2),如果数据集较大的话,是过不了测试的。比如络谷的一道题,(具体的题号忘记了)只有以O(nlogn)的复杂度才能过掉测试。那么该怎么优化呢?方法很简单,看下面的分析。

4.复杂度为O(NlogN)的算法

因为最长不下降子序列是从左往右看的,我们规定
tail[i]表示的是长度为i的最长不下降子序列的结尾元素的最小值。例如:在序列(1 2 3)中,分别有如下的最长不下降子序列:

长度为1 的 LNDS:(1) (2) (3) =>但是1,2,3中1最小,所以 tail[1] = 1
长度为2 的 LNDS:(1 2)(1 3)(2 3)=> tail[2] = 2
长度为3 的 LNDS:(1 2 3)=> tail[3] = 3

可以看到 tail数组的值是单调递增 的!我们就可以利用这个单调递增做文章了!对于这个序列,我们的 LNDS 的长度就是 tail 数组取非零值的最后一个下标 3。在往下继续阅读时,请确保已经了解上面这个知识!

上面这个样例比较简单,再来分析一个复杂的情况:
如果此时的序列是(1 2 3 2 5 4),我们再来分析一下:

长度为1 的 LNDS:(1) (2) (3) => tail[1] = 1
长度为2 的 LNDS:(1 2)(1 3)(2 3)=> tail[2] = 2
长度为3 的 LNDS:(1 2 3)=> tail[3] = 3

接着判断数字2,发现数字2 比tail[3] = 3小,那此时怎么办?我们就从tail数组中找出第一个大于等于2的数字,将其替换成2即可。【为什么这么做?因为找到的那个数字比现在的这个2地位还要低,因为我们用1 2 2就可以构成一个长度为3的LNDS,所以就不想用1 2 3构成长度为3的LNDS,因为前者对于后序还有2的序列,可以变成长度为4的LNDS,但是后者就不行了。】正是基于这个思想,就有了复杂度为O(NlogN)的算法。

这里给出上面两种方法的代码。

#include<iostream>

using namespace std;
const int maxN = 105; 
int n;
int arr[maxN];//初始序列

//使用O(n^2)的方法 
void getLIS1(){
	int f[maxN];//f[i]表示 arr[i]的最长不下降子序列的长度	
	fill(f,f+maxN,1);//初始化为1 
	int res = 0;	
	for(int i =1;i< n;i++){
		for(int j = 0;j<i;j++){
			if(arr[i] >= arr[j]){//如果当前的数不小于 arr[j]
				f[i] = max(f[i],f[j]+1);				
			}			
		}
		res = max(res,f[i]);
	} 	
	cout << res <<"\n"; 
}

//使用O(nlogn)的方法
void getLIS2(){
	int tail[maxN ];//tail[i]表示长度为i的LNDS中结尾的元素 
	fill(tail,tail+maxN,0);
	int cnt = 1;
	tail[cnt] = arr[0];//第一个长度为1的最长不下降子序列的结尾元素是arr[0] 
	for(int i = 1;i< n;i++){
		if(arr[i] >= tail[cnt]){
			tail[++cnt] = arr[i];
		}
		else{//二分找出最小的 
			int idx= lower_bound(tail+1,tail+cnt+1,arr[i]) - tail;
			tail[idx] = arr[i];//换掉这个元素 
		}
	} 
	
	cout << cnt<<"\n"; 	
}

//计算最长不下降子序列 
int main(){
	cin >> n;
	for(int i = 0;i< n;i++){
		cin >> arr[i];
	}
	
	getLIS1(); 
	getLIS2();
}

再给出一些测试用例。

5
1 2 5 3 4

5
1 2 2 3 4

5
1 7 5 9 6

8
9 8 3 7 4 9 5 2
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

说文科技

看书人不妨赏个酒钱?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值