【问题标题】:Complex Combinatorial Conditions on Dynamic Programming动态规划的复杂组合条件
【发布时间】:2018-02-15 14:37:51
【问题描述】:

我正在探索动态编程设计方法如何与问题的基本组合属性相关联。

为此,我正在查看硬币找零问题的典型实例:让S = [d_1, d_2, ..., d_m]n > 0 成为请求的金额。除了S 中的元素,我们可以通过多少种方式将n 相加?

如果我们遵循动态规划方法来设计一个算法来解决这个问题,该算法将允许具有多项式复杂性的解决方案,我们将首先查看问题以及它与更小和更小的问题之间的关系。更简单的子问题。这将产生一个递归关系,描述一个归纳步骤,根据其相关子问题的解决方案来表示问题。然后,我们可以实现 memoization 技术或 tabulation 技术,以分别以自上而下或自下而上的方式有效地实现这种递归关系。

解决此问题实例的递归关系可能如下(Python 3.6 语法和基于 0 的索引):

def C(S, m, n):
    if n < 0:
        return 0
    if n == 0:
        return 1
    if m <= 0:
        return 0
    count_wout_high_coin = C(S, m - 1, n)
    count_with_high_coin = C(S, m, n - S[m - 1])
    return count_wout_high_coin + count_with_high_coin

这种递归关系产生了正确数量的解决方案,但忽略了顺序。但是,这种关系:

def C(S, n):
  if n < 0:
    return 0
  if n == 0:
    return 1
  return sum([C(S, n - coin) for coin in S])

在考虑订单时产生正确数量的解决方案。

我有兴趣通过可以通过记忆/制表进一步优化的递归关系来捕捉更微妙的组合模式。

比如这个关系:

def C(S, m, n, p):
    if n < 0:
        return 0
    if n == 0 and not p:
        return 1
    if n == 0 and p:
        return 0
    if m == 0:
        return 0
    return C(S, m - 1, n, p) + C(S, m, n - S[n - 1], not p)

产生一个不考虑顺序的解决方案,但只计算具有偶数个被加数的解决方案。同样的关系可以修改为关于偶数和数的顺序和计数:

def C(S, n, p):
    if n < 0:
        return 0
    if n == 0 and not p:
        return 1
    if n == 0 and p:
        return 0
    return sum([C(S, n - coin, not p) for coin in S])

但是,如果我们想要在其中分割硬币的人超过 1 人怎么办?假设我想将n 分成 2 个人。每个人得到相同数量的硬币,无论每个人得到的总和是多少。在 14 个解决方案中,只有 7 个包含偶数个硬币,这样我就可以平均分配它们。但我想排除给每个人多余的硬币分配。例如,1 + 2 + 2 + 11 + 2 + 1 + 2 在订单很重要时是不同的解决方案,但它们代表相同的硬币分成两个人,即人 B 将得到 1 + 2 = 2 + 1。我很难想出一个递归来以非冗余方式计算拆分。

【问题讨论】:

  • 您正在尝试解决partition problem。引用的参考文献给出了一种基于动态规划的伪多项式算法。请注意,该问题实际上是 NP 完全问题(理论运行时间是所涉及数字的函数)。
  • @collapsar 我不认为这是分区问题。 OP 声明,“我想将 n 分成 2 个人,每个人得到相同数量的硬币,不管每个人得到的总和是多少。”
  • 添加了自下而上的实现。这会在大约 2 秒内生成f(500, [1, 2, 6, 12, 24, 48, 60]) 的答案。
  • 我认为在忽略顺序的情况下计算偶数和数的代码有错误......不应该读成return C(S, m - 1, n, p) + C(S, m, n - S[m - 1], not p)吗? IE。 n - S[m - 1] 而不是 n - S[n - 1]?

标签: algorithm recursion dynamic-programming recurrence coin-change


【解决方案1】:

(在我详细说明可能的答案之前,让我指出,即使n按总和计算代币交换的分裂也会更多或不那么琐碎,因为我们可以计算交换n / 2 的方式的数量并将其相乘:)

现在,如果您想根据硬币计数计算硬币交换的拆分,并排除给每个人分配的多余硬币(例如,拆分 1 + 2 + 2 + 1 分成两个相等大小的部分只有 (1,1) | (2,2)(2,2) | (1,1)(1,2) | (1,2) 并且每个部分中的元素顺序无关紧要),我们可以依靠您的第一个分区枚举,其中顺序被忽略。

但是,我们需要知道每个分区中元素的多重集(或相似元素的集合),以便计算将它们一分为二的可能性。例如,要计算拆分1 + 2 + 2 + 1的方式,我们将首先计算我们拥有的每个硬币的数量:

def partitions_with_even_number_of_parts_as_multiset(n, coins):
  results = []

  def C(m, n, s, p):
    if n < 0 or m <= 0:
      return

    if n == 0:
      if not p:
        results.append(s)
      return

    C(m - 1, n, s, p)

    _s = s[:]
    _s[m - 1] += 1

    C(m, n - coins[m - 1], _s, not p)

  C(len(coins), n, [0] * len(coins), False)

  return results

输出:

=> partitions_with_even_number_of_parts_as_multiset(6, [1,2,6])
=> [[6, 0, 0], [2, 2, 0]]
                ^ ^ ^ ^ this one represents two 1's and two 2's

既然我们在计算选择其中一半的方法,我们需要在多项式乘法中找到x^2的系数

(x^2 + x + 1) * (x^2 + x + 1) = ... 3x^2 ...

表示从多重集计数中选择两个的三种方式[2,2]

2,0 => 1,1
0,2 => 2,2
1,1 => 1,2

在 Python 中,我们可以使用numpy.polymul 来乘以多项式系数。然后我们在结果中查找合适的系数。

例如:

import numpy    

def count_split_partitions_by_multiset_count(multiset):
  coefficients = (multiset[0] + 1) * [1]

  for i in xrange(1, len(multiset)):
    coefficients = numpy.polymul(coefficients, (multiset[i] + 1) * [1])

  return coefficients[ sum(multiset) / 2 ]

输出:

=> count_split_partitions_by_multiset_count([2,2,0])
=> 3

【讨论】:

    【解决方案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])
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-13
      • 2014-11-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多