最长公共子序列
最长公共子序列(Longest Common Sequence,LCS)问题是典型的适用于动态规划求解的问题。LCS的定义是:
给定一个串,以及另外一个串
,如果存在一个单调增的序列
,对于所有
,有
,则称
是的一个子序列。如果对于两个串
,
,
既是
的子序列,又是
的子序列,那么就称
是
与
的公共子序列,LCS就是指所有子序列中最长的那个子序列(可能有多个)。
使用动态规划求解LCS时,首先我们需要找出递推公式。令,
,并设
为它们的LCS。我们可以看到:
(1)如果,并且
,那么
是
与
的LCS;
(2)如果,并且
,那么是
与
的LCS;
(3)如果,并且
,那么是
与
的LCS;
上述3个性质不再证明,读者如果有兴趣,可以阅读算法导论的相关内容。根据上述3个性质,我们可以很容易地写出递推公式。设m[i,j]为串与
的LCS的长度,则
我使用了C++进行了实现,包括了自底向上的构建方法(solve函数)与自顶向下的递归方法(solve1函数)。m数组记录了LCS的长度,b数组则记录了LCS的路径,其中b[i,j]为1,表示(1)的情形,b[i,j]为2表示(2)的情形,b[i,j]为3表示(3)的情形。但是只有当b[i,j]为1时才产生结果输出,这是因为此时与
才属于LCS。两种求解方法的时间复杂度都为
。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <cstdio> 2 #include <cstring> 3 4 #define MAX_LENGTH 100 5 int m[MAX_LENGTH][MAX_LENGTH]; 6 int b[MAX_LENGTH][MAX_LENGTH]; 7 8 char str1[MAX_LENGTH]; 9 char str2[MAX_LENGTH]; 10 int length1; 11 int length2; 12 void print(int a,int c) 13 { 14 if(a==0||c==0) return ; 15 else if(b[a][c]==1) 16 { 17 print(a-1,c-1); 18 printf("%d %d\n",a-1,c-1); 19 } 20 else if(b[a][c]==2) 21 print(a-1,c); 22 else if(b[a][c]==3) 23 print(a,c-1); 24 /* 25 if(a==0||c==0) return ; 26 else if(str1[a-1]==str2[c-1]) 27 { 28 print(a-1,c-1); 29 printf("%d %d\n",a-1,c-1); 30 } 31 else if(m[a][c]==m[a-1][c]) 32 print(a-1,c); 33 else if(m[a][c]==m[a][c-1]) 34 print(a,c-1); 35 */ 36 } 37 void solve() 38 { 39 int i,j,k; 40 for(i=0;i<=length1;i++) 41 { 42 for(j=0;j<=length2;j++) 43 { 44 if(i==0||j==0) continue; 45 if(str1[i-1]==str2[j-1]) 46 { 47 m[i][j]=m[i-1][j-1]+1; 48 b[i][j]=1; 49 } 50 else 51 { 52 if(m[i-1][j]>=m[i][j-1]) 53 { 54 m[i][j]=m[i-1][j]; 55 b[i][j]=2; 56 } 57 else 58 { 59 m[i][j]=m[i][j-1]; 60 b[i][j]=3; 61 } 62 } 63 } 64 } 65 printf("%d\n",m[length1][length2]); 66 print(length1,length2); 67 } 68 69 int solve1(int a,int c) 70 { 71 72 if(m[a][c]!=-1) return m[a][c]; 73 74 if(a==0||c==0) 75 { 76 m[a][c]=0; 77 return m[a][c]; 78 } 79 80 if(str1[a-1]==str2[c-1]) 81 { 82 m[a][c]=solve1(a-1,c-1)+1; 83 b[a][c]=1; 84 } 85 else 86 { 87 if(solve1(a-1,c)>=solve1(a,c-1)) 88 { 89 m[a][c]=solve1(a-1,c); 90 b[a][c]=2; 91 } 92 else 93 { 94 b[a][c]=3; 95 m[a][c]=solve1(a,c-1); 96 } 97 } 98 return m[a][c]; 99 } 100 101 int main(void) 102 { 103 freopen("data.in","r",stdin); 104 scanf("%s",str1); 105 scanf("%s",str2); 106 length1=strlen(str1); 107 length2=strlen(str2); 108 // solve(); 109 int i,j; 110 for(i=0;i<=length1;i++) 111 { 112 for(j=0;j<=length2;j++) 113 m[i][j]=-1; 114 } 115 solve1(length1,length2); 116 printf("%d\n",m[length1][length2]); 117 print(length1,length2); 118 return 0; 119 }
当然,在实现的过程中,我们如果要输出结果,不要b数组也是可以的,在print函数中,被注释掉的内容,就没有利用b数组,而是直接使用m数组中的结果进行LCS的构建工作。这样在增加了些许时间复杂度的情况下,将空间复杂度降低了一半。
同时,如果我们只关心LCS的长度,那么空间复杂度可以再次降低,至多要的空间即可。我们观察递推公式,可以看到,m[i,j]的求解最多只与m[i-1,j-1],m[i-1,j]与m[i,j-1]相关联。当我们使用自底向上(solve函数)的方法求解时,我们甚至可以只用
个空间的数组b来保存计算结果,用1个空间来保存m[i-1,j-1]。这是由于,当我们计算m[i,j]时,m[i-1,j-1]所在的位置(b[j-1])已经被本行结果(m[i,j-1])所覆盖,而计算所需的m[i,j-1]在b[j-1]的位置上,并且刚刚被计算出来,m[i-1,j]在计算结果写入b[j]之前,存在于b[j]。
另外的个空间用来存放较短的那个串。基本原理就是这样,我没有写程序实现,有兴趣的读者可以自己写动手写一下。
最长递增子序列
我们接下来考虑另外一个相似的问题,对于一组数字序列,以相同的方法定义子序列,如果这个序列中的数字是单调递增的,则称为递增子序列。我们所要求的就是最长的递增子序列。设串为一个数字串,如果使用暴力搜索求最长递增子序列,时间复杂度为
,显然不可行。求解此问题,关键在于如何找出递推式。
我们可以发现一个明显的关系,设c[i]为串并且包含了
的最长递增子序列的长度。设一个串为{8,9,10,1,2},那么c[1]=1,c[2]=2,c[3]=3,c[4]=1,c[5]=2。我们可以很方便地得出递推关系:
通过该递归式,我们可以求出所有的c[i],最后遍历一遍c,从中找出最长的递增子序列即可,或者直接在求c[i]的过程中保存当前求出的最长递增子序列,当结束的时候,当前最长递增子序列就变成了全局最长递增子序列。该算法的时间复杂度为,当我们需要得到最长递增子序列的内容时,需要另外一个数组d来跟踪最长递增子序列。d[i]中记录的是c[i]那个递增子序列的前一个数。使用递归的方法就可以得到递增子序列。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
#include <cstdio> #define MAX_LENGTH 100 int N; int c[MAX_LENGTH],d[MAX_LENGTH],num[MAX_LENGTH]; void print(int pos) { if(d[pos]!=pos) { print(d[pos]); } printf("%d ",pos); } void solve() { int i,j,max=0,maxpos=0; for(i=0;i<N;i++) { if(i==0) { c[i]=1; max=1; maxpos=0; d[i]=i; } else { bool flag=false; int tempmax=0; int tempmaxpos=0; for(j=0;j<i;j++) { if(num[j]<num[i]) { if(c[j]+1>tempmax) { tempmax=c[j]+1; tempmaxpos=j; } flag=true; } } if(flag==true) { c[i]=tempmax; d[i]=tempmaxpos; } else { c[i]=1; d[i]=i; } if(c[i]>max) { max=c[i]; maxpos=i; } } } printf("max:%d\n",max); printf("path:\n"); print(maxpos); printf("\n"); } int main(void) { scanf("%d",&N); int i,j; for(i=0;i<N;i++) { scanf("%d",&num[i]); //-1标示还没有得到结果 //c[i]=-1; //d[i]=-1; } solve(); return 0; }
如果我们只关心最长递增子序列的长度,我们可以以更快的速度求解。此时,我们需要一个最长为m的数组。我先阐述一个较为不严谨的原理:对于序列中的某一个数,我们期望它能够成为最长递增子序列一个时,我们就必须使当前最长递增子序列中的最大的数尽可能的小。单纯地讲理论无法理解这种思想,并且我也没有信心能够讲好。因此,下面我将就一个例子阐述这种思路。
示例
串X={5,6,9,2,3,1,4,6,7,8},当前最长递增子序列的长度max_length=0
Step 1:读取5,将5加入m,此时m数组为空,直接加入即可:
m:5;max_length=1
Step 2:读取6,将6加入m,覆盖位置为仅小于6的数之后的那个数:
m:5,6;max_length=2
Step 3:读取9,将9加入m,覆盖位置为仅小于9的数之后的那个数:
m:5,6,9;max_length=3
Step 4:读取2,将2加入m,覆盖位置为仅小于2的数之后的那个数,注意数组中的数都比2大,那么就将直接覆盖掉5:
m:2,6,9;max_length=3
Step 5:读取3,将3加入m,覆盖位置为仅小于3的数之后的那个数:
m:2,3,9;max_length=3
注意Step 4和Step 5的步骤,覆盖掉了m的前两项,这样就破坏了最长递增子序列的内容,但其长度仍然保留下来。
Step 6:读取1,将1加入m,覆盖位置为仅小于1的数之后的那个数:
m:1,3,9;max_length=3
Step 7:读取4,将4加入m,覆盖位置为仅小于4的数之后的那个数:
m:1,3,4;max_length=3
Step 8:读取6,将6加入m,覆盖位置为仅小于6的数之后的那个数:
m:1,3,4,6;max_length=4
此时最长的递增子序列为2,3,4,6。数组中并没有保留递增子串的内容,只是维护了递增子序列的长度。
Step 9:读取7,将7加入m,覆盖位置为仅小于7的数之后的那个数:
m:1,3,4,6,7;max_length=5
Step 10:读取8,将8加入m,覆盖位置为仅小于8的数之后的那个数:
m:1,3,4,6,7,8;max_length=6
经过以上各个步骤,我们得到了max_length为6。在计算过程中,我们对于序列中的每一个数都进行了处理,将其插入到数组m适当的位置上,这个插入过程的定位使用二分法定位,复杂度为,m个数的复杂度就为
。