最长递增子序列(Longest Increasing Subsequence)下面我们简记为:LIS。
假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,我们可以很轻松的看出来它的LIS长度为5。
但是如果一个序列太长后,就不能直接看出来了!
下面我们试着逐步找出答案。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len记录目前最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,表示当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时的Len = 1。
然后,把d[2]有序地放到B里,令B[1] = 1,表示长度为1的LIS的最小末尾是1,d[1]=2已经没用了,同样这时的Len = 1。
接着,d[3] = 5,d[3] > B[1],所以令B[1+1] = B[2] = d[3] = 5,表示长度为2的LIS的最小末尾是5,这时候B[1..2] = 1, 5,这时的Len = 2。
接着,d[4] = 3,它正好夹在了1和5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是把5淘汰掉,这时候B[1..2] = 1, 3,这时Len = 2。
继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,这时的Len = 3。
第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4,同样这时Len = 3。
第7个, d[7] = 8,它很大,比4大,于是B[4] = 8,这时的Len = 4。
第8个, d[8] = 9,得到B[5] = 9,这时的Len = 5。
最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,这时的Len = 5。
于是我们知道了LIS的长度为5。
注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。
最后我们发现:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)!
一般的情况下(O(n^2)):
代码(1):
状态设计:F[i]代表以A[i]结尾的LIS的长度
状态转移:F[i]=max{F[j]+1}(1<=j< i,A[j]< A[i])
边界处理:F[i]=1(1<=i<=n)
时间复杂度:O(n^2)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn = 103,INF=0x7f7f7f7f;
int a[maxn],f[maxn];
int n,ans=-INF;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
f[i]=1;
}
for(int i=1;i<=n;i++)
for(int j=1;j<i;j++)
if(a[j]<a[i])
f[i]=max(f[i],f[j]+1);
for(int i=1;i<=n;i++)
ans=max(ans,f[i]);
printf("%d\n",ans);
return 0;
}
代码(2):
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int a[1007],dp[1007],n;
int LIS(int *a)
{
int i,j,ans,m;
dp[1] = 1;
ans = 1;
for(i = 2; i <= n; i++)
{
m = 0;
for(j = 1; j < i; j++)
{
if(dp[j]>m && a[j]<a[i])
m = dp[j];
}
dp[i] = m+1;
if(dp[i]>ans)
ans = dp[i];
}
return ans;
}
二分优化(O(nlogn)):
代码(1):
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int a[40007], dp[40007], n;
int bin(int len,int k)
{
int l = 1, r = len;
while(l <= r)
{
int mid = (l+r)/2;
if(k > dp[mid])
l = mid+1;
else
r = mid-1;
}
return l;
}
int LIS(int *a)
{
int i,j,ans=1;
dp[1] = a[1];
for(i = 2; i <= n; i++)
{
if(a[i] <= dp[1])//如果比最小的还小
j = 1;
else if(a[i] > dp[ans])//如果比最大的还大
j = ++ans;
else
j = bin(ans,a[i]);
dp[j] = a[i];
}
return ans;
}
代码(2):
新建一个low数组,low[i]表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,我们只需要维护low数组,对于每一个a[i],如果a[i] > low[当前最长的LIS长度],就把a[i]接到当前最长的LIS后面,即low[++当前最长的LIS长度]=a[i]。
那么,怎么维护low数组呢?
对于每一个a[i],如果a[i]能接到LIS后面,就接上去;否则,就用a[i]取更新low数组。具体方法是,在low数组中找到第一个大于等于a[i]的元素low[j],用a[i]去更新low[j]。如果从头到尾扫一遍low数组的话,时间复杂度仍是O(n^2)。我们注意到low数组内部一定是单调不降的,所有我们可以二分low数组,找出第一个大于等于a[i]的元素。二分一次low数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn =300003,INF=0x7f7f7f7f;
int low[maxn],a[maxn];
int n,ans;
int binary_search(int *a,int r,int x)
//二分查找,返回a数组中第一个>=x的位置
{
int l=1,mid;
while(l<=r)
{
mid=(l+r)>>1;
if(a[mid]<=x)
l=mid+1;
else
r=mid-1;
}
return l;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
low[i]=INF;//由于low中存的是最小值,所以low初始化为INF
}
low[1]=a[1];
ans=1;//初始时LIS长度为1
for(int i=2;i<=n;i++)
{
if(a[i]>=low[ans])//若a[i]>=low[ans],直接把a[i]接到后面
low[++ans]=a[i];
else //否则,找到low中第一个>=a[i]的位置low[j],用a[i]更新low[j]
low[binary_search(low,ans,a[i])]=a[i];
}
printf("%d\n",ans);//输出答案
return 0;
}
树状数组维护(O(nlogn)):
我们再来回顾O(n^2)DP的状态转移方程:F[i]=max{F[j]+1}(1<=j< i,A[j]< A[i])
我们在递推F数组的时候,每次都要把F数组扫一遍求F[j]的最大值,时间开销比较大。我们可以借助数据结构来优化这个过程。用树状数组来维护F数组(据说分块也是可以的,但是分块是O(n*sqrt(n))的时间复杂度,不如树状数组跑得快),首先把A数组从小到大排序,同时把A[i]在排序之前的序号记录下来。然后从小到大枚举A[i],每次用编号小于等于A[i]编号的元素的LIS长度+1来更新答案,同时把编号小于等于A[i]编号元素的LIS长度+1。因为A数组已经是有序的,所以可以直接更新。有点绕,具体看代码。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn =103,INF=0x7f7f7f7f;
struct Node{
int val,num;
}z[maxn];
int T[maxn];
int n;
bool cmp(Node a,Node b)
{
return a.val==b.val?a.num<b.num:a.val<b.val;
}
void modify(int x,int y)//把val[x]替换为val[x]和y中较大的数
{
for(;x<=n;x+=x&(-x)) T[x]=max(T[x],y);
}
int query(int x)//返回val[1]~val[x]中的最大值
{
int res=-INF;
for(;x;x-=x&(-x)) res=max(res,T[x]);
return res;
}
int main()
{
int ans=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&z[i].val);
z[i].num=i;//记住val[i]的编号,有点类似于离散化的处理,但没有去重
}
sort(z+1,z+n+1,cmp);//以权值为第一关键字从小到大排序
for(int i=1;i<=n;i++)//按权值从小到大枚举
{
int maxx=query(z[i].num);//查询编号小于等于num[i]的LIS最大长度
modify(z[i].num,++maxx);//把长度+1,再去更新前面的LIS长度
ans=max(ans,maxx);//更新答案
}
printf("%d\n",ans);
return 0;
}