(这个问题与音乐无关,但我以音乐为例 (一个用例。)
在音乐中,短语结构的一种常见方式是按音符序列 中间部分重复一次或多次。因此,这个短语 由引言、循环部分和输出部分组成。这里有一个 例如:
[ E E E F G A F F G A F F G A F C D ]
我们可以“看到”介绍是[E],重复部分是[F G A] F]输出为[cd]。因此,拆分列表的方法是
[ [ E E E ] 3 [ F G A F ] [ C D ] ]
其中第一项是简介,第二项是 重复部分重复,第三部分输出
我需要一个算法来执行这样的拆分
但有一点需要注意,那就是可能有多种方法 拆分列表。例如,上述列表可分为:
[ [ E E E F G A ] 2 [ F F G A ] [ F C D ] ]
但这是一个更糟糕的分裂,因为介绍和介绍更长。所以 该算法的标准是找到最大化 环件的长度,并使环件的组合长度最小 开场白和开场白。这意味着正确的分割
[ A C C C C C C C C C A ]
是
[ [ A ] 9 [ C ] [ A ] ]
因为导入和导出的组合长度是2,而 循环部分的长度为9
此外,虽然intro和outro可以是空的,但只有“true”重复是空的 允许。因此,不允许进行以下拆分:
[ [ ] 1 [ E E E F G A F F G A F F G A F C D ] [ ] ]
可以将其视为为为数据找到最佳的“压缩” 序列请注意,在某些序列中可能没有任何重复:
[ A B C D ]
对于这些退化情况,任何合理的结果都是允许的
以下是我对算法的实现:
def find_longest_repeating_non_overlapping_subseq(seq):
candidates = []
for i in range(len(seq)):
candidate_max = len(seq[i + 1:]) // 2
for j in range(1, candidate_max + 1):
candidate, remaining = seq[i:i + j], seq[i + j:]
n_reps = 1
len_candidate = len(candidate)
while remaining[:len_candidate] == candidate:
n_reps += 1
remaining = remaining[len_candidate:]
if n_reps > 1:
candidates.append((seq[:i], n_reps,
candidate, remaining))
if not candidates:
return (type(seq)(), 1, seq, type(seq)())
def score_candidate(candidate):
intro, reps, loop, outro = candidate
return reps - len(intro) - len(outro)
return sorted(candidates, key = score_candidate)[-1]
我不确定它是否正确,但它通过了我做的简单测试 描述。问题是,这是一种缓慢的方式。我已经看过了 在后缀树上,但它们似乎不适合我的用例,因为 我要寻找的子字符串应该是不重叠和相邻的
这是我对你所说内容的实现。它与您的非常相似,但它跳过了子字符串,这些子字符串已被检查为先前子字符串的重复
把你的三个例子都传给我。这似乎是一种可能有很多奇怪的边缘情况的事情,但考虑到它是一种优化的暴力,它可能更像是更新规范的问题,而不是修复代码本身的错误
看起来您正在尝试的是LZ77压缩算法。您可以对照我链接到的维基百科文章中的参考实现检查代码
这是一种明显是二次时间的方法,但常数因子相对较低,因为除了长度为1的子串对象外,它不构建任何子串对象。结果是一个2元组
其中
bestlen
是重复相邻块的最长子串的长度,每个结果是一个3元组这意味着正在重复的子字符串是
还有
numreps
个相邻的。永远都是这样问题描述留下了歧义。例如,考虑这个输出:
因此,它找到了5种方法来将“最长”的拉伸视为长度6:
它不返回intro或outro,因为从它返回的内容中可以推断出它们是微不足道的:
the_string[: start_index]
李>the_string[start_index + bestlen :]
李>如果没有重复的相邻块,则返回
其他示例(来自您的帖子):
其工作原理的关键:假设您有相邻的重复块,每个块的宽度为
进行比较时会发生什么情况W
。然后考虑当将原始字符串与由左移位的字符串{{CD5>}:然后在相同的位置获得
(N-1)*W
个连续的相等字符。但是也在另一个方向起作用:如果向左移动W
并找到(N-1)*W
个连续的相等字符,那么您可以推断:所以所有
N
块都必须是block1的重复因此,代码重复地将原始字符串左移(副本)一个字符,然后在这两个字符串上从左向右移动,以识别相等字符的最长长度。这只需要一次比较一对字符。为了使“左移”有效(恒定时间),字符串的副本存储在
collections.deque
中编辑:
update()
在许多情况下做了太多无用的工作;换了它使用regexp
[由于尺寸限制,已删除此项]
使用后缀数组
这是迄今为止我发现的最快的,尽管仍然可以激发成二次时间行为
请注意,是否找到重叠字符串并不重要。正如上文对
crunch2()
计划所解释的(此处以次要方式详述):n = len(s)
的字符串s
李>i
和j
与0 <= i < j < n
李>然后,如果
w = j-i
和c
是s[i:]
和s[j:]
之间共同的前导字符数,则从s[i]
开始,重复块s[i:j]
(长度为w
),共1 + c // w
次下面的程序直接按照该步骤查找所有重复的相邻块,并记住最大总长度的块。返回与
crunch2()
相同的结果,但有时顺序不同后缀数组简化了搜索,但很难消除它。后缀数组直接查找具有最大
c
的<i, j>
对,但仅限制搜索以最大化w * (1 + c // w)
。最糟糕的情况是letter * number
形式的字符串,如"a" * 10000
我没有给出下面}的输出:
sa
模块的代码。这是一个冗长的过程,任何后缀数组的实现都会计算相同的内容。{sa
是后缀数组,是range(n)
的唯一排列,因此对于range(1, n)
中的所有i
,s[sa[i-1]:] < s[sa[i]:]
rank
在这里不使用对于
range(1, n)
中的i
,lcp[i]
给出从sa[i-1]
开始的后缀和sa[i]
之间最长公共前缀的长度为什么它会赢?部分原因是它从不必搜索以使用相同的字母(后缀数组通过构造使它们相邻),并检查重复的块,以及它是否是新的最佳块,无论块有多大或重复了多少次,都需要很短的恒定时间。如上所述,这只是关于
c
和w
的简单算术免责声明:后缀数组/树对我来说就像连分数:我可以在必要时使用它们,并且可以对结果感到惊奇,但它们让我头疼。易怒,易怒,易怒
一些时间安排
我把一小段英文单词读入一个字符串,
xs
。每行一个字:因此,大约210K字节中有25K个字。这些是二次时间算法,所以我没想到它会运行得很快,但是
crunch2()
在几个小时后仍然在运行,而当我让它运行一夜时,仍然在运行这使我意识到它的
update()
函数可能会做大量无用的工作,使算法总体上更像立方时间。所以我把它修好了。然后:这花了大约38分钟,与我预期的差不多
regexp版本
crunch3()
只花了不到十分之一秒的时间正如前面所解释的,regexp版本可能找不到最佳答案,但这里还有其他一些东西在起作用:默认情况下,“.”与换行符不匹配,因此代码实际上进行了许多tiny搜索。文件中的~25K个换行符中的每一个都有效地结束了本地搜索范围。使用
re.DOTALL
标志编译regexp(因此不专门处理换行):14分钟多一点
最后,
不到9分钟。构建后缀数组的时间只是其中微不足道的一部分(不到一秒钟)。这实际上相当令人印象深刻,因为并非总是正确的蛮力regexp版本速度较慢,尽管它几乎完全以“C速度”运行
但这是相对的。从绝对意义上说,所有这些仍然是缓慢的:——
注意:下一节中的版本将此时间缩短到5秒以下(!)
快得多
这一个采用了完全不同的方法。对于上面的大型词典示例,它在不到5秒钟的时间内得到了正确的答案
我对此感到相当自豪;-)这是出乎意料的,我以前从未见过这种方法。它不做任何字符串搜索,只对索引集进行整数运算
对于形式为
letter * largish_integer
的输入,它仍然非常慢。按原样,只要子字符串(当前长度的)至少存在两个副本(不一定相邻,甚至不重叠!),它就会继续增加1。例如,在它将尝试从1到999999的所有子字符串大小
但是,通过将当前大小重复增加一倍(而不是仅仅添加1),保存类,执行混合形式的二进制搜索来定位存在重复的最大子字符串大小,似乎可以极大地改善这一点
我将把它作为一个毫无疑问的乏味练习留给读者。我在这里的工作已经完成;-)
而且更快
crunch6()
在我的框中,将较大的字典示例的时间降低到2秒以下。它结合了crunch4()
(后缀和lcp数组)和crunch5()
(在一组索引中查找具有给定步长的所有算术级数)的思想与
crunch5()
类似,它也会循环多次,循环次数等于重复的最长子串的长度(重叠与否)的1倍。因为如果没有长度为n
的重复,那么任何大于n
的大小也没有重复。这使得查找重复而不考虑重叠变得更容易,因为这是一个可利用的限制。当将“胜利”约束到相邻的重复时,这种情况就发生了。例如,“abcabc”中没有偶数长度为1的相邻重复,但有一个长度为1的相邻重复3.这似乎使得任何形式的直接二进制搜索都是徒劳的(大小为n
的相邻重复序列的存在或不存在与任何其他大小的相邻重复序列的存在无关)形式为
'x' * n
的输入仍然很糟糕。存在从1到n-1
的所有长度的重复观察:我给出的所有程序都会生成所有可能的方法来分解最大长度的重复相邻块。例如,对于9“x”的字符串,它表示可以通过重复“x”9次或重复“xxx”3次来获得。因此,令人惊讶的是,它们也可以用作因式分解算法;-)
总是快,出版,但有时错
在生物信息学中,这是在“串联重复”、“串联阵列”和“简单序列重复”(SSR)的名称下研究的。您可以搜索这些术语来查找相当多的学术论文,其中一些声称是最坏情况下的线性时间算法
但这些似乎分为两大阵营:
在第一个阵营中,有几篇文章可以归结为上面的
crunch4()
,但是没有它的内部循环crunch4a()
总是很快,但有时是错误的。事实上,它为这里出现的每个示例找到至少一个最大重复拉伸,在几分之一秒内解决了较大的字典示例,并且对形式为'x' * 1000000
的字符串没有问题。大部分时间用于构建后缀和lcp阵列。但它可能会失败:问题是不能保证相关后缀在后缀数组中是相邻的。以“b”开头的后缀按如下顺序排列:
用这种方法找到后面的重复块需要比较第一个和第三个。这就是为什么
crunch4()
有一个内环,尝试所有以一个公共字母开头的对。相关对可以由后缀数组中任意数量的其他后缀分隔。但这也使得算法的时间是二次的O(n日志n)
这篇论文对我来说很合适,尽管我还没有对它进行编码:
然而,使用次二次算法需要做出一些折衷。例如,
"x" * n
有n-1
形式的子串"x"*2
,"x"*3
形式的n-2
,…,因此只有O(n**2)
这些子串。因此,找到所有子串的任何算法也必然处于最佳二次时间详细阅读本文;-)您正在寻找的一个概念是“原语”:我相信您只想要形式
S*n
的重复,其中S
本身不能表示为较短字符串的重复。例如,"x" * 10
是原语,但"xx" * 5
不是通往
O(n log n)
的一步crunch9()
是我在评论中提到的“暴力”算法的一个实现,来自:那里的实现草图只找到“分支串联”重复,我在这里添加了代码来推断任意数量的重复,并包括非分支重复。虽然它仍然是
O(n**2)
最坏的情况,但对于您在注释中指向的seq
字符串,它比这里的任何东西都要快。照原样,它可以复制(除订单外)与这里的大多数其他程序一样详尽无遗该报继续努力争取将最坏的情况削减到
O(n log n)
,但这大大减慢了它的速度。因此,它更努力了。我承认我失去了兴趣;-)赢取奖励积分;-)
对于某些技术意义;-)
crunch11()
是最坏情况的O(n logn)。除了后缀和lcp数组之外,还需要rank
数组^{正如代码注释所指出的,它还依赖于Python3的速度(
range()
行为)。这很肤浅,但重写起来会很乏味描述这一点的文章有几个错误,所以如果这段代码与您读到的内容不完全匹配,请不要发火。相反,严格执行他们所说的,它将失败
也就是说,代码变得异常复杂,我不能保证没有bug。我试过的所有东西都能用
形式为
'x' * 1000000
的输入仍然不是快速的,但显然不再是二次时间。例如,一个字符串将同一个字母重复一百万次,在近30秒内完成。这里的大多数其他项目永远不会结束;-)编辑:将
genlcpi()
更改为使用半开放Python范围;对crunch11()
进行了大部分表面上的改变;增加了“提前退出”,在最糟糕的情况下(如'x' * 1000000
)可以节省大约三分之一的时间相关问题 更多 >
编程相关推荐