分词递归解决方案的时间复杂度?

Posted

技术标签:

【中文标题】分词递归解决方案的时间复杂度?【英文标题】:Time complexity of the word break recursive solution? 【发布时间】:2015-07-12 17:39:12 【问题描述】:

从http://www.geeksforgeeks.org/dynamic-programming-set-32-word-break-problem/获取的代码中递归解决方案的时间复杂度是多少:

// returns true if string can be segmented into space separated
// words, otherwise returns false
bool wordBreak(string str)

    int size = str.size();

    // Base case
    if (size == 0)  return true;

    // Try all prefixes of lengths from 1 to size
    for (int i=1; i<=size; i++)
    
        // The parameter for dictionaryContains is str.substr(0, i)
        // str.substr(0, i) which is prefix (of input string) of
        // length 'i'. We first check whether current prefix is in
        // dictionary. Then we recursively check for remaining string
        // str.substr(i, size-i) which is suffix of length size-i
        if (dictionaryContains( str.substr(0, i) ) &&
            wordBreak( str.substr(i, size-i) ))
            return true;
    

    // If we have tried all prefixes and none of them worked
    return false;

我在想它的 n^2 因为对于该方法的 n 次调用,最坏的情况是 (n-1) 工作(递归地遍历字符串的其余部分?)。还是指数/n!?

我很难弄清楚这些递归函数的 Big(O)。非常感谢任何帮助!

【问题讨论】:

【参考方案1】:

答案是指数,确切地说是O(2^(n-2))(2 power n-2)

在每次调用中,您都在调用长度为 1,2....n-1 的递归函数(在最坏的情况下)。要完成长度为n 的工作,您需要递归地完成所有长度为n-1, n-2, ..... 1 的字符串的工作。所以 T(n) 是你当前调用的时间复杂度,你在内部做sum of T(n-1),T(n-2)....T(1)的工作。

数学:

  T(n) = T(n-1) + T(n-2) +.....T(1);
  T(1) = T(2) = 1 

如果你真的不知道如何解决这个问题,解决上述重复的更简单方法是替换值。

  T(1) = T(2) = 1
  T(3) = T(1) + T(2) = 1+1 =2; // 2^1
  T(4) = T(1)+ T(2) + T(3) = 1+1+2 =4; //2^2
  T(5) = T(1) + T(2) +T(3) +T(4) = 1+1+2+4 =8; //2^3

所以如果你替换前几个值,很明显时间复杂度是2^(n-2)

【讨论】:

我了解了最坏情况的递归调用是如何编写的,但是在可视化实际上导致最坏情况的字符串时遇到了麻烦。是不是要执行这种最坏的情况,每个字符前缀都应该在字典中,例如 s = 'abcd', a,b,c,ab,ac 等,但最后一个字符,例如。 d 这里应该不存在,以确保所有最坏情况的调用都实际发生 如果我们使用一个布尔数组来存储从索引开始的子字符串是否包含在dict中怎么办?在这种情况下,除非是第一次,否则不会进行任何工作,恕我直言,时间复杂度将降至 n^2,即子字符串的总数。请让我知道这是否正确。 我同意你的结论,但有两个警告。首先,O(2^n-2) 与 O(2^n) 相同,因为 2^n-2 = (2^n) / 4 并且 big-O 咀嚼领先的系数。其次,您的分析忽略了使用 .substr 计算子字符串的成本和字典查找的成本。结果证明这些并不重要,但事实并非如此。【参考方案2】:

我相信答案实际上应该是O(2^(n-1))。你可以在这里看到一个证明和一个最坏的例子:

https://leetcode.com/problems/word-break/discuss/169383/The-Time-Complexity-of-The-Brute-Force-Method-Should-Be-O(2n)-and-Prove-It-Below

【讨论】:

【参考方案3】:

简短版:

这个函数最坏情况的运行时间是Θ(2n),这很令人惊讶,因为它忽略了每个递归调用所做的二次工作量,只是简单地分裂将字符串分成几部分并检查哪些前缀是单词。

更长的版本:假设我们有一个输入字符串,包含 n 个字母 a 的副本,后跟字母 b。 (我们将其缩写为aⁿb),并创建一个包含单词aaaaaa、...、aⁿ的字典。

现在,递归会做什么?

首先,请注意所有递归调用都不会返回 true,因为无法说明字符串末尾的 b。这意味着每个递归调用都将指向aᵏb 形式的字符串。让我们表示处理 T(k) 这样的字符串所需的时间。这些调用中的每一个都会触发 k 个较小的调用,每个后缀为 aᵏb

但是,我们还必须考虑运行时的其他贡献者。特别是,调用string::substr 来形成一个长度为 k 的子串需要时间 O(k)。我们还需要考虑检查前缀是否为单词的成本。此处未显示如何执行此操作的代码,但假设我们使用 trie 或哈希表,我们可以假设检查长度为 k 的字符串是否为单词的成本也是 O(k)。这意味着,在我们进行递归调用的每一点,我们将做 O(n) 的工作——检查前缀是否为单词的一些工作,以及形成与后缀对应的子字符串的一些工作。

因此,我们明白了

T(k) = T(0) + T(1) + T(2) + ... + T(k-1) + O(k2)

这里,循环的第一部分对应于每个递归调用,循环的第二部分说明了制作每个子字符串的成本。 (有 n 个子串,每个子串都需要时间 O(n) 来处理)。我们的目标是解决这种重复,为了简单起见,我们假设 T(0) = 1。

为此,我们将使用“扩展和收缩”技术。让我们把 T(k) 和 T(k+1) 的值挨个写出来:

T(k) = T(0) + T(1) + T(2) + ... + T(k-1) + O(k2)

T(k+1) = T(0) + T(1) + T(2) + ... + T(k-1) + T(k) + O(k2支持>)

从第二个表达式中减去第一个表达式可以得到

T(k+1) - T(k) = T(k) + O(k),

或者那个

T(k+1) = 2T(k) + O(k)。

我们如何从两个 O(k2) 项的差中得到 O(k)?这是因为 (k + 1)2 - k2 = 2k + 1 = O(k)。

这是一种更容易重复使用的方法,因为每个术语都依赖于前一个术语。为简单起见,我们将假设 O(k) 项实际上就是 k,给出递归

T(k+1) = 2T(k) + k.

这个递归求解 T(k) = 2k+1 - k - 1。要看到这一点,我们可以使用快速归纳论证。具体来说:

T(0) = 1 = 2 - 1 = 20+1 - 0 - 1

T(k+1) = 2T(k) + k = 2(2k - k - 1) + k = 2k+1 - 2k - 2 + k = 2k+1 - k - 2 = 2k+1 - (k + 1) - 1

因此,我们得到我们的运行时间是 Θ(2n),因为我们可以忽略低阶 n 项。

看到这一点我感到非常惊讶,因为这意味着每个递归调用完成的二次工作不会影响整个运行时间!在进行此分析之前,我最初会猜测运行时会类似于 Θ(n · 2n)。 :-)

【讨论】:

我认为你的意思是 T(0) = 1 = 2 - 1 = 2^(0+1) - 0 - 1 而不是 T(0) = 1 = 2 - 1 = 2^0 - 0 - 1【参考方案4】:

我认为这里的复杂性的一个直观方式是,有多少种方法可以在此处在字符串中添加空格或在此处中断单词?

对于 4 个字母的单词: 没有在索引 0-1 处中断的方法 * 在索引 1-2 处没有中断的方法 * 在索引 2-3 处没有中断的方法 = 2 * 2 * 2。

2 表示两个选项 => 你打破它,你不打破它

O(2^(n-1)) 是分词的递归复杂度 then ;)

【讨论】:

以上是关于分词递归解决方案的时间复杂度?的主要内容,如果未能解决你的问题,请参考以下文章

时间复杂度如何从蛮力变为递归解决方案?

应该选择递归而不是迭代?

使用递归方程的程序的时间复杂度

递归算法时间复杂度分析与改善

递归函数时间复杂度分析(转)

递归算法的空间复杂度