【问题标题】:Memoization algorithm time complexity记忆算法时间复杂度
【发布时间】:2014-02-11 22:54:43
【问题描述】:

看了这篇Retiring a Great Interview Problem的文章,作者想出了一个work break的问题,给出了三个解决方案。高效的使用memoization算法,作者说它的最坏情况时间复杂度是O(n^2),因为the key insight is that SegmentString is only called on suffixes of the original input string, and that there are only O(n) suffixes

但是,我很难理解为什么是O(n^2)。有人可以给我一个提示或证据吗?

Work Break Problem: 
    Given an input string and a dictionary of words,
    segment the input string into a space-separated
    sequence of dictionary words if possible. For
    example, if the input string is "applepie" and
    dictionary contains a standard set of English words,
    then we would return the string "apple pie" as output.

记忆算法来自 Retiring a Great Interview Problem

Map<String, String> memoized;

String SegmentString(String input, Set<String> dict) {
      if (dict.contains(input)) 
          return input;
      if (memoized.containsKey(input) {
          return memoized.get(input);
      }
      int len = input.length();
      for (int i = 1; i < len; i++) {
          String prefix = input.substring(0, i);
          if (dict.contains(prefix)) {
              String suffix = input.substring(i, len);
              String segSuffix = SegmentString(suffix, dict);
              if (segSuffix != null) {
                  return prefix + " " + segSuffix;
              }
          }
      }
      memoized.put(input, null);
      return null;
}

【问题讨论】:

  • 解决方案不是最优的,它只在字符串输入的结果为空时更新memoized map,应该在所有情况下更新!
  • 没错。请注意,segSuffix 仅在 prefix 位于 dict 中的单词中时才会被递归调用。如果该递归调用返回非null,则调用者也会立即返回,在堆栈上。换句话说,只要segSuffix 调用返回一个不是null 的值,算法就不会再调用segSuffix,所以不需要记住非null 的值。
  • @Alp 很好,谢谢。在这种情况下,我猜只有一个布尔数组就足够了,因为我们总是可以将后缀作为它在整个输入字符串中的索引。
  • 你们能帮我解释一下为什么是O(n^2)吗?

标签: algorithm recursion time-complexity memoization


【解决方案1】:

一般来说,记忆表的维度决定了复杂性。

对于只有 1 维的记忆表 (memoized[n]) -> 需要 O(n) 复杂度, 对于只有 2 维的记忆表 (memoized[n][n]) -> 需要 O(n^2) 复杂度 等等。

原因: 在记忆的情况下,最坏情况的复杂性是输入的运行时间,其中没有一个情况(重叠的子问题)尚未缓存(预先计算)。

现在,假设 memoization 表有 2 个维度(memoization[n][n])。最坏情况复杂度只能是记忆表的最大维度。因此,其最坏的情况只能达到 O(n^2)。

因此,Memoization 表的维度基本上决定了最坏情况的时间复杂度。

@shx2 的答案以数学方式解释了这一点。

【讨论】:

    【解决方案2】:

    直觉

    (很明显,最坏的情况是没有解决方案的情况,所以我专注于此)

    由于递归调用是在将值放入记忆缓存之前进行的,因此最后一个(最短的)后缀将首先被缓存。这是因为函数首先用长度为 N 的字符串调用,然后用长度为 N-1 的字符串调用自身,然后 .... ,用 len 0 的字符串,被缓存并返回,然后长度为1的字符串被缓存并返回,...,长度为N的字符串被缓存并返回。

    正如提示所暗示的,只有后缀被缓存,并且只有 N 个。这意味着当顶级函数获得其第一次递归调用的结果时(即在长度为N-1 的后缀上),缓存中已经填充了N-1 个后缀。

    现在,假设 N-1 个最后的后缀已经被缓存,for 循环需要进行 N-1 个递归调用,每个调用 O(1)(因为答案已经被缓存),总计 O(N)。但是,(预)构建最后 N-1 的缓存需要 O(N^2)(解释如下),总计 O(N)+O(N^2) = O(N^2)。


    数学归纳证明

    这个解释可以很容易地转化为使用归纳法的正式证明。这是它的要点:

    f(N) 是函数在长度为 N 的输入上完成所需的操作数)

    归纳假设——存在一个常数c s.t. f(N) &lt; c*N^2

    基本情况很简单——对于长度为 1 的字符串,您可以找到一个常量 c 使得 f(1) &lt; c*N^2 = c

    诱导步骤

    回顾事情发生的顺序:

    第 1 步:函数首先在长度为 N-1 的后缀上调用自身,构建一个缓存,其中包含最后 N-1 个后缀的答案

    第 2 步:然后函数调用自身 O(N) 多次,每次花费 O(1)(感谢此测试:if (memoized.containsKey(input)),以及缓存已在第 1 步中填充的事实)。

    所以我们得到:

    f(N+1) = f(N) + a*N <   (by the hypothesis)
           < c*N^2 + a*N <  (for c large enough)
           < c*N^2 + 2*c*N + c =
           = c*(N+1)^2
    

    因此我们得到了f(N+1) &lt; c*(N+1)^2,这就完成了证明。

    【讨论】:

      【解决方案3】:

      首先,对于一个长度为N的字符串,我们可以将它分成N*(N-1)/2段,并检查每个段是否包含在字典中。这个成本是 O(N^2)

      回到你的代码,从字符串 N 开始,我们把它分解成两个更小的字符串,长度 'a''N - a' .而对于每个从 0 开始到 'a' 结束的子字符串(或前缀),我们只检查一次!

      从每个段 N - a 中,它也会检查它的每个前缀并将其存储到记忆表中,所以,这一步将确保下一次,当我们做同样的事情时移动,在这个确切的位置拆分字符串,我们只需要返回结果,无需进一步工作(假设地图将在 O(1) 中检索并返回特定字符串的结果)。这个存储和检索步骤还确保征服和分割只进行一次。

      因此,在第一点和第二点之后,我们得出结论,只有一次需要检查 N*(N - 1)/2 段,这导致成本是 O(N^2)

      注意:仅假设 两者的成本 dict.contains(input)memoized.containsKey(input)O(1) 所以复杂度是 O(N^2)。

      【讨论】:

      • 你能解释一下为什么我们可以把它分解成N*(N-1)/2 segments吗?我认为应该是2^N,因为对于输入字符串中的每个字母,它可能包含也可能不包含在段中。
      • @mitchellc 做一个段,我们需要一个起点和一个终点,所以在索引i,从它​​开始的段的终点是i+1i+2,.. . 所以,索引 0 可以有 n 段,索引 1 有 n-1 段...从 n 到 1 的总和公式是 n*(n-1)/2
      • 好吧,我还是很困惑。正如文章中作者解释的那样,which reduces the analysis to determine the number of possible segmentations. I leave it as an exercise to the reader (with this hint) to determine that this number is O(2^n).(见thenoisychannel.com/2011/08/08/…中的General solution
      • @mitchelllc 因为地图存储了所有的可能性,可以帮助您避免重做任何步骤,并且总共有 O(N^2) 种情况,每个只计算一次,所以我们有结论。数学在这里不起作用,因为递归公式是不够的,在某些情况下,它会考虑一些先前的步骤(例如在索引 1、4、5 处拆分字符串,返回然后在索引 2、4、5 处再次拆分. 之后的两次拆分重新计算),从而使公式的结果高于应有的值。尝试移除地图,可以看到索引是如何遍历的。
      • @mitchelllc 重读了 shx2 的答案后,我认为他的答案是正确的 :) 重点是关注函数的第一步
      【解决方案4】:

      如果输入的长度为 N,那么它可以包含的最大字典单词数将是 N。因此您需要检查长度为 1..N 的所有不同组合。即 N(N-1)/2

      获取输入字符串 aaaaa

      有 N x 'a' 个字符串

      N-1 'aa' 字符串

      等等

      【讨论】:

      • 感谢您的快速回复!你能解释一下为什么它可以包含的字典单词的最大数量是 N 吗?
      • @mitchellc:因为每个单词至少有 1 个字母。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-07-25
      • 1970-01-01
      • 2012-05-09
      • 1970-01-01
      • 1970-01-01
      • 2010-10-17
      • 2013-12-31
      相关资源
      最近更新 更多