[Data Structure] LCSs——最長公共子序列和最長公共子串

1. 什麼是 LCSs?

  什麼是 LCSs? 好多博友看到這幾個字母可能比較困惑,由於這是我本身對兩個常見問題的統稱,它們分別爲最長公共子序列問題(Longest-Common-Subsequence)和最長公共子串(Longest-Common-Substring)問題。這兩個問題很是的類似,因此對不熟悉的同窗來講,有時候很容易被混淆。下面讓咱們去好好地理解一下二者的區別吧。html

1.1 子序列 vs 子串

  子序列是有序的,但不必定是連續,做用對象是序列。算法

  例如:序列 X = <B, C, D, B> 是序列 Y = <A, B, C, B, D, A, B> 的子序列,對應的下標序列爲 <2, 3, 5, 7>。函數

  子串是有序且連續的,左右對象是字符串。測試

  例如 a = abcd 是 c = aaabcdddd 的一個子串;可是 b = acdddd 就不是 c 的子串。優化

1.2 最長公共子序列 vs 最長公共子串

  最長公共子序列和最長公共子串是常見的兩種問題,雖然二者問題很類似,也都可以根據動態規劃進行求解,可是二者的本質是不一樣的。ui

  最長公共子序列問題是針對給出的兩個序列,求兩個序列最長的公共子序列。spa

  最長公共子串問題是針對給出的兩個字符串,求兩個字符串最長的公共子串(有關字符串匹配相關算法能夠轉至博客《[Algorithm] 字符串匹配算法——KMP算法》)。3d

2. 動態規劃方法求解LCSs

  前面提到,動態規劃方法都可以用到最長公共子序列和最長公共子串問題當中,在這裏咱們就不一一進行求解了。咱們以最長公共子序列爲例,介紹一下如何利用動態規劃的思想來解決 LCSs。
code

  給定兩個序列,找出在兩個序列中同時出現的最長子序列的長度。對於每個序列而言,其均具備 $a^{m}$ 中子序列,所以採用暴力算法的時間複雜度是指數級的,這顯然不是一種好的解決方案。htm

  下面咱們看一下,如何使用動態規劃的思想來解決最大公共子序列問題。

  首先考慮最大公共子序列問題是否知足動態規劃問題的兩個基本特性:

  1. 最優子結構:

  設輸入序列是X [0 .. m-1] 和 Y [0 .. n-1],長度分別爲 m 和 n。和設序列 L(X [0 .. m-1],Y[0 .. n-1]) 是這兩個序列的 LCS 的長度,如下爲 L(X [0 .. M-1],Y [0 .. N-1]) 的遞歸定義:

  1)若是兩個序列的最後一個元素匹配(即X [M-1] == Y [N-1])

  則:L(X [0 .. M-1],Y [0 .. N-1])= 1 + L(X [0 .. M-2],Y [0 .. N-1])

  2)若是兩個序列的最後字符不匹配(即X [M-1] != Y [N-1])
  則:L(X [0 .. M-1],Y [0 .. N-1]) = MAX(L(X [0 .. M-2],Y [0 .. N-1]),L(X [0 .. M-1],Y [0 .. N-2]))

  經過以下具體實例來更好地理解一下:

  1)考慮輸入子序列 <AGGTAB> 和 <GXTXAYB>。最後一個字符匹配的字符串。這樣的 LCS 的長度能夠寫成:

L(<AGGTAB>, <GXTXAYB>) = 1 + L(<AGGTA>, <GXTXAY>)

  2)考慮輸入字符串「ABCDGH」和「AEDFHR。最後字符不爲字符串相匹配。這樣的LCS的長度能夠寫成:

L(<ABCDGH>, <AEDFHR>) = MAX ( L(<ABCDG>, <AEDFHR>), L(<ABCDGH>, <AEDFH>) )

  所以,LCS問題有最優子結構性質。

  2. 重疊子問題:

  很明顯,基於上述的分析,LCS 不少子問題也都共享子子問題,所以能夠對其進行遞歸求解。具體的算法時間度爲 O(m*n),能夠優化至 O(m+n)。

  下圖給出了回溯法找出LCS的過程:

  具體的C++實現代碼以下:

/ *動態規劃實現的LCS問題* /
#include<stdio.h>
#include<stdlib.h>

int max(int a, int b);

/* Returns length of LCS for X[0..m-1], Y[0..n-1] */
int lcs( char *X, char *Y, int m, int n )
{
   int L[m+1][n+1];
   int i, j;

   /* Following steps build L[m+1][n+1] in bottom up fashion. Note 
      that L[i][j] contains length of LCS of X[0..i-1] and Y[0..j-1] */
   for (i=0; i<=m; i++)
   {
     for (j=0; j<=n; j++)
     {
       if (i == 0 || j == 0)
         L[i][j] = 0;

       else if (X[i-1] == Y[j-1])
         L[i][j] = L[i-1][j-1] + 1;

       else
         L[i][j] = max(L[i-1][j], L[i][j-1]);
     }
   }

   /* L[m][n] contains length of LCS for X[0..n-1] and Y[0..m-1] */
   return L[m][n];
}

/* Utility function to get max of 2 integers */
int max(int a, int b)
{
    return (a > b)? a : b;
}

/*測試上面的函數 */
int main()
{
  char X[] = "AGGTAB";
  char Y[] = "GXTXAYB";

  int m = strlen(X);
  int n = strlen(Y);

  printf("Length of LCS is %d\n", lcs( X, Y, m, n ) );

  getchar();
  return 0;
}

  Python實現代碼以下:

def lcs(a,b):
  lena=len(a)
  lenb=len(b)
  c=[[0 for i in range(lenb+1)] for j in range(lena+1)]
  flag=[[0 for i in range(lenb+1)] for j in range(lena+1)]
  for i in range(lena):
    for j in range(lenb):
      if a[i]==b[j]:
        c[i+1][j+1]=c[i][j]+1
        flag[i+1][j+1]='ok'
      elif c[i+1][j]>c[i][j+1]:
        c[i+1][j+1]=c[i+1][j]
        flag[i+1][j+1]='left'
      else:
        c[i+1][j+1]=c[i][j+1]
        flag[i+1][j+1]='up'
  return c,flag

def printLcs(flag,a,i,j):
  if i==0 or j==0:
    return
  if flag[i][j]=='ok':
    printLcs(flag,a,i-1,j-1)
    print(a[i-1],end='')
  elif flag[i][j]=='left':
    printLcs(flag,a,i,j-1)
  else:
    printLcs(flag,a,i-1,j)
    
a='ABCBDAB'
b='BDCABA'
c,flag=lcs(a,b)
for i in c:
  print(i)
print('')
for j in flag:
  print(j)
print('')
printLcs(flag,a,len(a),len(b))
print('')

  awk 命令也能夠很容易的寫出 LCS 的代碼:

echo "123456abcd567
234dddabc45678"|awk -vFS="" 'NR==1{str=$0}NR==2{N=NF;for(n=0;n++<N;){s="";for(t=n;t<=N;t++){s=s""$t;if(index(str,s)){a[n]=t-n;b[n]=s;if(m<=a[n])m=a[n]}else{t=N}}}}END{for(n=0;n++<N;)if(a[n]==m)print b[n]}'

3. 參考內容

  1. 《算法導論》動態規劃之最長公共子序列;

相關文章
相關標籤/搜索