我一直在解决一些算法编程问题并且对一个问题有疑问。问题与此问题中引用的问题相同: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;
有人可以为我澄清这种联系,或者指出一些解释这个具体问题的参考资料?感谢。
答案 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
子集只是从Smax
到S
的数字,所以我们可以摆脱{{ 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.这正是你的算法所做的; max
为N(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。