【问题标题】:Why is my algorithm timing out?为什么我的算法超时?
【发布时间】:2018-08-02 02:52:12
【问题描述】:

这是问题(来自 Leetcode):

Given an unsorted array of integers, find the length of longest increasing subsequence.

Example:

Input: [10,9,2,5,3,7,101,18]
Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 
Note:

There may be more than one LIS combination, it is only necessary for you to return the length.
Your algorithm should run in O(n2) complexity.

这是我的解决方案:

memo = {} 
def lis_calc(lower_bound, offset):
    if memo.get((lower_bound, offset), None):
        return memo[(lower_bound, offset)]
    if offset >= len(nums):
        return 0
    if nums[offset] > lower_bound:
        res = max(1 + lis_calc(nums[offset], offset + 1), lis_calc(lower_bound, offset + 1))
    else:
        res = lis_calc(lower_bound, offset + 1)

    memo[(lower_bound, offset)] = res 
    return memo[(lower_bound, offset)]

在最坏的情况下(列表已经按升序排序),我们将有 NxN 个唯一的函数调用(每个 arg 成对的 N 个值)。但是,我的算法对于非常大的输入会超时,这表明我的算法没有 O(NxN) 的最坏情况时间成本。我在这里做错了什么吗?似乎是 DP + memoization 的简单实现。它超时的测试输入是list(range(1,2501))

我通过lis_calc(float('-inf'), 0)调用函数

【问题讨论】:

  • 在每个步骤中,您都会进行两次递归调用,除非您低于下限。它们有不同的参数,所以备忘录缓存无济于事。这两个电话中的每一个都打两个电话,每个电话打两个电话,依此类推。因此,除非您有一些证据证明这只能在到达下限之前记录 N 步,而不是 N 步,否则您的代码是 O(2**N),而不是 O(N**2)
  • 实际上,现在我想起来了……你不会在每次加倍调用时缓存一半的调用,但你应该缓存 N-1 的一半 N,所以,时间应该是N * (N+1) + 1,毕竟是二次的。所以也许只有一些可以修复的小缺陷?要对此进行测试,请尝试仅添加调用次数的计数。它是否以N * (N+1) + 1 为界——或者,为了简单起见,仅以2 * N**2 为界?并统计缓存命中次数;离N**2 / 2很近吗?
  • 如果答案是肯定的,那么我的答案是错误的,但没关系;我只能删除它。 :)
  • 请注意,您的代码正在执行显式递归。这与动态编程相同。 DP 通常使用数组来存储结果,这样您就不必每次需要时都重新计算。
  • 您应该阅读ericlippert.com/2014/03/05/how-to-debug-small-programs 了解有关如何调试代码的一些提示。您可以使用这些提示来验证您的算法是否正确,或者找出不正确的地方。

标签: python dynamic-programming memoization


【解决方案1】:

您的算法可能不是二次的,而是指数的。

看看这段代码:

if nums[offset] > lower_bound:
    res = max(1 + lis_calc(nums[offset], offset + 1), lis_calc(lower_bound, offset + 1))

在最坏的情况下,在每个步骤中,您都会进行两次调用。这两个电话中的每一个,在最坏的情况下,都会打两个电话。这四个调用中的每一个调用两个调用,依此类推。

如果以下两种情况之一为真,您的算法仍可能是多项式:

  • 如果这些新调用中至少有一半被缓存了,或者
  • 如果保证最坏的情况会在最坏的log N 步骤中减少到下限情况(变为线性)。

但据我所知,这些都不是真的。因此,在最坏的情况下,您的算法采用O(2**N) 步骤。这就是为什么它太慢了。


或者……也许这不是真的,也许它只是用一个额外的常数因子来计算二次时间,而 2500 就在他们期望你的代码能够舒适地工作的边缘附近,而你只是没有完全通过?

每次加倍调用时,您不会缓存其中的一半,但您应该缓存其中的一半 N-1。因此,如果一切顺利,您的总步数应该会达到N * (N+1) + 1,但如果您稍有错误,可能足以偏离 4 倍……虽然真的,我不认为这会很棒测试即使是他们测试的最大数量的常数因子 4 是否足以产生影响。

【讨论】:

    猜你喜欢
    • 2012-03-25
    • 2015-01-26
    • 1970-01-01
    • 2020-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-20
    相关资源
    最近更新 更多