最长公共子序列问题及扩展

来源:互联网 发布:java类成员变量初始化 编辑:程序博客网 时间:2024/05/16 06:38

转自:
http://jbm3072.iteye.com/blog/1074392
http://imlazy.ycool.com/post.1861423.html

问题

如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,则字符串一称之为字符串二的子串。

       注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。

      请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串。

      例如:输入两个字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子串,

              则输出它们的长度4,并打印任意一个子串。

 

 

思想

求最长公共子串(Longest Common Subsequence, LCS)是一道非常经典的动态规划题。

在开始想这道题之前应先理解什么是LCS(最长公共子串)。如串“abcbbc”,"abccbb",其中的一个字串"abc"是其公共子串,但不时最长公共字串。最长公共子串的定义中,并不要求最长公共子串必须连续出现在两个字符串中,只需要能保持顺序的出现在序列中即可。

 

在理解了什么是LCS之后,LCS的递归解就容易理解了:

c[i,j] =

0  (当 i=0或者j=0

c[i-1,j-1] +1 xi = yj,且 i>0,j>0

Max(c[i,j-1],  c[i-1,j]  ) (xi!=yj , i, j >0 )

c[i,j]表示字符串 X[ 1, i]Y[1, j]LCS长度。

 

 

 

 

算法导论给出了该算法。算法分成了两步。第一步找到LCS的长度。在找LCS长度的过程中,构造运算矩阵,以方便第二步求解LCS串。

 

扩展

 

    LCS的定义不要求公共子串中的字符连续出现在两个字符串中。如果要求这样做。那么该怎么来做呢。

可以想到和LCS算法差不多的算法。即也维护一个M*N的矩阵c。如果a[i]==b[j],那么c[i][j]=c[i-1][j-1]+1;当i或者j等于0时,c[i][j]=1;如果不等c[i][j]=0

       代码实现参考代码2

 

 

代码

 

 

Java代码  收藏代码
  1. package com.jy.string;  
  2.   
  3. public class LCS {  
  4.     public static void main(String[] args) {  
  5.         char[] string = "abcbbc".toCharArray();  
  6.         char[] string2 = "abccbb".toCharArray();  
  7.           
  8.         findLCS(string, string2);  
  9.     }  
  10.     /** 
  11.      *  
  12.      * @param string 
  13.      * @param string2 
  14.      */  
  15.     public static void findLCS(char[] string ,char[] string2) {  
  16.         int m = string.length;  
  17.         int n = string2.length;  
  18.         int[][] c = new int[m+1][n+1];  
  19.         int[][] b = new int[m+1][n+1];  
  20.         for(int i=0;i<m;i++) {  
  21.             for(int j=0;j<n;j++) {  
  22.                 if(string[i]==string2[j]) {  
  23.                     c[i+1][j+1]=c[i][j]+1;  
  24.                     b[i+1][j+1]= '\\';  
  25.                 } else {  
  26.                     if(c[i][j+1] >= c[i+1][j]) {  
  27.                         c[i+1][j+1] = c[i][j+1];  
  28.                         b[i+1][j+1]= '|';  
  29.                     } else {  
  30.                         c[i+1][j+1] = c[i+1][j];  
  31.                         b[i+1][j+1]= '-';  
  32.   
  33.                     }  
  34.                 }  
  35.             }  
  36.         }  
  37.           
  38.         System.out.println("LCS长度:"+c[m][n]);  
  39.           
  40.         System.out.println("LCS序列:");  
  41.         printLCS(b,string,m,n);  
  42.     }  
  43.     private static void printLCS(int[][] b, char[] string, int i, int j) {  
  44.         if(i==0 || j==0) {  
  45.             return ;  
  46.         }  
  47.           
  48.         if(b[i][j]=='\\') {  
  49.             printLCS(b, string, i-1, j-1);  
  50.             System.out.println(string[i-1]);  
  51.         } else if(b[i][j]=='|'){  
  52.             printLCS(b, string, i-1, j);  
  53.         } else {  
  54.             printLCS(b, string, i, j-1);  
  55.         }  
  56.            
  57.     }  
  58. }  
Java代码  收藏代码
  1. <pre name="code" class="java">  
  2. package com.jy.string;  
  3.   
  4. public class LCS2 {  
  5.     public static void main(String[] args) {  
  6.         findLCS("abcdefg".toCharArray(), "abcdged".toCharArray());  
  7.     }  
  8.       
  9.     public static void findLCS(char[] string1, char[] string2) {  
  10.         int m = string1.length;  
  11.         int n = string2.length;  
  12.         int[][] c = new int[m][n];  
  13.         int max=0;  
  14.         int maxPosX = 0;  
  15.         for(int i=0;i<m;i++) {  
  16.             for(int j=0;j<n;j++) {  
  17.                 if(string1[i]==string2[j]) {  
  18.                     if(i==0||j==0) {  
  19.                         c[i][j] = 1;  
  20.                     } else {  
  21.                         c[i][j] = c[i-1][j-1]+1;  
  22.                     }  
  23.                     if(c[i][j]>max) {  
  24.                         max =c[i][j];  
  25.                         maxPosX = i;  
  26.                     }  
  27.                       
  28.                 } else {  
  29.                     c[i][j] = 0;  
  30.                 }  
  31.             }  
  32.         }  
  33.         System.out.println(max);  
  34.         for(int i = maxPosX-max+1;i<=maxPosX;i++) {  
  35.             System.out.print(string1[i]);  
  36.         }  
  37.     }  
  38. }  

求多个字符串的最大公共子串

 如果所有字符串的长度之和是L,则下面介绍的这个算法的平均效率O(L * logL),但是最坏情况下可能会再乘以O(l),l是每个字符串的平均长度。

    首先对于每个字符串,取出以每个字符开头,到字符串尾的子串。比如字符串“acb”,从中取出的子串有“acb”、“cb”和“b”。如果所有字符串的总长度为L,则总共就有L个子串。我们把这些子串存在一个名为sub的数组中。(注意,最好用C风格的字符,这样可以直接引用每个子串的首地址,不用把这些子串另外转存。)
    
    接下来就是主要花时间的步骤:把这L个子串排序。如果用快速排序的话时间就是O(L * logL)。比如共有三个字符串“acb”、“cbb”和“dcba”,从它们中取出的子串有:“acb”、“cb”、“b”、“cbb”、“bb”、“b”、“dcba”、“cba”、“ba”和“a”。把它们排序后的结果如下(为了看得清楚,我把字符串坚着放):

sub: 0 1 2 3 4 5 6 7 8 9

     a a b b b b c c 
c d
       c     a b b b 
b c
       b           a 
b b
                       a

    但是这里有个问题,这个排序中的每次比较都是字符串的比较,它们会不会花很多时间呢。其实比较两个字符串的时间取决于它们开头有几个字符是相同的,如果相同的少,则很快就可以比完了。假设字符串的每个字符都是26个英文字母之一,则两个字符串开头有1个字符相同的概率是1 / 26,有2个字符相同的概率是1 / 262;学过概率论就知道,这样两个字符串开头相同的字符数的期望值为1 / 26 + 2 / 262 + 3 / 263 ... < 1。也就是说比较两个字符串的平均时间是常数极的。
    但以上说的只是平均情况。在最最极端的情况下(虽然概率上不几乎不可能出现,但是出题的人一定会出这种极限数据),可能所有字符串都只含有同样的字符(比如要你求“aaaa”、“aaaa”和“aaaa”和最大公共子串),那么在排序算法中比较每两个子串时,都至少要把其中一个子串从头读到尾,时间数量级大约就是所有字符串的平均长度,也就是本文一开始说的,间时复杂度会上升到O(l * L * logL)。

   把所有子串排好了序,就离答案很近了。不难想象,我们要求的最大公共子串一定是数组sub中相邻的几项的最大公共前缀。比如在上面的例子中,最大公共子串是“cb”,它就是数组sub中下标6、7和8这三项的最大公共前缀。

    其实数组sub中需要存放的不只是每个子串的首地址,还需要存放每个子串属于第几个原字符串(在上面的例子中用颜色来表示),这在最后一个步骤中需要用到。

    下面是最后的寻找步骤:在数组sub中,对于每一段相邻的且覆盖了所有原字符串的元素(而且只要最小段,也就是去掉该段的首、尾任意一个元素,它就不能覆盖所有原字符串了),求出该段首尾两个元素的最大公共前缀,即找到了所有原字符串的一个公共子串。枚举所有符合要求的段,就可以找出所有原字符串的最大公共子串。在前面的例子中,符合要求的段有[0,2]、[2,4]、[3,5]、[4,6]、[5,7]和[6,8],经过比较,在[6,8]这一段就找到了我们要求的最大公共子串“cb”。
    这一步骤所花的时间是O(L),具体流程虽然不难,但是用文字说起来有点麻烦,所以还是详见后面的代码吧。可见这一步花的时间比起总的O(L * logL)算不了什么。

    到这里算是讲完了。在UVA 11107有一个类似的问题,虽然有点不一样,但是道理是完全一样的。为了弥补我上面没有讲清楚的最后一个步骤,下面附上我这题的代码。


#include <cstdio>#include <cstdlib>#include <cstring>using namespace std;const int MAX_LEN = 1004;//Notice, the test data is wrong.                         //The bound is 1004 but not 1000.const int MAX_STR = 100;struct SubStr {    const char* addr;    int num;};char g_str[MAX_STR][MAX_LEN + 1];int g_strCnt;SubStr g_subStr[MAX_STR * MAX_LEN];int g_subStrCnt;int subStrCmp(const void* a, const void* b) {    return strcmp(((const SubStr*)a)->addr, ((const SubStr*)b)->addr);}int commonLen(const SubStr& a, const SubStr& b) {    const char* i = a.addr;    const char* j = b.addr;    int len = 0;    while (*i && *j && *i == *j) {        len++;        i++;        j++;    }    return len;}void printStr(const char* str, int len) {    for (int i = 0; i < len; i++) {        printf("%c", *str);        str++;    }    printf("\n");}void initSubStr() {    g_subStrCnt = 0;    for (int i = 0; i < g_strCnt; i++) {        for (const char* j = g_str[i]; *j; j++) {            g_subStr[g_subStrCnt].addr = j;            g_subStr[g_subStrCnt].num = i;            g_subStrCnt++;        }    }    qsort(g_subStr, g_subStrCnt, sizeof(SubStr), subStrCmp);}int findLongest() {    int longest = 0;    SubStr* head = g_subStr;    SubStr* tail = g_subStr;    const SubStr* end = g_subStr + g_subStrCnt;    int half = g_strCnt / 2;    int coverCnt = 0;    int cover[MAX_STR];    memset(cover, 0, sizeof(cover));    while (head != end) {        //To find every pair of head and tail,        //that in the range [tail, head] there are exactly half + 1        //strings are covered.        while (coverCnt <= half && head != end) {            if (cover[head->num] == 0) {                coverCnt++;            }            cover[head->num]++;            head++;        }        while (coverCnt > half) {            cover[tail->num]--;            if (cover[tail->num] == 0) {                coverCnt--;            }            tail++;        }        if (coverCnt == half) {            int len = commonLen(*(tail - 1), *(head - 1));            if (len > longest) {                longest = len;            }        }    }    return longest;}//The work flow of this function is just like "findLongest()".void printCommon(int longest) {    const SubStr* head = g_subStr;    const SubStr* tail = g_subStr;    const SubStr* pre = NULL;    const SubStr* const end = g_subStr + g_subStrCnt;    int half = g_strCnt / 2;    int coverCnt = 0;    int cover[MAX_STR];    memset(cover, 0, sizeof(cover));    while (head != end) {        while (coverCnt <= half && head != end) {            if (cover[head->num] == 0) {                coverCnt++;            }            cover[head->num]++;            head++;        }        while (coverCnt > half) {            cover[tail->num]--;            if (cover[tail->num] == 0) {                coverCnt--;            }            tail++;        }        if (coverCnt == half) {            int len = commonLen(*(tail - 1), *(head - 1));            if (len == longest                && (pre == NULL                    || commonLen(*(tail - 1), *pre) < longest                   )               ) {                printStr((tail - 1)->addr, longest);                pre = tail - 1;            }        }    }}bool input() {    bool hasNext = false;    scanf("%d", &g_strCnt);    if (g_strCnt > 0) {        hasNext = true;        for (int i = 0; i < g_strCnt; i++) {            scanf("%s", g_str[i]);        }    }    return hasNext;}void solve() {    initSubStr();    int len = findLongest();    if (len == 0) {        printf("?\n");    }    else {        printCommon(len);    }}int main() {    int cnt = 0;    while (input()) {        if (cnt > 0) {            printf("\n");        }        solve();        cnt++;    }    return 0;}



原创粉丝点击