我想知道以下算法的复杂性,最重要的是,逐步推导出它的过程。
我怀疑它是O(长度(文本)^ 2 *长度(模式))但我无法解决递归方程。
在递归调用上进行memoization(即动态编程)时,复杂性会如何提高?
此外,我希望能够帮助我学习如何分析这种算法的技巧/书籍指南。
在Python中:
def count_matches(text, pattern):
if len(pattern) == 0: return 1
result = 0
for i in xrange(len(text)):
if (text[i] == pattern[0]):
# repeat the operation with the remaining string a pattern
result += count_matches(text[i+1:], pattern[1:])
return result
在C:
int count_matches(const char text[], int text_size,
const char pattern[], int pattern_size) {
if (pattern_size == 0) return 1;
int result = 0;
for (int i = 0; i < text_size; i++) {
if (text[i] == pattern[0])
/* repeat the operation with the remaining string a pattern */
result += count_matches(text+i, text_size-(i+1),
pattern+i, pattern_size-(i+1));
}
return result;
}
注意:算法故意重复每个子字符串的匹配。请不要关注算法执行的匹配,只关注其复杂性。
为算法中的(现已修复)拼写错误道歉
答案 0 :(得分:2)
我的直觉是复杂度为O(长度(文本)^ 3)是不正确的。它实际上是O(n!),纯粹是因为实现的形式是
def do_something(relevant_length):
# base case
for i in range(relevant_length):
# some constant time work
do_something(relevant_length - 1)
中讨论过
如果使用memoization,则会生成一次递归树,然后每次都会查找。
描绘递归树的形状。
我们每层进行一个字符的进度。有2个基本案例。当我们到达模式结束时,如果文本中不再有任何要迭代的字符,则递归最低点。第一个基本情况是显式的,但第二个基本情况恰好在实现时发生。
因此递归树的深度(高度)是min [length(text),length(pattern)]。
有多少子问题?我们还每层进行一个字符的进度。如果比较文本中的所有字符,使用高斯技巧求和S = [n(n + 1)] / 2,将在所有递归层中评估的子问题总数为{length(text)* [篇(文字)+ 1]} / 2。
取长度(文本)= 6和长度(模式)= 10,其中长度(文本)&lt;长度(图案)。深度为min [长度(文本),长度(模式)] = 6.
PTTTTT
PTTTT
PTTT
PTT
PT
P
如果长度(文本)= 10且长度(模式)= 6,则长度(文本)&gt;长度(图案)。深度为min [长度(文本),长度(模式)] = 6.
PTTTTTTTTT
PTTTTTTTT
PTTTTTTT
PTTTTTT
PTTTTT
PTTTT
我们看到的是长度(模式)并不真正有助于复杂性分析。在长度(模式)<1的情况下。长度(文字),我们只是扼杀了一点高斯和。
但是,由于文本和模式一对一地向前迈进,我们最终做的工作少得多。递归树看起来像方阵的对角线。
对于length(text)= 6和length(pattern)= 10以及length(text)= 10和length(pattern)= 6,树是
P
P
P
P
P
P
因此,记忆方法的复杂性是
O(min(长度(文本),长度(模式)))
编辑:给@fons注释,如果从未触发递归怎么办?特别是在所有i的text [i] == pattern [0]永远不变的情况下。然后迭代所有文本是主导因素,即使长度(文本)&gt;长度(图案)。
这意味着记忆方法的实际上限是
O(最大(长度(文本),长度(模式)))
在长度(文本)&gt;的情况下考虑更多一点。即使模式耗尽,也会触发长度(模式)和递归IS,它需要恒定的时间来递归并检查模式现在是否为空,因此长度(文本)仍占主导地位。
这使得te记忆版本O的上限(长度(文本))。
答案 1 :(得分:0)
嗯...我可能错了,但据我所知,你的运行时应该专注于这个循环:
for c in text:
if (c == pattern[0]):
# repeat the operation with the remaining string a pattern
result += count_matches(text[1:], pattern[1:])
基本上让文字的长度为 n ,我们不需要模式的长度。
第一次运行此循环(在父函数中),我们将对其进行 n 调用。在最糟糕的情况下,每次 n 调用都会调用程序的 n-1 个实例。然后,那些 n-1 实例将在最坏的情况下调用 n-2 实例,依此类推。
这导致一个方程式 n *(n-1)(n-2) ... * 1 ,这是 n !即可。所以最糟糕的情况是 O(n!)。非常糟糕(:
我使用输入运行你的python程序几次会导致最糟糕的运行时:
在[21]中:count_matches(&#34; aaaaaaa&#34;,&#34; aaaaaaa&#34;)
Out [21]:5040
在[22]中:count_matches(&#34; aaaaaaaa&#34;,&#34; aaaaaaaa&#34;)
Out [22]:40320
在[23]中:count_matches(&#34; aaaaaaaaa&#34;,&#34; aaaaaaaaa&#34;)
出[23]:362880
最后一个输入是9个符号和9个! = 362880。
要分析算法的运行时间,首先需要考虑导致最差可能运行时的输入。在您的算法中,最佳和最差的变化相当大,因此您可能需要进行平均案例分析,但这非常复杂。 (您需要定义哪些输入是平均值,以及最坏情况的常见程度。)
动态编程可以帮助缓解运行时间,但分析更难。让我们的第一个代码成为一个简单的未经优化的动态编程版本:
cache = {}
def count_matches_dyn(text, pattern):
if len(pattern) == 0: return 1
result = 0
for c in text:
if (c == pattern[0]):
# repeat the operation with the remaining string a pattern
if ((text[1:], pattern[1:]) not in cache.keys()):
cache[(text[1:], pattern[1:])] = count_matches_dyn(text[1:], pattern[1:])
result += cache[(text[1:], pattern[1:])]
else:
result += cache[(text[1:], pattern[1:])]
return result
这里我们缓存对字典中count_matches的所有调用,所以当我们用相同的输入调用count匹配时,我们将得到结果,而不是再次调用该函数。 (这被称为memoization)。
现在让我们分析一下。主循环
for c in text:
if (c == pattern[0]):
# repeat the operation with the remaining string a pattern
if ((text[1:], pattern[1:]) not in cache.keys()):
cache[(text[1:], pattern[1:])] = count_matches_dyn(text[1:], pattern[1:])
result += cache[(text[1:], pattern[1:])]
else:
result += cache[(text[1:], pattern[1:])]
第一次调用时会运行 n 次(我们的缓存为空)。但是,第一个递归调用将填充缓存:
cache[(text[1:], pattern[1:])] = count_matches_dyn(text[1:], pattern[1:])
同一循环中的每个其他调用都会花费(O(1)。所以基本上顶级递归将花费 O(n-1)+(n-1)* O(1)= O(n-1)+ O(n-1)= 2 * O(n-1)。你可以看到,从递归下来的调用中只有第一个将下降许多递归调用( O(n-1)调用),其余的将花费 O(1),因为它们只是字典查找。鉴于所有这些都说运行时(2 * O(n-1),摊销为 O(n)。
声明。我不完全确定动态编程版本的分析,请随时纠正我(:
免责声明2.动态编程代码包含昂贵的操作(文本[1:],模式[1:]),这些操作在分析中不作为因素。这是有目的的,因为在任何合理的实现中,您可以大大降低这些调用的成本。重点是展示简单缓存如何大幅减少运行时间。
答案 2 :(得分:0)
Python版本似乎将pattern
的出现次数计算为text
的{{3}}。 C版本目前看起来很糟糕,所以我假设Python版本是正确的。
该函数通过将0和1相加来计算答案。因此,操作的数量至少是为了得到答案而需要加起来的1的数量,即答案本身。
(text, pattern)
,它将为给定长度的text
和pattern
提供最差的运行时间。最大的答案显然是某些字母相等的情况。
当所有字母相等时,答案基本上是从k = len (pattern)
中选择n = len (text)
项(字母)的方式的数量,即subsequence。
text
和pattern
的长度,这会给我们带来最糟糕的复杂性。例如:对于text = 'a' * 100
和pattern = 'a' * 50
,我们得到答案choose (100, 50) = 100! / 50! / 50!
。
通常,对于text
的固定长度,pattern
的长度必须是其中的一半,如有必要,可以在任意一侧舍入。
这是看到choose (n, k)时得到的直观概念。
形式上,通过手工比较choose (n, k)
和choose (n, k+-1)
来证明这一点很简单。
总和choose (n, 0) + choose (n, 1) + ... + choose (n, n)
是2 n ,再次直观地说,choose (n, n/2)
是相当一部分。
更正式的是,Pascal's triangle Stirling's formula choose (n, n/2)
大约为2 n 除以sqrt(n)
。
当复杂度呈指数时,我们通常对精确多项式因子不太感兴趣。
比方说,2 100 (O (2^n)
)和100次2 100 (O (n * 2^n)
)操作同样不可能在合理的时间内完成。
重要的是将O (2^n)
减少到O (2^(n/2))
,或者更好,以找到多项式解。
实际上,如果我们在顶部添加以下行,那么复杂性确实会choose (len (text), len (pattern)
乘以某个多项式:
if len(pattern) < len(text): return 0
实际上,如果文本中剩余的字母数小于模式的长度,则无法匹配。
否则,我们可以有更多的递归分支,最终导致在答案中加0。
从另一方面来看,我们可以证明未更改代码中的操作数量可以高达len(text)
的幂。
确实,在text = 'a' * n
和pattern = 'a' * n
时,假设我们已经处理了k
的{{1}}个字母。
这些字母中的每一个都独立于其他字母,可以与text
的某个字母匹配,也可以在循环中省略。
因此,对于pattern
的每个字母,我们有两种方法可供使用,因此当我们处理text
2^n
的{{1}}个字母时,我们有n
个方法,即到达终止我们递归函数的调用。
答案 3 :(得分:0)
时间复杂度应该提高到某种程度 O(长度(文本)*长度(模式))来自递归的(O(n!))。
记忆解决方案(DP)将涉及构建text-vs-pattern的查找表,可以从文本和模式的末尾开始逐步建立。
答案 4 :(得分:-1)
我担心你的算法对于模式匹配是不正确的。 主要是因为一旦发现第一个字符匹配,它将在文本的其余部分中搜索子子字符串。 例如,对于文本“abbccc”和模式“accc”,您的算法将返回等于1的结果。
您应该考虑为模式匹配实现“朴素”算法,这与您尝试的非常相似,但没有递归。 其复杂度为O(n * m),其中'n'是文本长度,'m'是模式长度。 在Python中,您可以使用以下实现:
text = "aaaaabbbbcccccaaabbbcccc"
pattern = "aabb"
result = 0
index = text.find(pattern)
while index > -1:
result += 1
print index
index = text.find(pattern, index+1)
return result
关于这个主题的书籍,我最好的建议是Cormen的"Introduction to Algorithms",它涵盖了算法和复杂性的所有材料。