最长公共子序列(LCS)
【问题】 求两字符序列的最长公共字符子序列
问题描述:字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列<i0,i1,…,ik-1>,使得对所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。
考虑最长公共子序列问题如何分解成子问题,设A=“a0,a1,…,am-1”,B=“b0,b1,…,bm-1”,并Z=“z0,z1,…,zk-1”为它们的最长公共子序列。不难证明有以下性质:
(1) 如果am-1=bn-1,则zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一个最长公共子序列;
(2) 如果am-1!=bn-1,则若zk-1!=am-1,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列;
(3) 如果am-1!=bn-1,则若zk-1!=bn-1,蕴涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列。
这样,在找A和B的公共子序列时,如有am-1=bn-1,则进一步解决一个子问题,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一个最长公共子序列;如果am-1!=bn-1,则要解决两个子问题,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一个最长公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一个最长公共子序列,再取两者中较长者作为A和B的最长公共子序列。
求解:
引进一个二维数组c[][],用c[i][j]记录X[i]与Y[j] 的LCS 的长度,b[i][j]记录c[i][j]是通过哪一个子问题的值求得的,以决定搜索的方向。
我们是自底向上进行递推计算,那么在计算c[i,j]之前,c[i-1][j-1],c[i-1][j]与c[i][j-1]均已计算出来。此时我们根据X[i] = Y[j]还是X[i] != Y[j],就可以计算出c[i][j]。
问题的递归式写成:
回溯输出最长公共子序列过程:
算法分析:
由于每次调用至少向上或向左(或向上向左同时)移动一步,故最多调用(m + n)次就会遇到i = 0或j = 0的情况,此时开始返回。返回时与递归调用时方向相反,步数相同,故算法时间复杂度为Θ(m + n)。
那么以HDU 1159来做演示、
1 #include<cstring> 2 #include<cmath> 3 #include<cstring> 4 #include<cstdio> 5 const int qq=1e3+10; 6 char x[qq],y[qq]; 7 int dp[qq][qq]; 8 int main() 9 { 10 while(~scanf("%s%s",x+1,y+1)){ 11 x[0]=y[0]='.'; 12 int len=strlen(x)>strlen(y)?strlen(x):strlen(y); 13 // printf("%d %d ",strlen(x),strlen(y)); 14 for(int i=0;i<=len;++i) 15 dp[i][0]=dp[0][i]=0; 16 for(int j,i=1;i<strlen(x);++i) 17 for(j=1;j<strlen(y);++j) 18 if(x[i]==y[j]) 19 dp[i][j]=dp[i-1][j-1]+1; 20 else 21 dp[i][j]=dp[i-1][j]>dp[i][j-1]?dp[i-1][j]:dp[i][j-1]; 22 printf("%d ",dp[strlen(x)-1][strlen(y)-1]); 23 } 24 return 0; 25 }
最长递增子序列(LIS)
以POJ 2533来讲解吧、
现在给你一个序列,要你求最长上升子序列,那么把这个序列排一个序,然后和原有序列进行LCS 是不是就解决问题了呢?
当然还有dp思路了、
dp[ i ]以序列中第i个元素结尾的最长上升子序列的长度
那么状态转移方程为:if (a[i] > a[j]) dp[i] = MAX (dp[i], dp[j] + 1);
1 #include<cstdio> 2 #include<cmath> 3 #include<cstring> 4 const int qq=1005; 5 int dp[qq]; 6 int num[qq]; 7 int main() 8 { 9 int n; 10 while(~scanf("%d",&n)){ 11 memset(dp,0,sizeof(dp)); // 5 6 7 8 1 12 for(int i=0;i<n;++i) 13 scanf("%d",&num[i]); 14 dp[0]=1; 15 int x=0; 16 for(int j,i=0;i<n;++i){ 17 int maxn=0; 18 for(j=0;j<i;++j) //这里利用了递推的原理、 19 if(num[i]>num[j]) //由前面的最长递增子序列推出后面的最长递增子序列、 20 maxn=maxn>dp[j]?maxn:dp[j]; 21 dp[i]=maxn+1; 22 if(dp[i]>x) x=dp[i]; //x记录的是当前的最大值、 23 } 24 printf("%d ",x); 25 } 26 return 0; 27 }
对于LIS还有一种O(n*logn)的做法、
这里以HDU 1025为例、
转载自 kuangbin
假设要寻找最长上升子序列的序列是a[n],然后寻找到的递增子序列放入到数组b中。
(1)当遍历到数组a的第一个元素的时候,就将这个元素放入到b数组中,以后遍历到的元素都和已经放入到b数组中的元素进行比较;
(2)如果比b数组中的每个元素都大,则将该元素插入到b数组的最后一个元素,并且b数组的长度要加1;
(3)如果比b数组中最后一个元素小,就要运用二分法进行查找,查找出第一个比该元素大的最小的元素,然后将其替换。
在这个过程中,只重复执行这两步就可以了,最后b数组的长度就是最长的上升子序列长度。例如:如该数列为:
5 9 4 1 3 7 6 7
那么:
5 //加入
5 9 //加入
4 9 //用4代替了5
1 9 //用1代替4
1 3 //用3代替9
1 3 7 //加入
1 3 6 //用6代替7
1 3 6 7 //加入
最后b中元素的个数就是最长递增子序列的大小,即4。
要注意的是最后数组里的元素并不就一定是所求的序列,
例如如果输入 2 5 1
那么最后得到的数组应该是 1 5
而实际上要求的序列是 2 5
1 #include<iostream> 2 #include<algorithm> 3 #include<cstdio> 4 #include<cstring> 5 const int qq=500000+5; 6 int city[qq]; 7 int dp[qq]={0}; 8 int search(int x,int l,int r) 9 { //二分查找找到一个位置,使得x>dp[i-1], 并且x<dp[i],并用x代替dp[i] 10 int m; 11 while(l<=r){ 12 m=(l+r)>>1; 13 if(x>=dp[m]) l=m+1; 14 else r=m-1; 15 } 16 return l; 17 } 18 int DP(int n) 19 { 20 int i,len; 21 dp[1]=city[1]; 22 len=1; 23 for(int i=2;i<=n;++i){ 24 if(city[i]>=dp[len]){ 25 len=len+1; 26 dp[len]=city[i]; 27 } 28 else{ 29 int temp=search(city[i],1,len); 30 dp[temp]=city[i]; 31 } 32 } 33 return len; 34 } 35 int main() 36 { 37 int n; 38 int t=1; 39 while(~scanf("%d",&n)){ 40 int a,b; 41 for(int i=0;i<n;++i){ 42 scanf("%d%d",&a,&b); 43 city[a]=b; 44 } 45 int maxn=DP(n); 46 printf("Case %d: ",t++); 47 if(maxn==1) printf("My king, at most 1 road can be built. ");//注意这里road的复数问题,超级坑 48 else printf("My king, at most %d roads can be built. ",maxn); 49 memset(dp,0,sizeof(dp)); 50 } 51 return 0; 52 }
最长递增公共子序列(LICS)
转载:传送门
那么以ZOJ的2432为例、
最长公共上升子序列(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<cmath> 2 #include<cstdio> 3 #include<cstring> 4 const int qq=505; 5 int a[qq],b[qq]; 6 int dp[qq][qq]; 7 int prex[qq][qq]; 8 int prey[qq][qq]; 9 int count,ans; 10 void out(int x,int y) //递归输出路径、 11 { 12 if(dp[x][y]==0) return; 13 int xx=prex[x][y]; 14 int yy=prey[x][y]; 15 out(xx,yy); 16 if(dp[x][y]!=dp[xx][yy] && y!=0){ 17 printf("%d",b[y]); 18 count++; 19 if(count<ans) printf(" "); 20 else printf(" "); 21 } 22 } 23 int main() 24 { 25 int t;scanf("%d",&t); 26 while(t--){ 27 int n; 28 scanf("%d",&n); 29 for(int i=1;i<=n;++i) 30 scanf("%d",&a[i]); 31 int m; 32 scanf("%d",&m); 33 for(int i=1;i<=m;++i) 34 scanf("%d",&b[i]); 35 memset(prex,-1,sizeof(prex)); 36 memset(prey,-1,sizeof(prey)); 37 memset(dp,0,sizeof(dp)); 38 for(int j,i=1;i<=n;++i){ 39 int maxn=0; 40 int x,y;x=y=0; 41 for(j=1;j<=m;++j){ 42 dp[i][j]=dp[i-1][j]; //先更新状态,如果后面会更新的话再去更新 43 prex[i][j]=i-1; 44 prey[i][j]=j; 45 if(a[i]>b[j] && maxn<dp[i-1][j]){ //我开始一直没想通为什么要在a[i]>b[j]的时候才更新值 46 maxn=dp[i-1][j];// 其实你想想dp[i][j]中i,j都有什么含义、 47 x=i-1; // 这个if里面更新出来的maxn只有在满足a[i]==b[j]的时候才有用、 48 y=j; // 这里更新出来的值就是为了保证当a[i]==b[j]的时候 49 } //所更新出来的最大值是一个递增的子序列、 50 else if(a[i]==b[j]){ 51 dp[i][j]=maxn+1; 52 prex[i][j]=x; 53 prey[i][j]=y; 54 } 55 } 56 } 57 for(int i=1;i<=n;++i){ 58 for(int j=1;j<=m;++j) 59 printf("%d ",dp[i][j]); 60 printf(" "); 61 } 62 ans=0; 63 int flag=-1; 64 for(int i=1;i<=m;++i){ 65 if(ans<dp[n][i]){ 66 flag=i; 67 ans=dp[n][i]; 68 } 69 } 70 printf("%d ",ans); 71 int x=n,y=flag; 72 count=0; 73 if(ans>0) 74 out(x,y); 75 if(t) printf(" "); 76 } 77 return 0; 78 }