【问题标题】:0/1 Knapsack Problem Simplified in Python0/1 用 Python 简化的背包问题
【发布时间】:2020-03-19 17:49:59
【问题描述】:

我的以下代码执行速度太慢。这个想法类似于 0/1 背包问题,你有一个给定的整数 n,你必须找到 1 到 n - 1 范围内的数字,当平方加起来为 n 平方时。

例如,如果 n 为 5,那么它应该输出 3 , 4 因为 3 ** 2 和 4 ** 2 = (25 or 5 ** 2)。我一直在努力了解如何提高效率,并想知道用于提高此类程序效率的概念。

其他一些例子:n = 8 [无] n = 30 [1, 3, 7, 29] n = 16 [2, 3, 5, 7, 13]

我发现了一些关于此的帖子,但它们似乎仅限于两个数字,因为我的程序需要使用尽可能多的数字才能加起来。

我看了一些关于 0/1 背包问题的视频。我努力将相同的概念应用到我自己的程序中,因为问题完全不同。他们有可以放在包里的东西,它们有重量和利润。

这已经伤害了我几个小时的大脑,如果有人能指出我正确的方向,我将不胜感激,谢谢 :)

from math import sqrt
def decompose(n):

    lst = []

    sets = []

    temp = []

    perm = {}

    out = []

    for i in range (n):
        lst.append(i**2)


    for i in lst:
        for x in sets:
            temp.append(i + x)
            perm[i + x] = (i, x)
        for x in temp:
            if x not in sets:
                sets.append(x)
        if i not in sets:
            sets.append(i)
        temp = []

    if n**2 not in perm.keys():
        return None

    for i in perm[n**2]:
        if str(i).isdigit():
            out.append(i)
        if i == ' ':
            out.append(i)


    for i in out:
        if i not in lst:
            out.remove(i)
            for i in perm[i]:
                if str(i).isdigit():
                    out.append(i)
                if i == ' ':
                    out.append(i)

    out.sort()

    return [sqrt(i) for i in out]

【问题讨论】:

  • 这正是 0/1 背包或“硬币找零问题”(en.wikipedia.org/wiki/Change-making_problem)。你的目标是 25(如果 n = 5)。你的“硬币”是 1、4、9、16 等。
  • 查看 Google 的 OR-tools 库...它有一个非常快的求解器

标签: python python-3.x performance knapsack-problem processing-efficiency


【解决方案1】:

这对于评论来说太大了,所以我把它放在这里作为答案:

这正是 0/1 背包或“硬币找零问题”(en.wikipedia.org/wiki/Change-making_problem)。你的目标是赚 25 美分(如果 n = 5)。您的“硬币”是 1 美分、4 美分、9 美分、16 美分等。我假设由于您查看的是 0/1 背包,因此您不能重复使用相同的硬币(如果您可以重复使用相同的硬币)硬币,问题就简单多了)。

这样的动态规划问题有两种方法。它们都以自己的方式直观,但目前对您来说可能更直观。

1.

第一个是记忆化(称为自上而下)。这是您为decompose 编写递归函数的地方,但您将每次调用decompose 的结果缓存起来。这里的递归公式类似于

decompose_cache = dictionary that stores results of calls to decompose
def decompose(n = 25, coins_to_use={1,4,9,16}):
  if (n, coins_to_use) in decompose_cache:
    return decompose_cache[(n, coins_to_use)]
  biggest_coin = max(coins_to_use)
  other_coins = coins_to_use - {biggest_coin}
  decomposition_with_biggest_coin = decompose(n-biggest_coin, other_coins)
  decomposition_without_biggest_coin = decompose(n, other_coins)
  ans = decomposition_with_biggest_coin or decomposition_without_biggest_coin
  decompose_cache[(n, coins_to_use)] = ans
  return ans
print(decompose(25, {1,4,9,16}))

也就是说,要确定我们是否可以使用 {1,4,9,16} 赚 25 美分,我们只需要检查我们是否可以使用 {1,4,9} 赚 25 美分,或者我们是否可以赚 9美分 (25 - 16) 使用 {1,4,9}。这个递归定义,如果我们不缓存每个调用的结果,将导致类似O(n^n) 函数调用,但由于我们缓存结果,我们最多只对某些(目标,硬币)对进行计算.有 n^2 个可能的目标,n 个可能的硬币组,所以有 n^2 * n 对,所以有 O(n^2 * n = n^3) 函数调用。

2.

第二种方法是动态编程(称为自底向上)。 (我个人觉得这个比较简单,不会遇到python中最大递归深度的问题)

这是您填写表格的地方,从空的基本情况开始,表格中条目的值可以通过查看已填写条目的值来计算。我们可以称表为“DP”。
在这里,我们可以建立一个表格,其中 DP[n][k] 为真,如果您可以仅使用前 k 个“硬币”(其中第一个硬币为 1,第二个硬币为 4,等等)求和为 n 的值)。

我们计算表格中某个单元格值的方法是:
DP[n][k] = DP[n - kth coin][k-1] OR DP[n][k-1]

逻辑与上面相同:我们可以用硬币 {1,4}(前两个硬币)找 5 美分,当且仅当我们可以使用 1 美分(5-4)找零{1}(第一枚硬币)或者如果我们可以使用 {1} 找零 5 美分。因此,DP[5][2] = DP[1][1] 或 DP[5][1]。 同样,该表有 n^3 个条目。您可以从 [0][0] 到 [0][5] 逐行填写,然后从 [0][...] 到 [25][...] 的每一行,答案将在 [25][5] 中。

【讨论】:

  • 非常感谢您写的令人满意的文章。跳过了很多细节,但你会写一本书的章节。如果您想找到具有尽可能少的元素的集合,则 DP 方法似乎不太适用。专注于小集合似乎对这个问题很有用,因为它大大减少了搜索空间。
  • 我认为 DP 方法可以很容易地适应找到最小数量的元素:例如,要计算赚取 n 美分所需的最小硬币数量,我们可以存储 DP[n][k] = min number of coins needed to make n cents with the first k coins,并且DP方程变为DP[n][k] = min(DP[n - kth coin][k-1] + 1, DP[n][k-1])
【解决方案2】:

这是一个找到分解的递归程序。速度可能不是最佳的。当然,对于搜索大范围的输入,它并不是最佳选择,因为当前的方法不缓存中间结果。

在这个版本的函数 find_decomposition(n, k, uptonow, used) 中,我们尝试仅使用从 kn-1 的数字来找到 n2 的分解,而我们已经使用了used 数字,这些数字给出了 uptonow 的部分总和。该函数递归地尝试两种可能性:解决方案包括k 本身,或者不包括k。首先尝试一种可能性,如果可行,请退回。如果没有,请尝试其他方式。因此,首先尝试不使用k 的解决方案。如果没有成功,请做一个快速测试,看看是否只使用k 可以提供解决方案。如果这也不起作用,递归地尝试使用k 的解决方案,因此used 数字集现在包括k,其中uptonow 的总和需要增加@987654336 @2.

可以想到很多变体:

  • k 可以以相反的顺序运行,而不是从 1 运行到 n-1。请注意 if-test 的测试条件。
  • 不要先尝试不包含k 的解决方案,而是先尝试确实包含k 的解决方案。

请注意,对于较大的n,该函数可以运行到最大递归深度。例如。当n=1000 时,大约有2999 个可能的数字子集需要递归检查。这可能会导致 999 级深度的递归,这在某些时候对于 Python 解释器来说太多了。

首先使用大量数字的方法可能是有益的,因为它可以迅速缩小要填补的空白。幸运的是,存在许多可能的解决方案,因此可以快速找到解决方案。请注意,在@Kevin Wang 描述的一般背包问题中,如果不存在解决方案,则任何具有 999 个数字的方法都需要很长时间才能完成。

def find_decomposition(n, k=1, uptonow=0, used=[]):

    # first try without k
    if k < n-1:
        decomp = find_decomposition(n, k+1, uptonow, used)
        if decomp is not None:
            return decomp

    # now try including k
    used_with_k = used + [k]
    if uptonow + k * k == n * n:
        return used_with_k
    elif k < n-1 and uptonow + k * k + (k+1)*(k+1) <= n * n:
        # no need to try k if k doesn't fit together with at least one higher number
        return find_decomposition(n, k+1, uptonow+k*k, used_with_k)
    return None

for n in range(5,1001):
    print(n, find_decomposition(n))

输出:

5 [3, 4]
6 None
7 [2, 3, 6]
8 None
9 [2, 4, 5, 6]
10 [6, 8]
11 [2, 6, 9]
12 [1, 2, 3, 7, 9]
13 [5, 12]
14 [4, 6, 12]
15 [9, 12]
16 [3, 4, 5, 6, 7, 11]
...

PS:此链接包含有关相关问题的代码,但其中的方块可以重复: https://www.geeksforgeeks.org/minimum-number-of-squares-whose-sum-equals-to-given-number-n/

【讨论】:

  • 对不起,我忘了提到结果必须包含可能的最大数字,例如,50 应该返回 1、3、5、8、49 而不是 30、40。此外,这达到了最大递归深度,我不知道这是否相关。感谢您的深入回答我不完全理解第一个“尝试不使用 k 部分”,如果您能解释得更多,将不胜感激。
  • 描述是否清楚,你可以尝试重写函数吗?
  • 是的,不用担心。我将研究递归,以便我有更多的理解。让我困惑的部分是为什么 if k
  • 函数更深入,然后返回,尝试不同的路径。这是一整棵被遍历的可能性树,每次都有略微不同的参数。这些呼叫在时间上不是平行的,它是较早的呼叫暂时保持,直到它从更深的部分得到答案。
猜你喜欢
  • 2019-10-03
  • 1970-01-01
  • 2021-12-21
  • 2016-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-28
相关资源
最近更新 更多