如何从n个元素的数组中获得“公平组合”?

时间:2016-06-21 10:58:37

标签: ruby algorithm math combinations

在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], ...]

是否有任何好的算法可以创建像这样的“公平组合”?

2 个答案:

答案 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

现在2abc 1d的高度为e0 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

坦率地说,我不明白这个函数应该如何对104起作用,但无论如何这个实现太耗费内存以至于无法在大范围内正常工作(在我的机器上它会卡在范围内大小> 8。)

要将此调整为更强大的解决方案,我们需要摆脱permutation,以支持“智能连接置换数组。”

希望这对初学者有好处。