【问题标题】:Why can the KMP failure function be computed in O(n) time?为什么可以在 O(n) 时间内计算 KMP 失效函数?
【发布时间】:2013-09-11 07:52:17
【问题描述】:

Wikipedia claims故障函数表可以在O(n)时间内计算出来。

让我们看看它的“规范”实现(在 C++ 中):

vector<int> prefix_function (string s) {
    int n = (int) s.length();
    vector<int> pi (n);
    for (int i=1; i<n; ++i) {
        int j = pi[i-1];
        while (j > 0 && s[i] != s[j])
            j = pi[j-1];
        if (s[i] == s[j])  ++j;
        pi[i] = j;
    }
    return pi;
}

为什么它在 O(n) 时间内工作,即使有一个内部 while 循环?算法分析我不是很厉害,谁能解释一下?

【问题讨论】:

  • 您的算法错误 - 向量 pi 未初始化!
  • @EdHeal:实际上是这样。 vector 将其值初始化为零。
  • @usamec:仍然是错误的,因为 pi[0] 通常为 -1(您不会因为第一个字符不匹配而回溯,而只会继续下一个字符)。

标签: c++ algorithm knuth-morris-pratt


【解决方案1】:

这一行: if (s[i] == s[j]) ++j;最多执行 O(n) 次。 它导致 p[i] 的值增加。请注意,p[i] 的起始值与 p[i-1] 相同。

现在这一行:j = pi[j-1];导致 p[i] 减少至少 1。并且由于它最多增加了 O(n) 次(我们也计算了以前值的增加和减少),它不能减少超过 O(n) 次。 所以它最多也执行了 O(n) 次。

因此整个时间复杂度为 O(n)。

【讨论】:

  • 不能。正如我所说,循环中的条件总共只能满足 O(n) 次。所以它总共执行了 O(n) 次。有时做一些比 O(n) 乘以 O(n) 更聪明的事情会更好。
【解决方案2】:

这里已经有两个答案是正确的,但我经常认为一个完整的答案 证明可以使事情更清楚。你说你想要一个 9 岁孩子的答案,但是 我不认为这是可行的(我认为很容易被愚弄以为这是真的 实际上没有任何直觉知道为什么它是真的)。也许解决这个答案会有所帮助。

首先,外部循环运行n 次很明显,因为i 没有被修改 循环内。循环中唯一可以运行多次的代码是 块

while (j > 0 && s[i] != s[j])
{   
    j = pi[j-1]
}   

那么它可以运行多少次?请注意,每次条件都是 满意我们减少j 的值,此时最多为 pi[i-1]。如果它达到 0,则 while 循环完成。要了解为什么这很重要, 我们首先证明一个引理(你是一个非常聪明的 9 岁孩子):

pi[i] <= i

这是通过归纳来完成的。 pi[0] &lt;= 0 因为它在 pi 的初始化中设置过一次,并且再也没有被触及过。然后归纳地让0 &lt; k &lt; n 并假设 该声明适用于0 &lt;= a &lt; k。考虑p[k] 的值。设置好了 恰好在pi[i] = j 中出现一次。那么j 能有多大呢?它已初始化 通过感应到pi[k-1] &lt;= k-1。在 while 块中,它可能会更新为pi[j-1] &lt;= j-1 &lt; pi[k-1]。通过另一个小归纳,您可以看到j 永远不会超过pi[k-1]。因此之后 while 循环我们仍然有 j &lt;= k-1。最后它可能会增加一次,所以我们有 j &lt;= k 和 pi[k] = j &lt;= k(这是我们完成归纳所需要证明的)。

现在回到原点,我们问“我们可以减少多少次 j"? 有了我们的引理,我们现在可以看到while 循环的每次迭代都会 单调减少j 的值。特别是我们有:

pi[j-1] <= j-1 < j 

那么这可以运行多少次?最多pi[i-1] 次。精明的读者可能会想 “你什么都没证明!我们有pi[i-1] &lt;= i-1,但它在while循环内,所以 它仍然是O(n^2)!”。稍微精明的读者会注意到这个额外的事实:

无论我们运行j = pi[j-1] 多少次,我们都会减少pi[i] 的值,这会缩短循环的下一次迭代!

例如,假设j = pi[i-1] = 10。但是在 while 循环的 ~6 次迭代之后,我们有 j = 3,假设它在 s[i] == s[j] 行中增加 1,所以 j = 4 = pi[i]。 那么在外循环的下一次迭代中,我们从j = 4开始...所以我们最多只能执行while 4次。

最后一个难题是++j 每个循环最多运行一次。所以这不是我们可以拥有的 在我们的pi 向量中是这样的:

0 1 2 3 4 5 1 6 1 7 1 8 1 9 1
           ^   ^   ^   ^   ^
Those spots might mean multiple iterations of the while loop if this 
could happen

为了使这个真正正式,你可以建立上面描述的不变量,然后使用归纳 显示while 循环运行的总 次,与pi[i] 相加最多为i。 由此可见,while 循环运行的total 次是O(n),这意味着整个外部循环具有复杂性:

O(n)     // from the rest of the outer loop excluding the while loop
+ O(n)   // from the while loop
=> O(n) 

【讨论】:

    【解决方案3】:

    让我们从外部循环执行 n 次的事实开始,其中 n 是我们寻找的模式的长度。内部循环将j 的值至少减少1,因为pi[j] &lt; j。循环最迟在j == -1 时终止,因此它最多可以减少j 的值,就像之前增加j++(外循环)一样频繁。由于j++ 在外循环中正好执行了n 次,所以内循环的总执行次数被限制为n。因此,预处理算法需要 O(n) 步。

    如果您介意,请考虑预处理阶段的这种更简单的实现:

    /* ff stands for 'failure function': */
    void kmp_table(const char *needle, int *ff, size_t nff)
    {
        int pos = 2, cnd = 0;
    
        if (nff > 1){
            ff[0] = -1;
            ff[1] = 0;
        } else {
            ff[0] = -1;
        }
    
        while (pos < nff) {
            if (needle[pos - 1] == needle[cnd]) {
                ff[pos++] = ++cnd;
            } else if (cnd > 0) {
                cnd = ff[cnd]; /* This is O(1) for the reasons above. */
            } else {
                ff[pos++] = 0;
            }
        }
    }
    

    很明显,失败函数是 O(n),其中 n 是所寻找的模式的长度。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-22
      • 2015-06-29
      • 1970-01-01
      • 2013-02-03
      • 2018-03-04
      • 2011-09-29
      • 1970-01-01
      • 2016-01-27
      相关资源
      最近更新 更多