我认为这个问题应该分两步解决。
第一步
第一步是确定1s、3s 和4s 的不同数字之和为给定数字。对于n = 5,只有3个,我们可以这样写:
[[5,0,0], [2,1,0], [1,0,1]]
这3个元素分别被解释为“5个1、0个3、0个4”、“2个1、1个3、0个4”和“1个1、0个3、1个4”。
为了有效地计算这些组合,我首先只使用 1 来计算可能的组合,它们的总和为 0 到 5 之间的每个数字(这当然是微不足道的)。这些值保存在哈希中,其键是和数,值是与键的值相加所需的 1 的个数:
h0 = { 0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5 }
(如果第一个数字是 2,而不是 1,则应该是:
h0 = { 0 => 0, 2 => 1, 4 => 2 }
因为没有办法仅将 2s 相加等于 1 或 3。)
接下来我们考虑同时使用 1 和 3 来对 0 和 5 之间的每个值求和。使用的 3 的数量只有两种选择,零或一。这会产生哈希:
h1 = { 0 => [[0,0]], 1 => [[1,0]], 2 => [[2,0]], 3 => [[3,0], [0,1]],
4 => [[4,0], [1,1]], 5 => [[5,0], [2,1]] }
这表明,例如:
- 只有一种方法可以使用 1 和 3 相加为 1:
1 => [1,0],表示一个 1 和零个 3。
- 有两种方法求和为 4:
4 => [[4,0], [1,1]],表示四个 1 和零个 3 或一个 1 和一个 3。
同理,当1、3、4都可以使用时,我们得到hash:
h2 = { 5 => [[5,0,0], [2,1,0], [1,0,1]] }
由于此哈希对应于所有三个数字 1、3 和 4 的使用,因此我们只关心总和为 5 的组合。
在构造h2时,我们可以使用零个4或一个4。如果我们使用零个4,我们将使用一个1和3的总和为5。我们从h1看到有两种组合:
5 => [[5,0], [2,1]]
对于h2,我们将这些写为:
[[5,0,0], [2,1,0]]
如果使用一个 4,则使用 1s 和 3s 总计 5 - 1*4 = 1。从h1 我们看到只有一种组合:
1 => [[1,0]]
h2 我们写成这样
[[1,0,1]]
所以
h2 中键 5 的值是:
[[5,0,0], [2,1,0]] + [[1,0,1]] = [[5,0,0], [2,1,0]], [1,0,1]]
除此之外:由于我选择表示哈希h1 和h2 的哈希形式,实际上将h0 表示为更方便:
h0 = { 0 => [[0]], 1 => [[1]],..., 5 => [[5]] }
应该很明显,这种顺序方法可以用于任何要对组合求和的整数集合。
第 2 步
在步骤 1 中产生的每个数组[n1, n3, n4] 的不同排列的数量等于:
(n1+n3+n4)!/(n1!n3!n4!)
请注意,如果n 之一为零,则这些将是二项式系数。事实上,这些是来自multinomial distribution 的系数,它是二项分布的推广。道理很简单。分子给出所有数字的排列数。对于每个不同的排列,n1 1 可以以n1! 方式置换,因此我们除以n1!。 n3 和 n4 也一样
求和到5的例子有:
-
5!/5! = 1 [5,0,0] 的不同安排
-
(2+1)!/(2!1!) = 3 [2,1,0] 和 的不同安排
-
(1+1)!/(1!1!) = 2[1,0,1]的不同安排,总共:
1+3+2 = 6 数字 5 的不同排列。
代码
def count_combos(arr, n)
a = make_combos(arr,n)
a.reduce(0) { |tot,b| tot + multinomial(b) }
end
def make_combos(arr, n)
arr.size.times.each_with_object([]) do |i,a|
val = arr[i]
if i.zero?
a[0] = (0..n).each_with_object({}) { |t,h|
h[t] = [[t/val]] if (t%val).zero? }
else
first = (i==arr.size-1) ? n : 0
a[i] = (first..n).each_with_object({}) do |t,h|
combos = (0..t/val).each_with_object([]) do |p,b|
prev = a[i-1][t-p*val]
prev.map { |pr| b << (pr +[p]) } if prev
end
h[t] = combos unless combos.empty?
end
end
end.last[n]
end
def multinomial(arr)
(arr.reduce(:+)).factorial/(arr.reduce(1) { |tot,n|
tot * n.factorial })
end
还有一个助手:
class Fixnum
def factorial
return 1 if self < 2
(1..self).reduce(:*)
end
end
示例
count_combos([1,3,4], 5) #=> 6
count_combos([1,3,4], 6) #=> 9
count_combos([1,3,4], 9) #=> 40
count_combos([1,3,4], 15) #=> 714
count_combos([1,3,4], 30) #=> 974169
count_combos([1,3,4], 50) #=> 14736260449
count_combos([2,3,4], 50) #=> 72581632
count_combos([2,3,4,6], 30) #=> 82521
count_combos([1,3,4], 500) #1632395546095013745514524935957247\
00017620846265794375806005112440749890967784788181321124006922685358001
(为了显示的目的,我将示例(一个长数字)的结果分成两部分。)
count_combos([1,3,4], 500) 计算大约需要 2 秒;其他的基本上是瞬时的。
@sawa 的方法和我的方法在 6 到 9 之间对n 给出了相同的结果,所以我相信它们都是正确的。使用n 时,sawa 的求解时间增加得比我的要快得多,因为他正在计算然后计算所有排列。
编辑:@Karole,他刚刚发布了一个答案,我的所有测试(包括最后一个!)都得到了相同的结果。我更喜欢哪个答案?嗯。让我考虑一下。)