最長公共子序列(LCS)、最長遞增子序列(LIS)、最長遞增公共子序列(LICS)


最長公共子序列(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]。

問題的遞歸式寫成:

 

recursive formula

回溯輸出最長公共子序列過程:

flow

 

算法分析:
由於每次調用至少向上或向左(或向上向左同時)移動一步,故最多調用(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\n",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\n",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\n",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:\n",t++);
47         if(maxn==1)    printf("My king, at most 1 road can be built.\n\n");//注意這里road的復數問題,超級坑 
48         else    printf("My king, at most %d roads can be built.\n\n",maxn);
49         memset(dp,0,sizeof(dp));
50     }
51     return 0;
52 }

最長遞增公共子序列(LICS)

轉載:傳送門

那么以ZOJ的2432為例、

最長公共上升子序列(LCIS)的O(n^2)算法

預備知識:動態規划的基本思想,LCSLIS

問題:字符串a,字符串b,求abLCIS(最長公共上升子序列)。

首先我們可以看到,這個問題具有相當多的重疊子問題。於是我們想到用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]呢?首先,這個等於起碼保證了長度為1LCIS。然后我們還需要去找一個最長的且能讓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("\n");
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("\n");
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\n",ans);
71         int x=n,y=flag;
72         count=0;
73         if(ans>0)
74             out(x,y);
75         if(t)    printf("\n");
76     }
77     return 0;
78 }

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM