您的位置:首页 > 编程语言 > Java开发

关于最长递增子序列问题的求解(LIS)

2011-04-05 02:42 405 查看
本文会介绍几种复杂度不同的算法,并给出实现。

 

一、转化为最长公共子序列(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);
}

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法 float n2 java ini