在尝试解决“网格上的路径”问题时,我编写了代码
def paths(n, k)
p = (1..n+k).to_a
p.combination(n).to_a.size
end
代码工作正常,例如if n == 8 and k == 2
代码返回45
这是正确的路径数。
然而,当使用更大的数字时代码非常慢,我正在努力弄清楚如何加快这个过程。
答案 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
请注意,这里的n
和k
指的是与您的方法略有不同的东西,但我保留它们,因为它是该函数的组合数学中的高度标准命名法。
答案 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)
这不是地球上最快的东西,但我可以使用。如果遇到问题,我可以考虑进行优化