分区相等子集总和的解决方案性能(DP,哈希表)

时间:2019-01-28 04:07:07

标签: python algorithm hashtable dynamic-programming

我知道在stackoverflow中已经提出了一些相关问题。但是,这个问题与3种方法之间的性能差异更多相关。

问题是:给定一个仅包含正整数的非空数组,请确定该数组是否可以划分为两个子集,以使两个子集中的元素之和相等。 {{3 }}

即[1,5,11,5] = True,[1,5,9] = False

通过解决此问题,我尝试了3种方法:

  • 方法1:动态编程。从上到下递归+备忘录(结果:超出了时间限制):

    def canPartition(nums):
        total, n = sum(nums), len(nums)
        if total & 1 == 1: return False
        half = total >> 1
        mem = [[0 for _ in range(half)] for _ in range(n)]
        def dp(n, half, mem):
            if half == 0: return True
            if n == -1: return False
            if mem[n - 1][half - 1]: return mem[n - 1][half - 1]
            mem[n - 1][half - 1] = dp(n - 1, half, mem) or dp(n - 1, half - nums[n - 1], mem)
            return mem[n - 1][half - 1]
        return dp(n - 1, half, mem)
    
  • 方法2:动态编程。自下而上。 (结果: 2208毫秒 已接受):

    def canPartition(self, nums):
        total, n = sum(nums), len(nums)
        if total & 1 == 1: return False
        half = total >> 1
        matrix = [[0 for _ in range(half + 1)] for _ in range(n)]
        for i in range(n):
            for j in range(1, half + 1):
                if i == 0: 
                    if j >= nums[i]: matrix[i][j] = nums[i]
                    else: matrix[i][j] = 0
                else:
                    if j >= nums[i]:
                        matrix[i][j] = max(matrix[i - 1][j], nums[i] + matrix[i - 1][j - nums[i]])
                    else: matrix[i][j] = matrix[i - 1][j]
                if matrix[i][j] == half: return True
        return False
    
  • 方法3:HashTable(字典)。结果( 172毫秒 已接受):

    def canPartition(self, nums):
        total = sum(nums)
        if total & 1 == 0:
            half = total >> 1
            cur = {0}
            for number in nums:
                cur |= { number + x for x in cur} # update the dictionary (hashtable) if key doesn't exist
                if half in cur: return True
        return False
    

对于时间复杂度的上述三种方法,我真的不了解两件事:

  • 我希望方法1 方法2 应该具有相同的结果。两者都使用表(矩阵)记录计算的状态,但是为什么自下而上的方法更快?
  • 我不知道为什么方法3比其他方法快得多。注意:乍一看,方法3似乎是2到Nth Power的方法,但它是使用字典丢弃重复值的,因此时间复杂度应为 T(n * half)

2 个答案:

答案 0 :(得分:2)

我对方法1与其他方法之间的区别的猜测是,由于递归,方法1需要生成更多的堆栈帧,这比仅分配矩阵并在条件条件上进行迭代要花费更多的系统资源。但是,如果我是您,我将尝试使用某种过程和内存分析器来更好地确定和确认正在发生的事情。方法1根据范围分配矩阵,但是该算法实际上将迭代次数限制为可能更少,因为下一个函数调用跳转到数组元素减去的总和,而不是合并所有可能性。

方法3仅取决于输入元素的数量和可生成的总和的数量。在每次迭代中,它将当前输入中的数字添加到所有先前可实现的数字中,仅将新数字添加到该列表中。例如,给定列表[50000, 50000, 50000],方法3最多可以迭代三个总和:50000、100000和150000。但是由于它取决于范围,方法2至少可以迭代75000 * 3次! >

给出列表[50000,50000,50000],方法1、2和3生成以下迭代次数:15、225000和6。

答案 1 :(得分:0)

您是对的,方法1)和3)具有相同的时间复杂度,方法2是背包(0/1)的DP版本,方法1是分支和绑定版本。您可以通过任何背包启发式方法修剪树来改善方法一,但优化必须严格,例如如果级别K上的现有总和与剩余元素的总和<一半,则将其跳过。这种方法1)比3)具有更好的计算复杂性。

为什么方法1)和3)的运行时间不同,

[某种程度上]

这更多与python中字典的实现有关。字典是由Python解释器本地实现的,对它们的任何操作都将比需要首先且经常被解释的任何事物都快。另外,函数调用在python中具有更高的开销,它们是对象。因此,调用一个不是简单的增加堆栈和jmp / call操作。

[很大程度上]

要考虑的另一方面是第三种方法的时间复杂性。对于方法3,时间复杂度是指数级的唯一方法是,如果每次迭代都导致插入与当前迭代的字典中的元素一样多的元素。

  cur |= { number + x for x in cur}

上面的行应加倍| cur |。

我认为有可能出现类似的系列

s = {k,K 2 ,K 3 ,...,k n ,(> K n + 1 )}

(其中K是质数> 2) 给出方法3的最坏情况时间为2 n 的时间。尚不确定平均预期时间复杂度是多少。