查找“n”个二进制字符串中最长的公共子串的长度

Posted

技术标签:

【中文标题】查找“n”个二进制字符串中最长的公共子串的长度【英文标题】:Find the length of the longest common substring in 'n' binary strings 【发布时间】:2018-10-02 22:04:39 【问题描述】:

我得到了n 字符串(n>=2 和 na 和 b。在这组字符串中,我必须找到所有字符串中存在的最长公共子字符串的长度。保证存在解决方案。我们来看一个例子:

n=4
abbabaaaaabb
aaaababab
bbbbaaaab
aaaaaaabaaab

The result is 5 (because the longest common substring is "aaaab").

我不必打印(甚至知道)子字符串,我只需要打印它的长度。

同时给出结果不能大于60,即使每个字符串的长度可以高达13 000

我尝试的是:我找到给定字符串中任何字符串的最小长度,然后将其与60 进行比较,然后选择两者之间的最小值为starting point。然后我开始取第一个字符串的序列,第一个字符串的每个序列的长度为len,其中len 取值从starting point1。在每次迭代中,我采用长度为len 的第一个字符串的所有可能序列,并将其用作pattern。使用 KMP 算法(因此,复杂度为 O(n+m)),我遍历所有其他字符串(从 2n)并检查是否在字符串 i 中找到 pattern。每当找不到时,我会中断迭代并尝试下一个长度为len 的可用序列,或者,如果没有,我减少len 并尝试所有长度为新的、减少的值@ 的序列987654344@。但是如果它匹配,我停止程序并打印长度len,因为我们从可能的最长长度开始,每一步递减,所以我们找到的第一个匹配代表最大可能的长度是合乎逻辑的。这是代码(但这并不重要,因为这种方法不够好;我知道我不应该使用using namespace std,但它并没有真正影响这个程序,所以我只是没有打扰):

#include <iostream>
#include <string>
#define nmax 50001
#define result_max 60

using namespace std;

int n,m,lps[nmax],starting_point,len;
string a[nmax],pattern,str;

void create_lps() 
    lps[0]=0;
    unsigned int len=0,i=1;
    while (i < pattern.length()) 
        if (pattern[i] == pattern[len]) 
            len++;
            lps[i] = len;
            i++;
        
        else 
            if (len != 0) 
                len = lps[len-1];
            
            else 
                lps[i] = 0;
                i++;
            
        
    


bool kmp_MatchOrNot(int index) 
    unsigned int i=0,j=0;
    while (i < a[index].length()) 
        if (pattern[j] == a[index][i]) 
            j++;
            i++;
        
        if (j == pattern.length()) 
            return true;
        
        else if (i<a[index].length() && pattern[j]!=a[index][i])
            if (j != 0) 
                j = lps[j-1];
            
            else 
                i++;
            
        
    
    return false;


int main()

    int i,left,n;
    unsigned int minim = nmax;
    bool solution;
    cin>>n;
    for (i=1;i<=n;i++) 
        cin>>a[i];
        if (a[i].length() < minim) 
            minim = a[i].length();
        
    

    if (minim < result_max) starting_point = minim;
    else starting_point = result_max;

    for (len=starting_point; len>=1; len--) 
        for (left=0; (unsigned)left<=a[1].length()-len; left++) 
            pattern = a[1].substr(left,len);
            solution = true;
            for (i=2;i<=n;i++) 
                if (pattern.length() > a[i].length()) 
                    solution = false;
                    break;
                
                else 
                    create_lps();
                    if (kmp_MatchOrNot(i) == false) 
                        solution = false;
                        break;
                    
                
            
            if (solution == true) 
                cout<<len;
                return 0;
            
        
    
    return 0;

事情是这样的:程序运行正常并且给出了正确的结果,但是当我在网站上发送代码时,它给出了“超出时间限制”的错误,所以我只得到了一半的分数。

这让我相信,为了以更好的时间复杂度解决问题,我必须利用字符串的字母只能是ab这一事实,因为它看起来就像我没有使用的一个非常大的东西,但我不知道我该如何使用这些信息。我将不胜感激。

【问题讨论】:

快速浏览一下您的代码有forforfor,它似乎大致为 O(lgn^2)。这个问题有一个非常有效的解决方案,接近 O(n) 你应该看到this。它是解决这个问题的经典算法 @user3386109 你是对的,我没有检查就输入了。示例的结果确实是 5。对不起。 @138 嗯,这就是我使用的算法,KMP(你链接的那个)。但这不仅仅是搜索和检查。我必须找到 N 个字符串之间最长的公共子字符串,所以还有很多工作要做。第一个'for'设置当前模式的长度(第一个字符串);第二个“for”选择当前模式(第一个字符串的)开始的位置。我这样做是为了获取实际模式并构建“lps []”数组。第三个“for”检查当前模式(第一个字符串)是否与所有其他字符串(从 2 到 n)匹配。我不知道如何才能缩短时间。 我认为这可以通过trie 来完成。使用最短的字符串来构建 trie。然后处理其他字符串,标记已访问的节点,但不添加任何新节点。最后,遍历 trie。所有字符串访问过的最深节点的深度就是答案。 @user3386109 如果没有记忆,比较后缀树的成本类似于比较尝试。有了记忆,它绝对更快。如果您重用数据结构中恰好相同的部分,则后缀树数据结构就是 trie 数据结构,因此解决方案在概念上是相同的,尽管后缀树是绝对的赢家。 【参考方案1】:

答案是单独构建所有字符串的后缀树,然后将它们相交。后缀树就像一个trie,同时包含一个字符串的所有后缀。

为固定字母构建后缀树是O(n) 和Ukkonen's algorithm。 (如果你不喜欢这个解释,你可以用 google 找其他的。)如果你有 m 大小为 n 的树,那么现在是 O(nm)

相交后缀树是并行遍历它们的问题,只有当你可以在所有树中走得更远时才会走得更远。如果你有m 大小为n 的树,则此操作可以在不超过O(nm) 的时间内完成。

这个算法的总时间是时间O(nm)。鉴于仅仅读取字符串是时间O(nm),你不能做得比这更好。


添加少量细节,假设您的后缀树被写为每个节点一个字符。所以每个节点只是一个字典,其键是字符,其值是树的其余部分。因此,以我们为例,对于字符串ABABA,https://imgur.com/a/tnVlSI1 处的图表将变成类似于(见下文)这样的数据结构:


    'A': 
        'B': 
            '': None,
            'A': 
                'B': 
                    '': None
                
            
        
    ,
    'B': 
        '': None
        'A': 
            'B': 
                '': None
            
        
    

同样BABA 会变成:


    'A': 
        '': None
        'B': 
            'A': 
                '': None
            
        
    ,
    'B': 
        'A': 
            '': None,
            'B': 
                'A': 
                    '': None
                
            
        
    

对于看起来像这样的数据结构,天真的 Python 比较它们看起来像:

def tree_intersection_depth (trees):
    best_depth = 0
    for (char, deeper) in trees[0].items():
        if deeper is None:
            continue
        failed = False

        deepers = [deeper]
        for tree in trees[1:]:
            if char in tree:
                deepers.append(tree[char])
            else:
                failed = True
                break

        if failed:
            continue

        depth = 1 + tree_intersection_depth(deepers)
        if best_depth < depth:
            best_depth = depth

    return best_depth

你可以这样称呼它tree_intersection_depth([tree1, tree2, tree3, ...])

对于上述两棵树,它确实给出了3 作为答案。

现在我实际上是在写出那个数据结构时作弊。使后缀树高效的原因在于您实际上并没有看起来像那样的数据结构。你有一个重用所有重复结构的。所以模拟设置数据结构并调用它的代码如下所示:

b_ = 'B': '': None
ab_ = '': None, 'A': b_
bab_ = 'B': ab_
abab = 'A': bab_, 'B': ab_

a_ = 'A': '': None
ba_ = '': None, 'B': a_
aba_ = 'A': ba_
baba = 'B': aba_, 'A': ba_

print(tree_intersection_depth([abab, baba]))

现在我们可以看到,要获得承诺的性能,还缺少一个步骤。问题是虽然树的大小是O(n),但在搜索它时,我们可能会访问O(n^2) 子字符串。在您的情况下,您不必担心,因为保证子字符串的深度永远不会超过 60。但在完全一般的情况下,您需要添加记忆,以便当递归导致比较数据结构时,您以前见过,您立即返回旧答案,而不是新答案。 (在 Python 中,您将使用 id() 方法将对象的地址与您之前看到的地址进行比较。在 C++ 中,有一组用于相同目的的指针元组。)

【讨论】:

很抱歉,我还是不太明白你建议做什么,因为我以前从未使用过后缀树。所以我阅读了它们并了解了如何构建它们,但我真的不明白之后要做什么。你是什​​么意思“并行遍历它们,只有当你可以在所有树中走得更远时才能走得更远”,我究竟应该怎么做?举个例子:2 个字符串:“ABAB”和“BABA”。第一个以 $0 作为结束字符,第二个以 $1 作为结束字符。所以后缀树看起来像这样:imgur.com/a/tnVlSI1。我如何得到答案 3? @BogdanVlad 递归地尝试以所有可能的方式将可能的字符串匹配扩展到所有树,从根匹配的空字符串开始。您对匹配模式的完整搜索结果应该是:(empty)AABABABBABAB。那是出现在两者中的所有子字符串的集合。其中两个的长度为 3,这就是答案。 所以我基本上取第一个字符串的所有子字符串并在其余字符串(从 2 到 n)中搜索该子字符串?对不起,如果我很烦人,但我真的很想了解这一点。 @BogdanVlad 解释说评论太多了,所以我用工作 Python 数据结构和代码更新了我的答案,以了解树比较的机制。希望对您有所帮助。

以上是关于查找“n”个二进制字符串中最长的公共子串的长度的主要内容,如果未能解决你的问题,请参考以下文章

最长公共子串

18.12.20 DSA 最长公共子串

[PHP]算法-最长公共子串的PHP实现

java求最长公共子串的长度

BZOJ 2946 POI2000 公共串 后缀自动机(多串最长公共子串)

动态规划系列最长公共子串