试图理解这个动态递归子集和的时间复杂度

Posted

技术标签:

【中文标题】试图理解这个动态递归子集和的时间复杂度【英文标题】:Trying to understand the time complexity of this dynamic recursive subset sum 【发布时间】:2022-01-07 16:09:39 【问题描述】:
# Returns true if there exists a subsequence of `A[0…n]` with the given sum
def subsetSum(A, n, k, lookup):
 
    # return true if the sum becomes 0 (subset found)
    if k == 0:
        return True
 
    # base case: no items left, or sum becomes negative
    if n < 0 or k < 0:
        return False
 
    # construct a unique key from dynamic elements of the input
    key = (n, k)
 
    # if the subproblem is seen for the first time, solve it and
    # store its result in a dictionary
    if key not in lookup:
 
        # Case 1. Include the current item `A[n]` in the subset and recur
        # for the remaining items `n-1` with the decreased total `k-A[n]`
        include = subsetSum(A, n - 1, k - A[n], lookup)
 
        # Case 2. Exclude the current item `A[n]` from the subset and recur for
        # the remaining items `n-1`
        exclude = subsetSum(A, n - 1, k, lookup)
 
        # assign true if we get subset by including or excluding the current item
        lookup[key] = include or exclude
 
    # return solution to the current subproblem
    return lookup[key]
 
 
if __name__ == '__main__':
 
    # Input: a set of items and a sum
    A = [7, 3, 2, 5, 8]
    k = 14
 
    # create a dictionary to store solutions to subproblems
    lookup = 
 
    if subsetSum(A, len(A) - 1, k, lookup):
        print('Subsequence with the given sum exists')
    else:
        print('Subsequence with the given sum does not exist')

据说这个算法的复杂度是O(n * sum),但是我不明白如何或为什么; 有人能帮我吗?可能是冗长的解释或递归关系,什么都可以

【问题讨论】:

哎呀。肯定是 O(n * k),但我不确定如何证明。好问题! 【参考方案1】:

我能给出的最简单的解释是,当lookup[(n, k)] 有一个值时,它是 True 或 False,并指示 A[:n+1] 的某个子集是否与 k 相加。

想象一个简单的算法,它只逐行填充查找的所有元素。

lookup[(0, i)](对于 0 ≤ itotal)只有两个元素为真,i = A[0]i = 0,所有其他元素都是假的。

lookup[(1, i)](对于 0 ≤ itotal)如果lookup[(0, i)] 为真或i ≥ A[1]lookup[(0, i - A[1]) 为真,则为真。我可以通过使用A[i] 或不使用i 来达到总和,并且我已经计算了这两个。

... 如果lookup[(r - 1, i)] 为真或i ≥ A[r]lookup[(r - 1, i - A[r]) 为真,lookup[(r, i)](对于 0 ≤ itotal)为真。

以这种方式填充此表,很明显我们可以在时间len(A) * total 中完全填充行0 ≤ row &lt; len(A) 的查找表,因为线性填充每个元素。而我们最终的答案就是检查表中是否有(len(A) - 1, sum) True。

您的程序正在执行完全相同的操作,但会根据需要计算 lookup 条目的值。

【讨论】:

【参考方案2】:

很抱歉提交了两个答案。我想我想出了一个稍微简单的解释。

想象一下您的代码将if key not in lookup: 中的三行放入一个单独的函数calculateLookup(A, n, k, lookup)。我将调用“为nk 调用nk 的特定值调用calculateLookup 的成本是调用calculateLookup(A, n, k, loopup) 所花费的总时间,但是排除对calculateLookup的任何递归调用。

关键的见解是,如上所述,为任何nk 调用calculateLookup() 的成本为O(1)。由于我们在成本中排除了递归调用,并且没有 for 循环,所以 calculateLookup 的成本是仅执行几个测试的成本。

整个算法做固定量的工作,调用calculateLookup,然后做少量的工作。因此,在我们的代码中花费的时间与询问我们调用calculateLookup 的次数相同?

现在我们回到之前的答案。由于查找表,对calculateLookup 的每次调用都使用不同的(n, k) 值调用。我们还知道,在每次调用 calculateLookup 之前,我们都会检查 nk 的边界,所以 1 ≤ k ≤ sum0 ≤ n ≤ len(A)。所以calculateLookup 最多被调用(len(A) * sum) 次。


一般来说,对于这些使用memoization/cacheing的算法,最简单的做法就是分别计算然后求和:

    假设您需要的所有值都已缓存,需要多长时间。 填充缓存需要多长时间。

您提出的算法只是填满了lookup 缓存。它以不寻常的顺序执行它,并且它没有填充表中的每个条目,但这就是它的全部。


代码会稍微快一点

lookup[key] =  subsetSum(A, n - 1, k - A[n], lookup) or subsetSum(A, n - 1, k, lookup)

在最坏的情况下不会改变代码的O(),但可以避免一些不必要的计算。

【讨论】:

从未收到 OP 的回复。这能回答你的问题吗?

以上是关于试图理解这个动态递归子集和的时间复杂度的主要内容,如果未能解决你的问题,请参考以下文章

掌握递归关系时间复杂度的定理

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

不能被 K 整除的两个和的最大子集

AtCoder Grand Contest 020 (AGC020) E - Encoding Subsets 动态规划

DP 动态规划初识

牛客网:连续子数组的最大和(动态递归解法)