查找“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 point
到1
。在每次迭代中,我采用长度为len
的第一个字符串的所有可能序列,并将其用作pattern
。使用 KMP 算法(因此,复杂度为 O(n+m)
),我遍历所有其他字符串(从 2
到 n
)并检查是否在字符串 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;
事情是这样的:程序运行正常并且给出了正确的结果,但是当我在网站上发送代码时,它给出了“超出时间限制”的错误,所以我只得到了一半的分数。
这让我相信,为了以更好的时间复杂度解决问题,我必须利用字符串的字母只能是a
或b
这一事实,因为它看起来就像我没有使用的一个非常大的东西,但我不知道我该如何使用这些信息。我将不胜感激。
【问题讨论】:
快速浏览一下您的代码有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)
、A
、AB
、ABA
、B
、BA
、BAB
。那是出现在两者中的所有子字符串的集合。其中两个的长度为 3,这就是答案。
所以我基本上取第一个字符串的所有子字符串并在其余字符串(从 2 到 n)中搜索该子字符串?对不起,如果我很烦人,但我真的很想了解这一点。
@BogdanVlad 解释说评论太多了,所以我用工作 Python 数据结构和代码更新了我的答案,以了解树比较的机制。希望对您有所帮助。以上是关于查找“n”个二进制字符串中最长的公共子串的长度的主要内容,如果未能解决你的问题,请参考以下文章