在Ruby上使用combination
方法,
[1, 2, 3, 4, 5, 6].combination(2).to_a
#=> [[1, 2], [1, 3], [1, 4], [1, 5], [1, 6], [2, 3],
# [2, 4], [2, 5], [2, 6], [3, 4], [3, 5], [3, 6],
# [4, 5], [4, 6], [5, 6]]
我们可以得到一个具有15(6C2)个元素的二维数组。
我想创建一个返回如下数组的fair_combination
方法:
arr = [[1, 2], [3, 5], [4, 6],
[3, 4], [5, 1], [6, 2],
[5, 6], [1, 3], [2, 4],
[2, 3], [4, 5], [6, 1],
[1, 4], [2, 5], [3, 6]]
这样每三个子数组(6个中的一半)包含所有给定元素:
arr.each_slice(3).map { |a| a.flatten.sort }
#=> [[1, 2, 3, 4, 5, 6],
# [1, 2, 3, 4, 5, 6],
# [1, 2, 3, 4, 5, 6],
# [1, 2, 3, 4, 5, 6],
# [1, 2, 3, 4, 5, 6]]
这使得它变得“公平”,通过在阵列继续使用尽可能不同的元素。
为了使它更通用,它需要满足的要求如下:
(1)当您从开始跟踪数组并计算每个数字出现的次数时,它应该尽可能地平坦;
(1..7).to_a.fair_combination(3)
#=> [[1, 2, 3], [4, 5, 6], [7, 1, 4], [2, 5, 3], [6, 7, 2], ...]
前7个数字为[1,2,...,7],以下7个数字也是如此。
(2)一旦数字A与B在同一个数组中,如果可能,A不希望与B在同一个数组中。
(1..10).to_a.fair_combination(4)
#=> [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 1, 5], [2, 6, 9, 3], [4, 7, 10, 8], ...]
是否有任何好的算法可以创建像这样的“公平组合”?
答案 0 :(得分:1)
不能保证提供最佳解决方案,但它提供了足够好的解决方案。
在每一步中,它选择一个最小子池,它是一组最小高度的项目,对于这个项目,仍然有一个组合可供选择(高度是之前使用过项目的次数)。
例如,让枚举器为
my_enum = FairPermuter.new('abcdef'.chars, 4).each
第一次迭代可能会返回
my_enum.next # => ['a', 'b', 'c', 'd']
此时这些字母的高度为1,但没有足够的高度字母0来组合,所以只需将它们全部用于下一个:
my_enum.next # => ['a', 'b', 'c', 'e'] for instance
现在2
,a
和b
,c
1
和d
的高度为e
, 0
f
,而最佳资源池仍然是完整的初始设置。
因此,对于大尺寸的组合,这并没有真正优化。另一方面,如果组合的大小最多只是初始集大小的一半,那么算法就相当不错了。
class FairPermuter
def initialize(pool, size)
@pool = pool
@size = size
@all = Array(pool).combination(size)
@used = []
@counts = Hash.new(0)
@max_count = 0
end
def find_valid_combination
[*0..@max_count].each do |height|
candidates = @pool.select { |item| @counts[item] <= height }
next if candidates.size < @size
cand_comb = [*candidates.combination(@size)] - @used
comb = cand_comb.sample
return comb if comb
end
nil
end
def each
return enum_for(:each) unless block_given?
while combination = find_valid_combination
@used << combination
combination.each { |k| @counts[k] += 1 }
@max_count = @counts.values.max
yield combination
return if @used.size >= [*1..@pool.size].inject(1, :*)
end
end
end
4对6的公平组合的结果
[[1, 2, 4, 6], [3, 4, 5, 6], [1, 2, 3, 5],
[2, 4, 5, 6], [2, 3, 5, 6], [1, 3, 5, 6],
[1, 2, 3, 4], [1, 3, 4, 6], [1, 2, 4, 5],
[1, 2, 3, 6], [2, 3, 4, 6], [1, 2, 5, 6],
[1, 3, 4, 5], [1, 4, 5, 6], [2, 3, 4, 5]]
2对6的公平组合的结果
[[4, 6], [1, 3], [2, 5],
[3, 5], [1, 4], [2, 6],
[4, 5], [3, 6], [1, 2],
[2, 3], [5, 6], [1, 6],
[3, 4], [1, 5], [2, 4]]
2比5的公平组合的结果
[[4, 5], [2, 3], [3, 5],
[1, 2], [1, 4], [1, 5],
[2, 4], [3, 4], [1, 3],
[2, 5]]
获得5比12的组合的时间:
1.19 real 1.15 user 0.03 sys
答案 1 :(得分:0)
天真的实施将是:
class Integer
# naïve factorial implementation; no checks
def !
(1..self).inject(:*)
end
end
class Range
# constant Proc instance for tests; not needed
C_N_R = -> (n, r) { n.! / ( r.! * (n - r).! ) }
def fair_combination(n)
to_a.permutation
.map { |a| a.each_slice(n).to_a }
.each_with_object([]) do |e, memo|
e.map!(&:sort)
memo << e if memo.all? { |me| (me & e).empty? }
end
end
end
▶ (1..6).fair_combination(2)
#⇒ [
# [[1, 2], [3, 4], [5, 6]],
# [[1, 3], [2, 5], [4, 6]],
# [[1, 4], [2, 6], [3, 5]],
# [[1, 5], [2, 4], [3, 6]],
# [[1, 6], [2, 3], [4, 5]]]
▶ (1..6).fair_combination(3)
#⇒ [
# [[1, 2, 3], [4, 5, 6]],
# [[1, 2, 4], [3, 5, 6]],
# [[1, 2, 5], [3, 4, 6]],
# [[1, 2, 6], [3, 4, 5]],
# [[1, 3, 4], [2, 5, 6]],
# [[1, 3, 5], [2, 4, 6]],
# [[1, 3, 6], [2, 4, 5]],
# [[1, 4, 5], [2, 3, 6]],
# [[1, 4, 6], [2, 3, 5]],
# [[1, 5, 6], [2, 3, 4]]]
▶ Range::C_N_R[6, 3]
#⇒ 20
坦率地说,我不明白这个函数应该如何对10
和4
起作用,但无论如何这个实现太耗费内存以至于无法在大范围内正常工作(在我的机器上它会卡在范围内大小> 8。)
要将此调整为更强大的解决方案,我们需要摆脱permutation
,以支持“智能连接置换数组。”
希望这对初学者有好处。