这里有两种计算答案的方法。第一个简单但效率不高;第二种依赖于优化技术,速度更快,但需要更多的代码。
紧凑但效率低下
这是一种简洁的计算方式,使用方法Array#repeated_combination:
代码
def combos(n,k)
[*(1..n-k+1)].repeated_combination(3).select { |a| a.reduce(:+) == n }
end
例子
combos(10,3)
#=> [[1, 1, 8], [1, 2, 7], [1, 3, 6], [1, 4, 5],
# [2, 2, 6], [2, 3, 5], [2, 4, 4], [3, 3, 4]]
combos(100,4).size
#=> 832
combos(1000,3).size
#=> 83333
评论
前两次计算不到一秒钟,但第三次需要几分钟。
更高效,但增加了复杂性
代码
def combos(n,k)
return nil if k.zero?
return [n] if k==1
return [1]*k if k==n
h = (1..k-1).each_with_object({}) { |i,h| h[i]=[[1]*i] }
(2..n-k+1).each do |i|
g = (1..[n/i,k].min).each_with_object(Hash.new {|h,k| h[k]=[]}) do |m,f|
im = [i]*m
mxi = m*i
if m==k
f[mxi].concat(im) if mxi==n
else
f[mxi] << im if mxi + (k-m)*(i+1) <= n
(1..[(i-1)*(k-m), n-mxi].min).each do |j|
h[j].each do |a|
f[mxi+j].concat([a+im]) if
((a.size==k-m && mxi+j==n) ||
(a.size<k-m && (mxi+j+(k-m-a.size)*(i+1))<=n))
end
end
end
end
g.update({ n=>[[i]*k] }) if i*k == n
h.update(g) { |k,ov,nv| ov+nv }
end
h[n]
end
例子
p combos(10,3)
#=> [[3, 3, 4], [2, 4, 4], [2, 3, 5], [1, 4, 5],
# [2, 2, 6], [1, 3, 6], [1, 2, 7], [1, 1, 8]]
p combos(10,4)
#=> [[2, 2, 3, 3], [1, 3, 3, 3], [2, 2, 2, 4], [1, 2, 3, 4], [1, 1, 4, 4],
# [1, 2, 2, 5], [1, 1, 3, 5], [1, 1, 2, 6], [1, 1, 1, 7]]
puts "size=#{combos(100 ,3).size}" #=> 833
puts "size=#{combos(100 ,5).size}" #=> 38224
puts "size=#{combos(1000,3).size}" #=> 83333
评论
combos(1000,3).size 的计算用了大约 5 秒,其他的都在 1 秒以内。
解释
此方法使用dynamic programming 来计算解决方案。状态变量是用于计算大小不超过k 的数组的最大正整数,其元素总和不超过n。从等于一的最大整数开始。下一步是计算 k 或更少元素的所有组合,包括数字 1 和 2,然后是 1、2 和 3,依此类推,直到我们拥有 k 或更少元素的所有组合,包括数字 1通过n。然后,我们从最后一次计算中选择总和为 n 的所有 k 元素组合。
假设
k => 3
n => 7
然后
h = (1..k-1).each_with_object({}) { |i,h| h[i]=[[1]*i] }
#=> (1..2).each_with_object({}) { |i,h| h[i]=[[1]*i] }
#=> { 1=>[[1]], 2=>[[1,1]] }
这读取,使用唯一的数字1,[[1]] 是所有数组和 1 的数组,[[1,1]] 是所有数组和 2 的数组。
请注意,这不包括元素3=>[[1,1,1]]。那是因为,已经有 k=3 元素,如果不能与任何其他元素组合,并且总和为 3 < 7。
我们接下来执行:
enum = (2..n-k+1).each #=> #<Enumerator: 2..5:each>
我们可以将此枚举器转换为数组,以查看它将传递到其块中的值:
enum.to_a #=> [2, 3, 4, 5]
作为n => 7,您可能想知道为什么这个数组以5 结尾。这是因为没有包含三个正整数的数组,其中至少有一个是6 或7,其元素之和为7。
第一个值enum传入块,由块变量i表示,是2。我们现在将计算一个哈希 g,它包括所有数组的总和为 n => 7 或更少,最多有 k => 3 元素,包括一个或多个 2 和零个或多个 1' s。 (这有点拗口,但仍然不准确,我将解释。)
enum2 = (1..[n/i,k].min).each_with_object(Hash.new {|h,k| h[k]=[]})
#=> (1..[7/2,3].min).each_with_object(Hash.new {|h,k| h[k]=[]})
#=> (1..3).each_with_object(Hash.new {|h,k| h[k]=[]})
Enumerable#each_with_object 创建一个初始为空的散列,由块变量f 表示。这个哈希的默认值是这样的:
f[k] << o
等价于
(f[k] |= []) << o
表示如果f没有密钥k,
f[k] = []
在之前执行
f[k] << o
执行。
enum2 会将以下元素传递到其块中:
enum2.to_a #=> => [[1, {}], [2, {}], [3, {}]]
(尽管当第一个元素之后的元素被传递到块中时散列可能不为空)。传递给块的第一个元素是[1, {}],由块变量表示:
m => 1
f => Hash.new {|h,k| h[k]=[]}
m => 1 表示我们将首先构造包含一个 (i=) 2 的数组。
im = [i]*m #=> [2]*1 => [2]
mxi = m*i #=> 2*1 => 2
作为(m == k) #=> (1 == 3) => false,我们接下来执行
f[mxi] << im if mxi + (k-m)*(i+1) <= n
#=> f[2] << [2] if 2 + (3-1)*(1+1) <= 7
#=> f[2] << [2] if 8 <= 7
这会考虑是否应将[2] 添加到f[2] 而不添加任何整数j < i = 2。 (我们还没有考虑将一个2 与小于2 的整数组合[即1]。)作为8 <= 7,我们不会将[2] 添加到f[2]。原因是,要成为长度为k=3 的数组的一部分,它将采用[2,x,y] 的形式,其中x > 2 和y > 2,所以2+x+y >= 2+3+3 = 8 > n = 7。清如泥?
接下来,
enum3 = (1..[(i-1)*(k-m), n-mxi].min).each
#=> = (1..[2,5].min).each
#=> = (1..2).each
#=> #<Enumerator: 1..2:each>
传递值
enum3.to_a #=> [1, 2]
进入它的块,由块变量j表示,它是哈希h的键。我们将在这里做的是将一个2 (m=1) 与包含不超过1(即1)且总和为j 的整数的元素数组结合起来,因此结果数组的元素总和为m * i + j => 1 * 2 + j => 2 + j。
enum3 没有将大于2 的j 的值传递到其块中的原因是h[l] 对于l > 2 是空的(但是当i > 2 时它有点复杂)。
对于j => 1,
h[j] #=> [[1]]
enum4 = h[j].each #=> #<Enumerator: [[1]]:each>
enum4.to_a #=> [[1]]
a #=> [1]
所以
f[mxi+j].concat([a+im]) if
((a.size==k-m && mxi+j==n) || (a.size<k-m && (mxi+j+(k-m-a.size)*(i+1))<=n))
#=> f[2+1].concat([[1]+[2]) if ((1==2 && 2+1==7) || (1<=3-1 && (2+1+(1)*(3)<=7))
#=> f[3].concat([1,2]) if ((false && false) || (1<=2 && (6<=7))
#=> f[3] = [] << [[1,2]] if (false || (true && true)
#=> f[3] = [[1,2]] if true
所以左边的表达式被计算了。同样,条件表达式有点复杂。首先考虑:
a.size==k-m && mxi+j==n
相当于:
([2] + f[j]).size == k && ([2] + f[j]).reduce(:+) == n
也就是说,如果数组[2] + f[j] 的k 元素总和为n,则包含该数组。
第二个条件考虑数组[2] + f[j]元素少于k的数组是否可以用整数l > i = 2“完成”并且总和为n或更少。
现在,f #=> {3=>[[1, 2]]}。
我们现在将j 增加到2 并考虑数组[2] + h[2],其元素总数为4。
对于j => 2,
h[j] #=> [[1, 1]]
enum4 = h[j].each #=> #<Enumerator: [[1, 1]]:each>
enum4.to_a #=> [[1, 1]]
a #=> [1, 1]
f[mxi+j].concat([a+im]) if
((a.size==k-m && mxi+j==n) || (a.size<k-m && (mxi+j+(k-m-a.size)*(i+1)<=n))
#=> f[4].concat([1, 1, 2]) if ((2==(3-1) && 2+2 == 7) || (2+2+(3-1-2)*(3)<=7))
#=> f[4].concat([1, 1, 2]) if (true && false) || (false && true))
#=> f[4].concat([1, 1, 2]) if false
因此不执行此操作(因为[1,1,2].size => 3 = k 和[1,1,2].reduce(:+) => 4 < 7 = n。
我们现在将 m 递增到 2,这意味着我们将构造具有两个 (i=) 2 的数组。这样做之后,我们看到:
f={3=>[[1, 2]], 4=>[[2, 2]]}
m => 3时没有添加其他数组,所以我们有:
g #=> {3=>[[1, 2]], 4=>[[2, 2]]}
声明
g.update({ n=>[i]*k }) if i*k == n
#=> g.update({ 7=>[2,2,2] }) if 6 == 7
如果元素之和等于n,则将元素7=>[2,2,2] 添加到哈希g,但事实并非如此。
我们现在将g 折叠成h,使用Hash#update(又名Hash#merge!):
h.update(g) { |k,ov,nv| ov+nv }
#=> {}.update({3=>[[1, 2]], 4=>[[2, 2]]} { |k,ov,nv| ov+nv }
#=> {1=>[[1]], 2=>[[1, 1]], 3=>[[1, 2]], 4=>[[2, 2]]}
现在h 包含所有其键为数组总计的数组(值),由整数1 和2 组成,它们最多具有3 元素并且总和最多为7,排除那些元素少于 3 的数组,当添加大于 2 的整数时,这些数组不能和 7。
执行的操作如下:
i m j f
h #=> { 1=>[[1]], 2=>[[1,1]] }
2 1 1 {3=>[[1, 2]]}
2 1 2 {3=>[[1, 2]]}
2 2 1 {3=>[[1, 2]], 4=>[[2, 2]]}
{3=>[[1, 2]], 4=>[[2, 2]]}
3 1 1 {}
3 1 2 {}
3 1 3 {}
3 1 4 {7=>[[2, 2, 3]]}
3 2 1 {7=>[[2, 2, 3], [1, 3, 3]]}
g before g.update: {7=>[[2, 2, 3], [1, 3, 3]]}
g after g.update: {7=>[[2, 2, 3], [1, 3, 3]]}
h after h.update(g): {1=>[[1]],
2=>[[1, 1]],
3=>[[1, 2]],
4=>[[2, 2]],
7=>[[2, 2, 3], [1, 3, 3]]}
4 1 1 {}
4 1 2 {}
4 1 3 {7=>[[1, 2, 4]]}
g before g.update: {7=>[[1, 2, 4]]}
g after g.update: {7=>[[1, 2, 4]]}
h after h.update(g): {1=>[[1]],
2=>[[1, 1]],
3=>[[1, 2]],
4=>[[2, 2]],
7=>[[2, 2, 3], [1, 3, 3], [1, 2, 4]]}
5 1 1 {}
5 1 2 {7=>[[1, 1, 5]]}
g before g.update: {7=>[[1, 1, 5]]}
g after g.update: {7=>[[1, 1, 5]]}
h after h.update(g): {1=>[[1]],
2=>[[1, 1]],
3=>[[1, 2]],
4=>[[2, 2]],
7=>[[2, 2, 3], [1, 3, 3], [1, 2, 4], [1, 1, 5]]}
最后,
h[n].select { |a| a.size == k }
#=> h[7].select { |a| a.size == 3 }
#=> [[2, 2, 3], [1, 3, 3], [1, 2, 4], [1, 1, 5]]