动态规划的复杂组合条件

时间:2018-02-15 14:37:51

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

我正在探索动态规划设计方法如何与问题的潜在组合属性相关联。

为此,我正在查看硬币更改问题的规范实例:让S = [d_1, d_2, ..., d_m]n > 0成为请求金额。除了n中的元素之外,我们可以使用多少种方式添加S

如果我们按照动态编程方法为这个问题设计一个允许多项式复杂度的解决方案的算法,我们首先要看问题以及它与小问题的关系。更简单的子问题。这将产生一个递归关系,描述归纳步骤,根据其相关子问题的解决方案来表示问题。然后,我们可以实现 memoization 技术或制表技术,以分别以自上而下或自下而上的方式有效地实现此递归关系。

解决此问题实例的递归关系可能如下(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分成两个人。每个人获得相同数量的硬币,无论每个人得到的总金额。从14个解决方案中,只有7个包括偶数个硬币,这样我就可以将它们均匀分开。但我想为每个人排除多余的硬币分配。例如,1 + 2 + 2 + 11 + 2 + 1 + 2在订单重要时是不同的解决方案,但它们代表两个人的相同硬币分割,即人B将获得1 + 2 = 2 + 1。我很难想出一个以非冗余的方式计算拆分的递归。

2 个答案:

答案 0 :(得分: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

答案 1 :(得分:1)

这是一个表实现,并对algrid's beautiful answer进行了一些阐述。这会在约2秒内为input { width: 100%; } 生成答案。

f(500, [1, 2, 6, 12, 24, 48, 60])的简单声明表示使用C(n, k, S) = sum(C(n - s_i, k - 1, S[i:]))硬币添加所有获取当前金额n的方法。然后,如果我们将k分成所有方式,它可以分成两部分,我们可以添加所有这些部分的所有方式,可以使用相同数量的n个硬币。

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

Python代码(请参阅here;确认here):

2