【问题标题】:Understanding the time complexity of the Longest Common Subsequence Algorithm了解最长公共子序列算法的时间复杂度
【发布时间】:2016-04-13 18:49:00
【问题描述】:

我不明白最长公共子序列算法的递归函数所具有的O(2^n) 复杂性。

通常,我可以将此表示法与算法的基本操作(在本例中为比较)的数量联系起来,但这一次在我看来没有意义。

例如,有两个长度相同的字符串5。在最坏的情况下,递归函数计算251 比较。而2^5 甚至还没有接近那个值。

谁能解释一下这个函数的算法复杂度?

def lcs(xstr, ystr):
    global nComp
    if not xstr or not ystr:
        return ""
    x, xs, y, ys = xstr[0], xstr[1:], ystr[0], ystr[1:]
    nComp += 1
    #print("comparing",x,"with",y)
    if x == y:
        return x + lcs(xs, ys)
    else:
        return max(lcs(xstr, ys), lcs(xs, ystr), key=len)

【问题讨论】:

  • 您可能希望包含您正在查看并试图理解的特定算法描述。
  • Big-O 复杂度决定了函数对于非常大的值如何增长,直至一个常数因子。一个算法可以是 O(1) 并且仍然需要 1 亿次操作。或O(n) 并为n=1 获取50000 次操作,为n=2 获取10000 次操作。这完全取决于它如何随着 N 的任意增长而增长。此外,5 是一个极小的n 值。没有算法,就没什么好说的了。
  • @CollinD 但是有两个字符串,它们的大小甚至可能不同,n 是什么?
  • 我正在将算法添加到帖子中。
  • 在这种情况下,n 可能是较长字符串的长度。

标签: algorithm recursion lcs subsequence


【解决方案1】:

要正确理解它,请仔细查看图表,并在阅读图表时遵循自上而下的递归方法。

Here, xstr = "ABCD"
      ystr = "BAEC"

                                    lcs("ABCD", "BAEC")       // Here x != y 
                                  /                     \  
                lcs("BCD", "BAEC")   <--  x==y   -->    lcs("ABCD", "AEC")  x==y
                          |                                        |
                          |                                        |
                  lcs("CD", "AEC")   <--  x!=y   -->     lcs("BCD", "EC")
                 /                \                     /                \
                /                  \                   /                  \
               /                    \                 /                    \
      lcs("D","AEC")                  lcs("CD", "EC")              lcs("BCD", "C")
    /                \              /               \              /        \       
lcs("", "AEC")        lcs("D","EC")                  lcs("CD", "C")        lcs("BCD","")
  |        \         /              \                       |             /       |
Return     lcs("", "EC")    lcs("D" ,"C")            lcs("D", "")   lcs("CD","")  Return
           /         \       /         \             /        \       /        \ 
        Return      lcs("","C")    lcs("D","") lcs("","")  Return  lcs("D","")  Return
                     /     \         /     \      /                 /      \
                  Return   lcs("","")       Return            lcs("", "")  Return
                                 |                                  |
                              Return                            Return

注意: 递归调用的正确表示方式通常是使用树方法来完成,但这里我使用图形方法只是为了压缩树,以便人们可以轻松理解递归调用走。而且,我当然很容易代表。


    1234563在lcs("BCD", "EC")。结果,这些对将在执行时被多次调用,这增加了程序的时间复杂度。
  • 您可以很容易地看到,每一对都会为其下一级生成两个结果,直到遇到任何 empty 字符串或 x==y。因此,如果字符串的长度是n,m (考虑xstr的长度是n,ystr是m,我们正在考虑最坏的情况)。然后,我们将在订单结束时得到数字结果:2n+m。 (怎么想?)

因为,n+m 是一个整数,比如说 N。因此,算法的时间复杂度为:O(2N),对于较大的N值来说效率不高。

因此,我们更喜欢 动态编程 方法而不是递归方法。它可以将时间复杂度降低到: O(nm) => O(n2) ,当 n == m.

即使是现在,如果你很难理解逻辑,我建议你为xstr = "ABC"ystr = "EF"。希望你能理解。

如有任何疑问,欢迎 cmets。

【讨论】:

  • 哇,好答案!最让我烦恼的是递归算法中的比较次数与 2^N 相差很大。对于动态编程,n*m 实际上也是比较的次数,所以人们会认为这应该〜总是〜适用。
  • 我们不能记忆递归 LCS 吗?
【解决方案2】:

O(2^n) 表示运行时间(2^n) 成比例足够大 n。这并不意味着这个数字是坏的、高的、低的或任何特定于 small n 的数字,并且它没有提供计算绝对运行时间的方法。

要了解其中的含义,您应该考虑 n = 1000、2000、3000 甚至 100 万、200 万等的运行时间。

在您的示例中,假设对于 n=5,该算法最多进行 251 次迭代,那么 O(n) 的预测是对于 n=50,它将采用 @ 987654326@ = 2^45*251 = ~8.8E15 迭代次数。

【讨论】:

  • 好的,我明白了,但是有两个字符串,它们的大小甚至可能不同,n 是什么?
  • 由于最长的公共子序列永远不会比较短的字符串长,它可能是较短字符串的长度。并不是说它太重要了
  • O(2^n) 并不意味着与 2^n 成正比。
  • @Aganju 它不是较长字符串的长度。考虑到字符串 a 是 2 个字符,字符串 b 是 100 个字符的情况,让我怀疑它是更长的一个。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-11
  • 2013-05-28
  • 2019-02-16
  • 1970-01-01
  • 2011-04-09
  • 2013-12-06
相关资源
最近更新 更多