前言
最长递增子序列(LIS)问题是指,在一个给定的数值序列中,找到一个子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能地大。最长递增子序列中的元素在原序列中不一定是连续的。解决最长递增子序列问题的算法最低要求O(nlogn)的时间复杂度。
基本思想
动态规划+二分法
通过一个数组dp[k]来缓存长度为k的递增子序列的最末元素,若有多个长度为k的递增子序列,则记录最小的那个。
首先len=1, dp[0]=seq[0];
然后对seq[i]:若seq[i]>dp[len],那么len++,dp[len]=seq[i];
否则,从dp[0]到dp[len-1]中找到一个j,满足dp[j-1]<seq[i]<d[j],然后更新dp[j]=seq[i]。
最终len即为最长递增子序列LIS的长度。
因为在dp中插入数据是有序并且只需替换不用挪动,因此我们可以使用二分查找,将每一个数字的插入时间优化为O(logn),于是算法的时间复杂度从使用排序+LCS的O(n^2)降低到了O(nlogn)。
例子
假设存在一个序列seq[0...8]=2 1 5 3 6 4 8 9 7,可以看出它的LIS长度为5。
下面试着一步步地找出它:
定义一个数组dp,然后令i=0到8逐个考察序列seq,此外用一个变量len来记录现在LIS的长度。
首先,把seq[0]=2有序地放入dp中,令dp[0]=2,len=1
然后,把seq[1]=1有序地放入dp中,令dp[0]=1,len=1
接着,把seq[2]=5有序地放入dp中,因为seq[2]>dp[0],所以令dp[1]=5,len=2
再来,seq[3]=3,正好在dp[0]到dp[1]之间,替换掉dp[1],令dp[1]=3,len=2
继续,把seq[4]=6有序地放入dp中,因为seq[4]>dp[1],所以令dp[2]=6,len=3
然后,seq[5]=4,正好在dp[1]到dp[2]之间,替换掉dp[2],令dp[2]=4,len=3
接着,把seq[6]=8有序地放入dp中,因为seq[6]>dp[2],所以令dp[3]=8,len=4
再来,把seq[7]=9有序地放入dp中,因为seq[7]>dp[3],所以令dp[4]=9,len=5
最后,seq[8]=7,正好在dp[2]到dp[3]之间,替换掉dp[3],令dp[3]=7,len=5
注意:dp[0..4]=1 3 4 7 9不是LIS,它只是存储的对应长度LIS的最小末尾。
完整代码
#include<iostream>
using namespace std;
int dp[500];
int seq[500];
int slen;
int LISlen;
int binSearch(int array[], int arraySize, int value)
{
int left = 0;
int right = arraySize-1;
int mid;
while(left<right)
{
mid = (right+left)/2;
if(array[mid]<=value)
left = mid+1;
else
right = mid-1;
}
return left;
}
void LIS()
{
dp[0] = seq[0];
LISlen = 1;
int k = 0;
for(int i=1;i<slen;++i)
{
if(seq[i]>dp[LISlen-1])
{
dp[LISlen++] = seq[i];
}
else
{
int pos = binSearch(dp, LISlen, seq[i]);
dp[pos] = seq[i];
}
}
cout<<LISlen<<endl;
}
int main()
{
cin>>slen;
for(int i=0;i<slen;++i)
cin>>seq[i];
LIS();
/* input: 16
0 8 4 12 2 10 6 14 1 9 5 13 3 11 7 15
output: 6 */
return 0;
}
测试案例
输入
16
0 8 4 12 2 10 6 14 1 9 5 13 3 11 7 15
输出
6
参考
博客-Felix021