查找字符串中最长的重复序列

Posted

技术标签:

【中文标题】查找字符串中最长的重复序列【英文标题】:Find longest repetitive sequence in a string 【发布时间】:2012-06-20 20:41:22 【问题描述】:

我需要在字符串中找到最长的序列,并注意该序列必须重复三次或更多次。因此,例如,如果我的字符串是:

fdwaw4helloworldvcdv1c3xcv3xcz1sda21f2sd1ahelloworldgafgfa4564534321fadghelloworld

那么我希望返回值“helloworld”。

我知道有几种方法可以做到这一点,但我面临的问题是实际的字符串非常大,所以我真的在寻找一种可以及时完成的​​方法。

【问题讨论】:

不知道有没有正则表达式解决这个问题。这个 can't 是一个正则表达式,但是 python 可能有一个非正则扩展来做这样的事情。一般情况下这是 LCS 问题,可以使用动态规划解决:en.wikipedia.org/wiki/Longest_common_subsequence_problem 【参考方案1】:

这个问题是longest repeated substring problem 的一个变体,并且有一个使用suffix trees 的O(n) 时间算法来解决它。这个想法(正如***所建议的)是构建一个后缀树(时间 O(n)),用后代的数量(时间 O(n) 使用 DFS)注释树中的所有节点,然后找到具有至少三个后代的树中最深的节点(使用 DFS 的时间 O(n))。这个整体算法需要时间 O(n)。

也就是说,众所周知,后缀树很难构建,因此在尝试此实现之前,您可能希望找到一个为您实现后缀树的 Python 库。一个快速的谷歌搜索出现了this library,虽然我不确定这是否是一个好的实现。

另一种选择是将suffix arrays 与LCP arrays 结合使用。您可以迭代 LCP 数组中的相邻元素对,取每对元素中的最小值,然后存储您以这种方式找到的最大数字。这将对应于重复至少 3 次的最长字符串的长度,然后您可以从那里读取字符串本身。

有几种简单的算法可用于构建后缀数组(Manber-Myers 算法在 O(n log n) 时间内运行,并且编写起来并不难),而 Kasai 的算法在 O(n) 时间内构建 LCP 数组并且编码起来相当简单。

希望这会有所帮助!

【讨论】:

“出了名的难以构建”——怎么说? @KonradRudolph-我不知道有任何“简单”算法可以在线性时间内构建后缀树。我知道的两种算法(Ukkonen 算法和 DC3 算法)非常复杂,没有明显的正确性证明。也就是说,如果我对此有误,我很乐意纠正! 我同意证明不是微不足道的。但是 Ukkonen 算法存在伪代码,可以直接适应。此外,虽然线性时间算法很难找到,但也有一些简单派生的非线性构造算法,但在实践中表现相当不错。 这是我第一次看到有人对 LCS 的任何变体发布有用/非参考的答案。感谢您提供图书馆链接。 @templatetypedef Ukkonnen 的算法(恕我直言)在original paper 中得到了很好的解释。您首先必须了解算法如何用于尝试(每条边一个字符)。直观地说,活动点对应于树可以生长的节点,即由最后处理的字符标记的节点。 Ukkonnen 算法使用各种技巧(后缀包含等)来更有效地找到当前活动节点的集合。后缀树版本使用一对索引来节省内存并简化树更新。【参考方案2】:

想到的第一个想法是使用逐渐变大的正则表达式进行搜索:

import re

text = 'fdwaw4helloworldvcdv1c3xcv3xcz1sda21f2sd1ahelloworldgafgfa4564534321fadghelloworld'
largest = ''
i = 1

while 1:
    m = re.search("(" + ("\w" * i) + ").*\\1.*\\1", text)
    if not m:
        break
    largest = m.group(1)
    i += 1

print largest    # helloworld

代码运行成功。时间复杂度似乎至少为 O(n^2)。

【讨论】:

不适用于banana。答案应该是ana 2 times【参考方案3】:

让我们从头开始,统计频率,一旦出现频率最高的元素出现 3 次或更多次就停止。

from collections import Counter
a='fdwaw4helloworldvcdv1c3xcv3xcz1sda21f2sd1ahelloworldgafgfa4564534321fadghelloworld'
times=3
for n in range(1,len(a)/times+1)[::-1]:
    substrings=[a[i:i+n] for i in range(len(a)-n+1)]
    freqs=Counter(substrings)
    if freqs.most_common(1)[0][1]>=3:
        seq=freqs.most_common(1)[0][0]
        break
print "sequence '%s' of length %s occurs %s or more times"%(seq,n,times)

结果:

>>> sequence 'helloworld' of length 10 occurs 3 or more times

编辑:如果你觉得你正在处理随机输入并且公共子串应该是小长度的,你最好从小子串开始(如果你需要速度)然后停止当您找不到至少出现 3 次的内容时:

from collections import Counter
a='fdwaw4helloworldvcdv1c3xcv3xcz1sda21f2sd1ahelloworldgafgfa4564534321fadghelloworld'
times=3
for n in range(1,len(a)/times+1):
    substrings=[a[i:i+n] for i in range(len(a)-n+1)]
    freqs=Counter(substrings)
    if freqs.most_common(1)[0][1]<3:
        n-=1
        break
    else:
        seq=freqs.most_common(1)[0][0]
print "sequence '%s' of length %s occurs %s or more times"%(seq,n,times) 

结果和上面一样。

【讨论】:

也就是说,构建每组子串 M 长,从M=len(a)/N 开始(其中 N 是最小的次数),并计算每个的次数。如果没有子串的出现次数 >= N,则从 M 中减去 1 并重试。是吗? 我在相反的方向(从小到大)尝试类似的东西。知道您的方法的时间复杂度是多少吗? @MattCoughlin 在最坏的情况下,子串的数量在第一视线二次方:它是 sum(len(a)-([len(a)/times] - i)+1) 我运行的地方从 0 到 [len(a)/K],即 O(len(a)^2)。然后,您在每个子字符串上建立频率,该频率在子字符串上是线性的。所以,最后可能是 O(len(a)^3),如果我错了,请纠正我。您可以在 Counter 上进行一些操作,因为您不需要整个分布,只需知道最大频率 >=3。如果你知道你正在处理随机输入,你最好从小字符串开始。因此,在实践中它必须非常快【参考方案4】:

使用 defaultdict 计算从输入字符串中每个位置开始的每个子字符串。 OP 不清楚是否应该包括重叠匹配,这种蛮力方法包括它们。

from collections import defaultdict

def getsubs(loc, s):
    substr = s[loc:]
    i = -1
    while(substr):
        yield substr
        substr = s[loc:i]
        i -= 1

def longestRepetitiveSubstring(r, minocc=3):
    occ = defaultdict(int)
    # tally all occurrences of all substrings
    for i in range(len(r)):
        for sub in getsubs(i,r):
            occ[sub] += 1

    # filter out all substrings with fewer than minocc occurrences
    occ_minocc = [k for k,v in occ.items() if v >= minocc]

    if occ_minocc:
        maxkey =  max(occ_minocc, key=len)
        return maxkey, occ[maxkey]
    else:
        raise ValueError("no repetitions of any substring of '%s' with %d or more occurrences" % (r,minocc))

打印:

('helloworld', 3)

【讨论】:

我真的很喜欢这个解决方案,但不幸的是我的字符串通常太大了。但是,我敢打赌,您的回答对于通过 Google 登陆这里的许多人来说将非常有用,因为它确实很好地解决了我给出的原始示例。【参考方案5】:

如果您反转输入字符串,则将其提供给像(.+)(?:.*\1)2这样的正则表达式 它应该为您提供重复 3 次的最长字符串。 (反向捕获组1为答案)

编辑: 我不得不说取消这种方式。这取决于第一场比赛。除非它针对当前长度与迄今为止的最大长度进行测试,否则在迭代循环中,正则表达式不适用于此。

【讨论】:

【参考方案6】:
from collections import Counter

def Longest(string):

    b = []
    le = []

    for i in set(string):

        for j in range(Counter(string)[i]+1): 
            b.append(i* (j+1))

    for i in b:
        if i in string:
            le.append(i)


    return ([s for s in le if len(s)==len(max( le , key = len))])

【讨论】:

以上是关于查找字符串中最长的重复序列的主要内容,如果未能解决你的问题,请参考以下文章

数组篇在python中如何查找最长字符串子串

每日一题查找两个字符串中的最长公共子串

每日一题查找两个字符串中的最长公共子串

查找堆栈中最长的字符序列

是否有用于查找字符串中最长且不重复长度的子字符串的结构函数?

为 Python 查找最长重复字符串的有效方法(来自 Programming Pearls)