更快 n 选择 k 组合数组 ruby
faster n choose k for combination of array ruby
为了解决"paths on a grid"问题,我写了代码
def paths(n, k)
p = (1..n+k).to_a
p.combination(n).to_a.size
end
代码工作正常,例如 if n == 8 and k == 2
代码 returns 45
这是正确的路径数。
然而,当使用较大的数字时,代码非常慢,我正在努力想出如何加快这个过程。
与其构建组合数组只是为了计算它,不如编写定义组合数量的 function。我敢肯定还有 gem 包括这个和许多其他组合函数。
请注意,我将 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
所指的内容与您的方法略有不同,但我保留了它们,因为它是此函数组合学中的高度标准命名法。
由于您对计数而不是实际组合集感兴趣,因此您应该使用 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
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 代码中所做的。
建议计算全阶乘的答案在处理大数时会产生大量不必要的开销。您应该使用以下方法计算二项式系数: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} 保证至少有一个元素可被二整除,{n, n+1, n+2} 保证至少有一个元素可被三整除,依此类推。
您的代码可以重写为:
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
reduce/inject 版本不错。但是由于速度似乎有点问题,我建议使用来自@google-fail 的n_choose_k 版本。
它非常有见地,建议将速度提高约 10 倍。
我建议迭代使用 k 和 ( n - k ) 中的较小者。
N-choose-K 和 N-choose-(N-K) 产生相同的结果(分母中的因式只是颠倒过来了)。所以像 52-choose-51 这样的事情可以在一次迭代中完成。
我通常会做以下事情:
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)
这不是这个星球上最快的东西,但对我来说还可以。如果它成为一个问题,那么我可以考虑优化
为了解决"paths on a grid"问题,我写了代码
def paths(n, k)
p = (1..n+k).to_a
p.combination(n).to_a.size
end
代码工作正常,例如 if n == 8 and k == 2
代码 returns 45
这是正确的路径数。
然而,当使用较大的数字时,代码非常慢,我正在努力想出如何加快这个过程。
与其构建组合数组只是为了计算它,不如编写定义组合数量的 function。我敢肯定还有 gem 包括这个和许多其他组合函数。
请注意,我将 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
所指的内容与您的方法略有不同,但我保留了它们,因为它是此函数组合学中的高度标准命名法。
由于您对计数而不是实际组合集感兴趣,因此您应该使用 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
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 代码中所做的。
建议计算全阶乘的答案在处理大数时会产生大量不必要的开销。您应该使用以下方法计算二项式系数: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} 保证至少有一个元素可被二整除,{n, n+1, n+2} 保证至少有一个元素可被三整除,依此类推。
您的代码可以重写为:
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
reduce/inject 版本不错。但是由于速度似乎有点问题,我建议使用来自@google-fail 的n_choose_k 版本。 它非常有见地,建议将速度提高约 10 倍。
我建议迭代使用 k 和 ( n - k ) 中的较小者。 N-choose-K 和 N-choose-(N-K) 产生相同的结果(分母中的因式只是颠倒过来了)。所以像 52-choose-51 这样的事情可以在一次迭代中完成。
我通常会做以下事情:
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)
这不是这个星球上最快的东西,但对我来说还可以。如果它成为一个问题,那么我可以考虑优化