首先介绍一下LIS和LCS的DP解法O(N^2)
LCS:两个有序序列a和b,求他们公共子序列的最大长度
我们定义一个数组DP[i][j],表示的是a的前i项和b的前j项的最大公共子序列的长度,那么由于是用迭代法,所以计算DP[i][j]前,DP[i-1][j]和DP[i][j-1]就都已经计算出来了,不难理解就可以得出状态转移方程:
DP[i][j] = DP[i-1][j-1] + 1; 如果a[i] == b[j]
MAX(DP[i-1][j], DP[i][j-1]) 如果a[i] != b[j]
LIS:一个a序列,求它的最长上升子序列的最大长度
另外,由于每次都是扫描b数组,那我们就只需要一个DP[2][B]的数组就可以了,DP[0][]依赖于DP[1][],DP[1][]依赖于DP[0][],只要实现动态处理就可以了。
这个状态转移方程也不难给出
DP[j] = MAX(DP[i]) + 1 满足条件a[j] > a[i]
对每一个a[j],枚举一遍a[0]....a[j-1]就可以了
下面再来看看他们的O(nlogn)的解法
首先是LCS,我们把a序列中的每个元素在b中出现的位置保存起来,再按照降序排列,排列后再代入a的每个对应元素,那就转化为了求这个新的序列的最长上升子序列了。如:a[] = {a, b, c,} b[] = {a,b,c,b,a,d},那么a中的a,b,c在b中出现的位置分别就是
{0,4},{1,3},{2}分别按降序排列后代入a序列就是{4,0,2,3,1},之所以要按照降序排列,目的就是为了让每个元素只取到一次。
接下来的问题就是要求最长升序子序列问题了,也就是求LIS。
PS:个人认为这种转化并没有多大意义,因为当数据比较大时,举一个例子来说:a[]= “aaaaaaaaaaa”,b[] = "aaaaaaaaaaa",他们都有n个a,那么a[]中‘a’在b[]出现了n次那么替换之后的a就变为了n-1...1,n-1...1,... ...这样a[]就有了n^2个数了这样不仅是空间,就连时间也比原来n^2要大。所以感觉这种转化还是要慎用。
下面的LIS的O(nlogn)转自于http://hi.baidu.com/fandywang_jlu/item/da673a3d83e2a65980f1a7e1
一、算法思想
算法还是容易想到的,两重循环DP即可。不过如果数据规模最大可以达到几十万甚至更大,经典的O(n^2)的动态规划算法明显会超时。我们需要寻找更好的方法来解决是最长上升子序列问题。以下以最长递增子序列为例进行说明:
先回顾经典的O(n^2)的动态规划算法,设A[i]表示序列中的第i个数,F[i]表示从1到i这一段中以i结尾的最长上升子序列的长度,初始时设 F[i] = 0(i = 1, 2, ..., len(A))。则有动态规划方程:F[i] = max{1, F[j] + 1} (j = 1, 2, ..., i - 1, 且A[j] < A[i])。
现在,我们仔细考虑计算F[i]时的情况。假设有两个元素A[x]和A[y],满足(1)y < x < i (2)A[x] < A[y] < A[i] (3)F[x] = F[y]
此时,选择F[x]和选择F[y]都可以得到同样的F[i]值,那么,在最长上升子序列的这个位置中,应该选择A[x]还是应该选择A[y]呢?
很明显,选择A[x]比选择A[y]要好。因为由于条件(2),在A[x+1] ... A[i-1]这一段中,如果存在A[z],A[x] < A[z] < A[y],则与选择A[y]相比,将会得到更长的上升子序列。
再根据条件(3),我们会得到一个启示:根据F[]的值进行分类。对于F[]的每一个取值k,我们只需要保留满足F[i] = k的所有A[i]中的最小值。设D[k]记录这个值,即D[k] = min{ A[i] } ( F[i] = k )。
注意到D[]的两个特点:
(1) D[k]的值是在整个计算过程中是单调不上升的。//此处需要特别注意!!!关键之所在!
(2) D[]的值是有序的,即D[1] < D[2] < D[3] < ... < D[n]。
利 用D[],我们可以得到另外一种计算最长上升子序列长度的方法。设当前已经求出的最长上升子序列长度为len。先判断A[i]与D[len],若A[i] > D[len],则将A[i]接在D[len]后将得到一个更长的上升子序列,len = len + 1,D[len+1] = A[i];否则,在D[1]..D[len]中,找到最大的j,满足D[j] < A[i].令k = j + 1,则有D[j] < A[i] <= D[k],将A[i]接在D[j]后将得到一个更长的上升子序列,同时更新D[k] = A[i].最后,len即为所要求的最长上升子序列的长度。
在上述算法中,若使用朴素的顺序查找在D[1]..D[len]查找,由于 共有O(n)个元素需要计算,每次计算时的复杂度是O(n),则整个算法的时间复杂度为O(n^2),与原来的算法相比没有任何进步.但是由于D[]的特 点(2),我们在D[]中查找时,可以使用二分查找高效地完成,则整个算法的时间复杂度下降为O(nlogn),有了非常显著的提高.需要注意的 是,D[]在算法结束后记录的并不是一个符合题意的最长上升子序列.
这个算法还可以扩展到整个最长子序列系列问题,整个算法的难点在于二分查找的设计,需要非常小心注意.
二、实现
// By Fandywang 2008.7.21
// Call: LIS(a, n); 求最大递增/上升子序列(如果为最大非降子序列,只需把上面的注释部分给与替换)
const int N = 1001;
int a[N], f[N], d[N]; // d[i]用于记录a[0...i]的最大长度
int bsearch(const int *f, int size, const int &a)
{
int l=0, r=size-1;
while( l <= r )
{
int mid = (l+r)/2;
if( a > f[mid-1] && a <= f[mid] ) return mid; // >&&<= 换为: >= && <
else if( a < f[mid] ) r = mid-1;
else l = mid+1;
}
}
int LIS(const int *a, const int &n){
int i, j, size = 1;
f[0] = a[0]; d[0] = 1;
for( i=1; i < n; ++i ){
if( a[i] <= f[0] ) j = 0; // <= 换为: <
else if( a[i] > f[size-1] ) j = size++; // > 换为: >=
else j = bsearch(f, size, a[i]);
f[j] = a[i]; d[i] = j+1;
}
return size;
}
三、学以致用
JOJ 1829 Candies 最大非下降子序列
JOJ 2162 Inuyasha And the Monsters 求和最大的非下降子序列 (这个我用的是O(n^2)的算法做的,居然还那个了第一...)
JOJ 2529 Chorus 最大上升子序列+最大下降子序列
JOJ 1048 Wooden Sticks 先自定义排下序(先按l从小到大, l相等再按w从小到大), 然后求w的最大下降子序列
POJ 3636 Nested Dolls 与JOJ1048类似 w从小到大,w相等h从大到小,然后求h最大非升子序列
至于LCIS,有了上面的比较深刻的理解,相信应该会比较好理解(转自百度文库)最长公共上升子序列(LCIS)的O(n^2)算法
预备知识:动态规划的基本思想,LCS,LIS。
问题:字符串a,字符串b,求a和b的LCIS(最长公共上升子序列)。
首先我们可以看到,这个问题具有相当多的重叠子问题。于是我们想到用DP搞。DP的首要任务是什么?定义状态。
1定义状态F[i][j]表示以a串的前i个字符b串的前j个字符且以b[j]为结尾构成的LCIS的长度。
为什么是这个而不是其他的状态定义?最重要的原因是我只会这个,还有一个原因是我知道这个定义能搞到平方的算法。而我这只会这个的原因是,这个状态定义实在是太好用了。这一点我后面再说。
我们来考察一下这个这个状态。思考这个状态能转移到哪些状态似乎有些棘手,如果把思路逆转一下,考察这个状态的最优值依赖于哪些状态,就容易许多了。这个状态依赖于哪些状态呢?
首先,在a[i]!=b[j]的时候有F[i][j]=F[i-1][j]。为什么呢?因为F[i][j]是以b[j]为结尾的LCIS,如果F[i][j]>0那么就说明a[1]..a[i]中必然有一个字符a[k]等于b[j](如果F[i][j]等于0呢?那赋值与否都没有什么影响了)。因为a[k]!=a[i],那么a[i]对F[i][j]没有贡献,于是我们不考虑它照样能得出F[i][j]的最优值。所以在a[i]!=b[j]的情况下必然有F[i][j]=F[i-1][j]。这一点参考LCS的处理方法。
那如果a[i]==b[j]呢?首先,这个等于起码保证了长度为1的LCIS。然后我们还需要去找一个最长的且能让b[j]接在其末尾的LCIS。之前最长的LCIS在哪呢?首先我们要去找的F数组的第一维必然是i-1。因为i已经拿去和b[j]配对去了,不能用了。并且也不能是i-2,因为i-1必然比i-2更优。第二维呢?那就需要枚举b[1]..b[j-1]了,因为你不知道这里面哪个最长且哪个小于b[j]。这里还有一个问题,可不可能不配对呢?也就是在a[i]==b[j]的情况下,需不需要考虑F[i][j]=F[i-1][j]的决策呢?答案是不需要。因为如果b[j]不和a[i]配对,那就是和之前的a[1]..a[j-1]配对(假设F[i-1][j]>0,等于0不考虑),这样必然没有和a[i]配对优越。(为什么必然呢?因为b[j]和a[i]配对之后的转移是max(F[i-1][k])+1,而和之前的i`配对则是max(F[i`-1][k])+1。显然有F[i][j]>F[i`][j],i`>i)
于是我们得出了状态转移方程:
a[i]!=b[j]: F[i][j]=F[i-1][j]
a[i]==b[j]: F[i][j]=max(F[i-1][k])+1 1<=k<=j-1&&b[j]>b[k]
不难看到,这是一个时间复杂度为O(n^3)的DP,离平方还有一段距离。
但是,这个算法最关键的是,如果按照一个合理的递推顺序,max(F[i-1][k])的值我们可以在之前访问F[i][k]的时候通过维护更新一个max变量得到。怎么得到呢?首先递推的顺序必须是状态的第一维在外层循环,第二维在内层循环。也就是算好了F[1][len(b)]再去算F[2][1]。
如果按照这个递推顺序我们可以在每次外层循环的开始加上令一个max变量为0,然后开始内层循环。当a[i]>b[j]的时候令max=F[i-1][j]。如果循环到了a[i]==b[j]的时候,则令F[i][j]=max+1。
最后答案是F[len(a)][1]..F[len(a)][len(b)]的最大值。
参考代码:
1 #include<cstdio> 2 #include<cstring> 3 int f[1005][1005],a[1005],b[1005],i,j,t,n1,n2,max; 4 int main() 5 { 6 scanf("%d",&t); 7 while(t--) 8 { 9 scanf("%d%d",&n1,&n2); 10 for(i=1;i<=n1;i++) scanf("%d",&a[i]); 11 for(i=1;i<=n2;i++) scanf("%d",&b[i]); 12 memset(f,0,sizeof(f)); 13 for(i=1;i<=n1;i++) 14 { 15 max=0; 16 for(j=1;j<=n2;j++) 17 { 18 f[i][j]=f[i-1][j]; 19 if (a[i]>b[j]&&max<f[i-1][j]) max=f[i-1][j]; 20 if (a[i]==b[j]) f[i][j]=max+1; 21 } 22 } 23 max=0; 24 for(i=1;i<=n2;i++) if (max<f[n1][i]) max=f[n1][i]; 25 printf("%d ",max); 26 } 27 }
其实还有一个很风骚的一维的算法。在此基础上压掉了一维空间(时间还是平方)。i循环到x的时候,F[i]表示原来F[x][j]。之所以可以这样,是因为如果a[i]!=b[j],因为F[x][j]=F[x-1][j]值不变,F[x]不用改变,沿用过去的就好了,和这个比较维护更新得到的max值依然是我们要的。而a[i]==b[j]的时候,就改变F[x]的值好了。具体结合代码理解。
参考代码:
1 #include<cstdio> 2 #include<cstring> 3 int f[1005],a[1005],b[1005],i,j,t,n1,n2,max; 4 int main() 5 { 6 scanf("%d",&t); 7 while(t--) 8 { 9 scanf("%d%d",&n1,&n2); 10 for(i=1;i<=n1;i++) scanf("%d",&a[i]); 11 for(i=1;i<=n2;i++) scanf("%d",&b[i]); 12 memset(f,0,sizeof(f)); 13 for(i=1;i<=n1;i++) 14 { 15 max=0; 16 for(j=1;j<=n2;j++) 17 { 18 if (a[i]>b[j]&&max<f[j]) max=f[j]; 19 if (a[i]==b[j]) f[j]=max+1; 20 } 21 } 22 max=0; 23 for(i=1;i<=n2;i++) if (max<f[i]) max=f[i]; 24 printf("%d ",max); 25 } 26 }
最长公共上升子序列(LCIS)的平方算法@我们都爱刘汝佳
2011-2-18