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

原文链接: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;
}

### 回答1: 最长公共子串是指在两个字符串中同时出现的最长的子串。可以使用动态规划的方法来解决这个问题。具体步骤如下: 1. 定义一个二维数组dp,其中dp[i][j]表示以字符串a的第i个字符和字符串b的第j个字符结尾的最长公共子串的长度。 2. 初始化dp数组,将dp[i][j]的初始值设为。 3. 遍历字符串a和字符串b,如果a[i]等于b[j],则dp[i][j]的值为dp[i-1][j-1]+1,否则dp[i][j]的值为。 4. 在遍历的过程中,记录最长公共子串的长度和起始位置,即dp[i][j]的值最大的位置。 5. 根据最长公共子串的长度和起始位置,可以得到最长公共子串。 6. 最终返回最长公共子串。 需要注意的是,如果最长公共子串有多个,只返回其中一个即可。 ### 回答2: 最长公共子串问题是指在两个字符串中查找到最长的相同的子串,这个子串在两个字符串中位置可以不同。这个问题是计算机科学中经典的问题,有多种解法。 一种简单的解法是暴力枚举。首先找到两个字符串的所有子串(可以用双重循环),然后比较每一对子串是否相同,找到相同的最长子串。这种解法的时间复杂度是O(n^3),其中n是字符串长度,效率比较低,适用于小数据量的字符串。 另一种解法是动态规划。定义一个二维数组dp,其中dp[i][j]表示字符串a的前i个字符和字符串b的前j个字符的最长公共子串长度。初始化dp[i][j]=0,然后用双重循环遍历a和b中的所有字符,如果a[i]==b[j],则dp[i][j]=dp[i-1][j-1]+1,表示在a的前i-1个字符和b的前j-1个字符的最长公共子串的基础上,加上这两个字符,可以得到a和b的前i个字符和前j个字符的最长公共子串。最后遍历dp数组,找到最大的dp[i][j],即为最长公共子串。这种解法的时间复杂度是O(n^2),效率较高,适用于中等数据量的字符串。 还有一种解法是基于后缀数组的。后缀数组是指对于一个字符串S,将它的所有后缀按照字典序排序后存储的数组,可以用于查找字符串中的子串。具体做法是将两个字符串a和b拼接成一个新的字符串S,然后求出S的后缀数组sa,然后求出sa中相邻两个后缀的最长公共前缀长度,最长的就是a和b的最长公共子串。这种解法的时间复杂度是O(nlogn),效率比较高,适用于大数据量的字符串。 综上所述,查找两个字符串中的最长公共子串有多种解法,每种解法的适用情况不同,需要根据实际情况选择合适的算法。 ### 回答3: 最长公共子串问题是计算机领域中的一个经典问题,旨在寻找两个字符串中相同的最长字符串。解决该问题对于文本比较和字符串匹配等应用非常有用。 解决这个问题的方法有很多种,但最常见的方法是使用动态规划算法。具体步骤如下: 1. 创建一个二维数组dp,其中dp[i][j]表示字符串a的前i个字符和字符串b的前j个字符之间的最长公共子串的长度。 2. 初始化dp数组的第一行和第一列为0。 3. 通过遍历两个字符串中的每个字符来填充dp数组。对于i和j,如果a[i-1]等于b[j-1],则dp[i][j]等于dp[i-1][j-1]加1;否则,dp[i][j]为0。 4. 在dp数组中找到最大值,这个最大值就是两个字符串最长公共子串的长度。 5. 找到最长公共子串的方法是定位dp数组中最大值所在的位置i和j,然后从a[i-dp[i][j]]到a[i-1]或从b[j-dp[i][j]]到b[j-1]这个子串就是所要的答案。 以上就是解决查找两个字符串a,b中最长公共子串的算法,可以在程序设计中进行应用,达到处理文本以及字符串匹配等操作的效果。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值