多个字符串的最长公共字串

原文链接:http://imlazy.ycool.com/post.1861423.html

 如果所有字符串的长度之和是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 / 26 2 ;学过概率论就知道,这样两个字符串开头相同的字符数的期望值为1 / 26 + 2 / 26 2  + 3 / 26 3  ... < 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值