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;//更新
}
}
}
}
i
从1
到N
,表示的是:需要更新arr[1]
和arr[N-1]
这N-1
个数下的最长上升子序列长度。
j
从0
到i-1
表示的就是:每次循环都从0
到i-1
这j
个树中找出一个最大的值更新,从而能够得以保证得到的值最终是最大的。这个是没啥难度的,对吧!
但是看看这个算法的时间复杂度是 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