适当的分区计数算法的解释?

时间:2013-01-24 21:41:28

标签: algorithm math combinatorics partition-problem

我一直在解决一些算法编程问题并且对一个问题有疑问。问题与此问题中引用的问题相同:USACO: Subsets (Inefficient)

我能够编写一些对于高N来说太慢的非动态解决方案。不得不作弊并在线查找一些解决方案。事实证明,快速算法非常简单,但即使知道答案,我仍然无法看到如何从问题到答案。我可以看到相等和的子集形式的模式,但我没有看到这些模式和算法解决方案之间的联系。

问题(上面的链接)是:

  

给定从1到N的一组连续整数(1 <= N <= 39),   有多少种方法可以将集合划分为两个总和的子集   是一样的?例如,{1,2,3}可以单向划分:{1,2} {3}。

对于较大的集合,答案为0(当N *(N + 1)/ 2为奇数时)或由此简单算法给出:

  arr = array of int with (N*(N+1)/4)+1 elements
  arr[0]=1  // all other elements initialized to 0
  for i = 1 to N
    for j = N*(N+1) / 4 downto i
      add arr[j-i] to arr[j]
  subsetpaircount = arr[N*(N+1)/4] / 2

同样,我可以看到算法是如何工作的,我甚至插入了打印语句,因此我可以观察&#34;这个怎么运作。我无法看到算法的操作如何链接到生成两组分区的不同方式的模式。

链接问题中的响应可能是相关的,但我也无法联系它的工作方式:&#34;这与在多项式中找到系数x ^ 0项相同(x ^ 1 + 1 / X)(X ^ 2 + 1 / X ^ 2)...(X ^ N + 1 / X ^ N)。 。 。 &#34;

有人可以为我澄清这种联系,或者指出一些解释这个具体问题的参考资料?感谢。

2 个答案:

答案 0 :(得分:7)

如果集合S = {1,...,N}被划分为两个具有相等和的子集,则该总和必须是S之和的一半; S的总和为N(N+1)/2,因此分区中每个子集的总和必须为N(N+1)/4。它也必须是整数,因此N(N+1)/2必须是偶数。

因此,问题在于找到总和为N(N+1)/4的S的子集数量。分区总数将正好是这个数字的一​​半,因为每个分区包含两个这样的子集,并且没有两个分区共享一个子集。

那应该是显而易见的。

现在,让我们列出S的子集,其总和为k,适用于任何k和任何集S。任何此类子集都必须具有最大值,该值必须是S的元素。最大值必须是S的最大元素,或者必须小于S的最大元素。这两组子集是不同的,因此我们可以单独枚举它们。我们称之为S Smax的最大元素。

第二组很简单:它只是S - { Smax }的子集,总和为k。我们可以通过递归调用子集列表来找到它们。但第一组几乎一样简单:组中的每个组都包含Smax,其余元素都在S - { Smax }中设置,其中k - Smax加起来S,我们再次列出递归。为了完成递归,我们注意到如果k = 0是空集,那么如果S,则恰好有一个限定子集(空集本身),如果k不是0,那么没有符合条件的子集。每次递归都会从S中删除一个元素,因此最终必须达到终止条件。

应该清楚的是,上面的递归函数将使用的1子集只是从SmaxS的数字,所以我们可以摆脱{{ 1}},并按如下方式编写递归:

Subsets(min, max, k) =
  Subsets(min, max - 1, k)
  ⋃ { {max, P} | P ∈ Subsets(min, max - 1, k - max) }

但是我们只想要分区数的计数,所以我们可以简化一下:

Count_Subsets(min, max, k) =
  Count_Subsets(min, max - 1, k)
  + Count_Subsets(min, max - 1, k - max)

我们需要通过添加结束条件来完成递归:

If min > max, Count_Subsets(min, max, k) = 1 if k = 0; otherwise 0

(事实上,很简单地表明递归意味着当k递减到1时值将为0,如果0小于k则递归0 Count_Subsets(1, N, N*(N+1)/4),所以我们可以提前终止条件。)

这为我们提供了计数的简单递归。但由于它自称两次,因此倒退工作会更好(“动态编程”)。我们需要计算Count_Subsets(1, max, k),这将要求我们为从1到N的所有max值以及从0到N *(N + 1)/的所有k值计算i的值4。我们从max = 0开始,然后一直工作直到达到min = N.这正是你的算法所做的; maxN(N+1)/4,数组是k从0到a[j]的值集。

顺便说一下,从上面的说明中可以看出,a[j - i]应该增加a[j - 1],而不是{{1}}

答案 1 :(得分:1)

我认为您的伪代码可能存在错误导致混淆。我希望这行

add arr[j-1] to arr[j]

add arr[j-i] to arr[j]

假设是这种情况,那么考虑这个问题的方法是,在i循环的每次迭代开始时,数组arr [j]包含选择整数子集的方式的数量1 ,2,...,i-1,使得所选整数的总和恰好为j。

当你开始时,i = 1所以子集的唯一选择是空子集,其总和等于1。

这就是arr [0] = 1(表示使用空子集得到总数为0)而所有其他条目都为0的原因(因为无法得到非零和)。

从那时起,迭代的每次传递都会考虑将数字i添加到子集中。获得总和j的方式数量取决于我是否被包括在内。

如果我不包括在内,那么我们的方式与之前相同(即arr [j])。

如果包含i,那么为了得到包括i的j的和,我们必须将i加到1,...,i-1的所有子集中,其总共有j-i。按照设计,如果我们查看索引j-i,我们的数组就包含这个数字。

因此获得j之和的总方式变为arr [j] + arr [j-i]。

当i循环完成时,arr会为您提供选择子集和获得所需总和的方法数。我们知道1,2,...,n的和是n *(n-1)/ 2,所以如果我们计算有多少子集达到这个值的一半,那么我们就把分区计算成相等的总和。

实际上,这超过了因子2,因为它将{1,2} / {3]和{3} / {1,2}计为单独的解,因此最终答案除以2。