更快n选择k组合红宝石组合

时间:2016-05-18 13:51:10

标签: ruby combinatorics

在尝试解决“网格上的路径”问题时,我编写了代码

def paths(n, k)
  p = (1..n+k).to_a
  p.combination(n).to_a.size
end

代码工作正常,例如if n == 8 and k == 2代码返回45这是正确的路径数。

然而,当使用更大的数字时代码非常慢,我正在努力弄清楚如何加快这个过程。

6 个答案:

答案 0 :(得分:5)

只需编写定义组合数量的function,而不是构建组合数组来计算它。我确信还有宝石包括这个和许多其他组合功能。

请注意,我使用gem Distribution作为Math.factorial方法,但这是另一个容易编写的方法。但鉴于此,我建议采用@ stefan的答案,因为它的开销较小。

def n_choose_k(n, k)
  Math.factorial(n) / (Math.factorial(k) * Math.factorial(n - k))
end

n_choose_k(10, 8)
# => 45

请注意,这里的nk指的是与您的方法略有不同的东西,但我保留它们,因为它是该函数的组合数学中的高度标准命名法。

答案 1 :(得分:4)

def combinations(n, k)
  return 1 if k == 0 or k == n
  (k + 1 .. n).reduce(:*) / (1 .. n - k).reduce(:*)
end

combinations(8, 2)  #=> 28

关于数学部分的说明

原始等式是

combinations(n, k) = n! / k!(n - k)!

n! / k! = (1 * 2 * ... * n) / (1 * 2 * ... * k)以来,对于任何k <= n,分子和分母都有(1 * 2 * ... * k)因子,因此我们可以取消此因子。这使得等式成为

combinations(n, k) = (k + 1) * (k + 2) * ... * (n) / (n - k)!

这正是我在Ruby代码中所做的。

答案 2 :(得分:3)

建议计算全因子的答案在处理大数字时会产生大量不必要的开销。您应该使用以下方法计算二项式系数:n!/(k!(n-k)!)

def n_choose_k(n, k)
  return 0 if k > n
  result = 1
  1.upto(k) do |d|
    result *= n
    result /= d
    n -= 1
  end
  result
end

这将执行所需的最少操作。请注意,在递减n的同时递增d可确保不会出现舍入错误。例如,{n,n + 1}保证至少有一个可被2整除的元素,{n,n + 1,n + 2}保证至少有一个元素可以被3整除,依此类推。

您的代码可以重写为:

def paths(x, y)
  # Choice of x or y for the second parameter is arbitrary
  n_choose_k(x + y, x)
end

puts paths(8, 2) # 45
puts paths(2, 8) # 45

我假设原始版本中的n和k意味着尺寸,所以我将它们标记为x和y。这里不需要生成数组。

编辑:这是一个基准脚本......

require 'distribution'

def puts_time
  $stderr.puts 'Completed in %f seconds' % (Time.now - $start_time)
  $start_time = Time.now
end

def n_choose_k(n, k)
  return 0 if k > n
  result = 1
  1.upto(k) do |d|
    result *= n
    result /= d
    n -= 1
  end
  result
end

def n_choose_k_distribution(n, k)
  Math.factorial(n) / (Math.factorial(k) * Math.factorial(n - k))
end

def n_choose_k_inject(n, k)
  (1..n).inject(:*) / ((1..k).inject(:*) * (1..n-k).inject(:*))
end

def benchmark(&callback)
  100.upto(300) do |n|
    25.upto(75) do |k|
      callback.call(n, k)
    end
  end
end

$start_time = Time.now

puts 'Distribution gem...'
benchmark { |n, k| n_choose_k_distribution(n, k) }
puts_time

puts 'Inject method...'
benchmark { |n, k| n_choose_k_inject(n, k) }
puts_time

puts 'Answer...'
benchmark { |n, k| n_choose_k(n, k) }
puts_time

我系统的输出是:

Distribution gem...
Completed in 1.141804 seconds
Inject method...
Completed in 1.106018 seconds
Answer...
Completed in 0.150989 seconds

答案 3 :(得分:0)

由于您对计数感兴趣而不是实际的组合集,因此您应该使用choose函数执行此操作。数学定义涉及评估三个不同的阶乘,但是有很多取消正在进行,所以你可以通过使用范围来避免计算将被取消。

class Integer
  def choose(k)
    fail 'k > n' if k > self
    fail 'args must be positive' if k < 0 or self < 1
    return 1 if k == n || k == 0
    mm = [self - k, k].minmax
    (mm[1]+1..self).reduce(:*) / (2..mm[0]).reduce(:*)
  end
end

p 8.choose 6  # => 28

要解决您的路径问题,您可以定义

def paths(n, k)
  (n + k).choose(k)
end

p paths(8, 2)  # => 45

答案 4 :(得分:0)

reduce / inject版本很不错。但由于速度似乎有点问题,我建议来自@ google-fail的n_choose_k版本。 这是非常有见地的,并表明速度增加了~10倍。

我建议迭代使用较小的k和(n - k)。 N-choose-K和N-choose-(N-K)产生相同的结果(分母中的因子被简单地颠倒)。所以像52-choose-51这样的东西可以在一次迭代中完成。

答案 5 :(得分:0)

我通常执行以下操作:

class Integer
  def !
    (2..self).reduce(1, :*)
  end

  def choose(k)
    self.! / (k.! * (self-k).!)
  end
end

基准化:

k = 5

Benchmark.bm do |x|
  [10, 100, 1000, 10000, 100000].each do |n|
    x.report("#{n}") { n.choose(k) }
  end
end

在我的机器上,我得到:

       user     system      total        real
10  0.000008   0.000001   0.000009 (  0.000006)
100  0.000027   0.000003   0.000030 (  0.000031)
1000  0.000798   0.000094   0.000892 (  0.000893)
10000  0.045911   0.013201   0.059112 (  0.059260)
100000  4.885310   0.229735   5.115045 (  5.119902)

这不是地球上最快的东西,但我可以使用。如果遇到问题,我可以考虑进行优化