本文会介绍几种复杂度不同的算法,并给出实现。
一、转化为最长公共子序列(LCS)求解
二、普通dp
三、LIS的O(nlogn)算法
问题描述:
设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。
很明显注意到子序列跟子串是有区别的。
一、转化为最长公共子序列(LCS)求解 , O(n2)
设序列X=<b1,b2,…,bn>是对序列L=<a1,a2,…,an>按递增排好序的序列。那么显然X与L的最长公共子序列即为L的最长递增子序列。这样就把求最长递增子序列的问题转化为求最长公共子序列问题LCS了。
最长公共子序列问题用动态规划的算法可解。可以参考这道题的分析。
二、普通dp, O(n2)
(一)、记忆化搜索解dp
①从后向前规划
d(i)表示L中以ai为末元素的最长递增子序列的长度
d[i] = max{dj+1 | ai>aj, j<i};
int a[MAXN]; //元素 int d[MAXN]; //状态 int vis[MAXN]; //记忆化搜索,访问标记组 int dp(int cur) { //不用停止条件,找不到相应的 i 自动退出 if(vis[cur]) return d[cur]; vis[cur] = 1; int max = 1; //若找不到相应的 i最少也是1(自己构成lis)。 for(int i=0; i<cur; i++) if(a[cur]>a[i]) //a[cur]>a[i] | 0<=i<cur { int t = dp(i)+1; max = max>t? max: t; //max更新为 max{di+1} } return d[cur] = max; } int main() { memset(vis, 0, sizeof(vis)); int ans=0; for(int i=0; i<n; i++) //ans为最大的d值 ans = ans>dp(i)? ans: dp(i); }
②从前向后规划
从前向后找,但是di为前元素,注意(*)标记处的不同
d[i] = max{dj+1 | ai<aj, j = i+1~n-1};
在某些情况下会要使用这种规划 ,如做过的一道最长增减子序列的题,由于开始的时候必须先增。
int dp(int cur) { if(vis[cur]) return d[cur]; vis[cur] = 1; int max = 1; //若找不到相应的 i最少也是1(自己构成lis)。 for(int i=cur+1; i<n; i++) if(a[cur]<a[i]) //!依次遍历后面的元素---------(*) { int t = dp(i)+1; max = max>t? max: t; } return d[cur] = max; }
(二)、递推法解dp
我们依次遍历整个序列,每一次求出从第一个数到当前这个数的最长上升子序列,
直至遍历到最后一个数字为止,然后再取d数组里最大的那个即为整个序列的最长上升子序列。
我们用d[i]来存放序列0~i-1的最长上升子序列的长度,那么d[i]=max{d[j])+1 | a[i]>a[j] , j∈[0, i-1]}.
显然d[0]=1,我们从i=1开始遍历后面的元素即可。
int lis() { memset(d, 0, sizeof(d)); d[0]=1; for(int i=1; i<n; i++) //d[i] { int max=0; //这里max初始化为0而不是1, 是为了能统一进行d[i] = max+1; 操作, 见(1) for(int j=0; j<i; j++) if(a[i]>a[j])// a[i]>a[j] , j∈[0, i-1] { max = max>d[j]? max: d[j]; } d[i] = max+1; //----------(1)。 } int ans = 0; for(int i=0; i<n; i++) ans = ans>d[i]? ans: d[i]; return ans; }
三、LIS的O(nlogn)算法
step 1
在以上算法中,计算d[i]时,都要找出最大的d[j](j<i,aj<ai)来。
由于d数组无序,只能顺序查找满足的d[j],如果能让d组有序
就可以使用二分查找,这样算法复杂度就可以降低到O(nlogn)。
于是构造F(d[i]) = a[i]. 表示长度为d[i]的子序列最小末尾元素为a[i]。
则问题的解为最后更新到的F下标(最大长度)。
显然F是递增的。
则在上面的算法中,要找最大的d[j]时,只要在F中用二分查找法
找到满足j<i且F[d[j]]=aj<ai的最大的j。(此时F[d[j]+1] > a[i]。)
然后将F[d[j]+1]置为ai,即F[d[i]] = a[i]。
注意这里咯,是不是d[i] = max{d[j]+1}了!
有关证明参考这里(http://www.bccn.net/Article/kfyy/vc/jszl/200709/6258.html)。
step 2
于是,去掉数组d
记F[k]为长度为k的最长子序列的最后一个数最小可以是多少。
下面引用两个别人的代码,本质一样:
①
在二分查找时,一直更新F内容,设此时b的总长度为k.
若1. a[i] >= F[k], 则F[k+1] = a[i];
若2. a[i] < F[k], 则在F[1..k]中用二分搜索大于a[i]的最小值,返回其位置pos,
然后更新b[pos]=a[i]。
int bSearch(int num, int k) { int low=1, high=k; while(low<=high) { int mid=(low+high)/2; if(num>=b[mid]) low=mid+1; else high=mid-1; } return low; } int LIS() { int low = 1, high = n; int k = 1; b[1] = p[1]; for(int i=2; i<=n; ++i) { if(p[i]>=b[k]) b[++k] = p[i]; else { int pos = bSearch(p[i], k); b[pos] = p[i]; } } return k; }
②
在计算d(i)时,在数组F中用二分查找法找到满足j<i且F[d(j)]=aj<ai的最大的j,并将F[d[j]+1]置为ai。
Java:
lis1(float[] L) { int n = L.length; float[] B = new float[n+1];//数组B; B[0]=-10000;//把B[0]设为最小,假设任何输入都大于-10000; B[1]=L[0];//初始时,最大递增子序列长度为1的最末元素为a1 int Len = 1;//Len为当前最大递增子序列长度,初始化为1; int p,r,m;//p,r,m分别为二分查找的上界,下界和中点; for(int i = 1;i<n;i++) { p=0;r=Len; while(p<=r)//二分查找最末元素小于ai+1的长度最大的最大递增子序列; { m = (p+r)/2; if(B[m]<L[i]) p = m+1; else r = m-1; } B[p] = L[i];//将长度为p的最大递增子序列的当前最末元素置为ai+1; if(p>Len) Len++;//更新当前最大递增子序列长度; } System.out.println(Len); }