【问题标题】:Dynamic Programming Solution for a Variant of Coin Exchange一种硬币交换变体的动态规划解决方案
【发布时间】:2018-05-06 11:48:26
【问题描述】:

我正在练习动态编程。我专注于硬币交换问题的以下变体:

S = [1, 2, 6, 12, 24, 48, 60] 是一组恒定的整数硬币面额。让n 是通过S 中的硬币可获得的正整数金额。考虑两个人AB。我可以通过多少种不同的方式将n 分配给AB 的人,以便每个人获得相同数量的硬币(忽略每个人实际获得的金额)?

示例

n = 6每人可以分成4种不同的方式:

  1. A 获得 {2, 2},B 获得 {1, 1}。
  2. A 人员获得 {2, 1},B 人员获得 {2, 1}。
  3. A 获得 {1, 1},B 获得 {2, 2}。
  4. A 人员获得 {1, 1, 1},B 人员获得 {1, 1, 1}。

请注意,每种方式对每个人来说都是非冗余的,即我们不会将 {2, 1} 和 {1, 2} 视为两种不同的方式。

以往的研究

我研究过非常相似的 DP 问题,例如硬币交换问题和分区问题。事实上,本站有问题指的几乎是同一个问题:

我最感兴趣的是可以帮助我解决这个问题的递归关系。定义它将使我能够轻松地应用制表方法的记忆来设计解决此问题的算法。

例如这个递归:

def f(n, coins):
  if n < 0:
    return 0

  if n == 0:
    return 1

  return sum([f(n - coin, coins) for coin in coins])

很诱人,但它不起作用,因为执行时:

# => f(6, [1, 2, 6]) # 14

以下是运行 S' = {1, 2, 6}n = 6 的示例,以帮助我澄清模式(可能存在错误):

【问题讨论】:

  • 所以您基本上是在寻找具有偶数个零件的分区数?
  • @m69:我是吗?我不确定我正在寻找分区,在我寻找的两个集合之前可能包含相同的元素,因此违反了对集合进行分区的定义。例如,对于 S = {1, 2, 6},有效解是 S1 = {1,1,1} 和 S2 = {1,1,1}。他们的交点不是空的。
  • 你说得对,我看错了问题。
  • 添加了自下而上的实现。这会在大约 2 秒内生成f(500, [1, 2, 6, 12, 24, 48, 60]) 的答案。

标签: algorithm recursion dynamic-programming coin-change


【解决方案1】:

这是你可以尝试的:

C(n, k, S) 是使用来自S 的一些k 硬币的金额n 的不同表示的数量。

然后C(n, k, S) = sum(C(n - s_i, k - 1, S[i:])) 求和是来自S 的每个s_iS[i:] 表示从S 开始的所有元素,从i-th 元素到结尾——我们需要这个来防止重复组合。

如果n &lt; 0k &lt; 0n &gt; 0k &lt; 1,初始条件为C(0, 0, _) = 1C(n, k, _) = 0

你要计算的数字:

R = sum(C(i, k, S) * C(n - i, k, S)) for i = 1..n-1, k = 1..min(i, n-i)/Smin 其中Smin - S 中最小的硬币面额。

min(i, n-i)/Smin 表示对给定总和进行分区时可能的最大硬币数量。例如,如果总和n = 20i = 8(第 1 人获得 8 美元,第 2 人获得 12 美元)并且最小硬币面额为 2 美元,则最大可能硬币数量为 8/2 = 4。用&gt;4 硬币你不能得到 8 美元。

【讨论】:

  • 我试图在这里实现你的想法:repl.it/@gl_dbrqn/CloseKindlyMemoryallocator 但我得到了f(6, [1, 2, 6]) 的 3,答案应该是 4;和 1 代表f(10, [1, 2, 6]),答案应该是 11。代码是非常简单的 Python。你能看一下,也许能帮我看看错误吗? (我相信这里的代码会产生正确的比较结果:repl.it/@gl_dbrqn/SlipperyStylishBases
  • @גלעדברקן 您实际上在我的公式中发现了一个错误,现在应该没问题,我已修复它。见repl.it/repls/WellwornGeneralMetrics
  • 酷。但这似乎没有多大帮助。您的代码 (repl.it/@gl_dbrqn/WelltodoBigheartedRar) 对于 n = 100 大约需要 8-9 秒,而我的代码大约需要 0.07-0.1 秒 (repl.it/@gl_dbrqn/RubberyIllRouter)。能否进一步优化?
  • @גלעדברקן 但该代码只是证明该公式有效,它根本没有记忆/DP。如果您有兴趣比较速度,请随时改进 :) 无论如何,由于您的解决方案非常快,您可以添加另一个答案。
  • 添加了一个自下而上的实现,将您的答案归功于您。干得好!
【解决方案2】:

这里是一个表格实现和对algrid's beautiful answer 的一点阐述。这会在大约 2 秒内生成f(500, [1, 2, 6, 12, 24, 48, 60]) 的答案。

C(n, k, S) = sum(C(n - s_i, k - 1, S[i:])) 的简单声明意味着使用k 硬币将所有方法添加到当前总和n。然后,如果我们将n 拆分为可以分成两部分的所有方式,我们只需将这些部分中的每一个都可以由相同数量的硬币k 组成的所有方式相加。

将我们选择的硬币子集固定到递减列表的美妙之处在于,任何硬币的任意组合只会被计算一次 - 它将在计算中计算,其中组合中最左边的硬币是第一个硬币我们的递减子集(假设我们以相同的方式对它们进行排序)。例如,取自[1, 2, 6, 12, 24, 48, 60] 的任意子集[6, 24, 48] 将仅计入子集[6, 12, 24, 48, 60] 的总和,因为下一个子集[12, 24, 48, 60] 将不包括6 和前一个子集[2, 6, 12, 24, 48, 60]至少有一枚2硬币。

Python 代码(见here;确认here):

import time

def f(n, coins):
  t0 = time.time()

  min_coins = min(coins)

  m = [[[0] * len(coins) for k in xrange(n / min_coins + 1)] for _n in xrange(n + 1)]

  # Initialize base case
  for i in xrange(len(coins)):
    m[0][0][i] = 1

  for i in xrange(len(coins)):
    for _i in xrange(i + 1):
      for _n in xrange(coins[_i], n + 1):
        for k in xrange(1, _n / min_coins + 1):
          m[_n][k][i] += m[_n - coins[_i]][k - 1][_i]

  result = 0

  for a in xrange(1, n + 1):
    b = n - a

    for k in xrange(1, n / min_coins + 1):
      result = result + m[a][k][len(coins) - 1] * m[b][k][len(coins) - 1]

  total_time = time.time() - t0

  return (result, total_time)

print f(500, [1, 2, 6, 12, 24, 48, 60])

【讨论】:

    猜你喜欢
    • 2011-02-07
    • 2016-10-25
    • 2023-03-20
    • 1970-01-01
    • 2018-02-25
    • 1970-01-01
    • 1970-01-01
    • 2012-11-24
    • 1970-01-01
    相关资源
    最近更新 更多