瓶颈在哪里?绩效差异......(项目欧拉#12)

时间:2014-05-20 01:59:55

标签: ruby performance

以下是 Project Euler 问题#12的解决方案:

def factor this_number, number=nil, *factors
  number = this_number if number.nil?
  m=2
  loop do 
    break factors << m if number % m == 0
    m+=1
  end
  arr = factors.flatten
  arr.inject(:*) != this_number ? factor(this_number, (number/factors.last), factors) : arr.uniq.collect {|k| arr.count(k)+1 }.inject(:*)
end

def highly_divisible_triangular_number number
  n = 2
  loop do
    num = (n*(n+1)/2)
    r = factor(num)
    break num if (r >= number)
    n+=1
  end
end

puts Benchmark.measure { p highly_divisible_triangular_number(n) }

这个解决方案是由Zachary Denton提供的:

require 'mathn' 

class Integer 
  def divisors
    return [1] if self == 1
    primes, powers = self.prime_division.transpose 
    exponents = powers.map{|i| (0..i).to_a} 
    divisors = exponents.shift.product(*exponents).map do |powers| 
      primes.zip(powers).map{|prime, power| prime ** power}.inject(:*) 
    end 
    divisors.sort.map{|div| [div, self / div]} 
  end
end

triangles = Enumerator.new do |yielder|
  i = 1
  loop do
    yielder.yield i * (i + 1) / 2
    i += 1
  end
end

puts Benchmark.measure { p triangles.detect { |t| t.divisors.count > n } }

基准测试结果:

__projecteuler: ruby 'problem_12a.rb' 100
73920
  0.010000   0.000000   0.010000 (  0.009514) [MINE]
73920
  0.020000   0.000000   0.020000 (  0.028339)

__projecteuler: ruby 'problem_12a.rb' 200
2031120
  0.120000   0.000000   0.120000 (  0.123996) [MINE]
2031120
  0.250000   0.010000   0.260000 (  0.251311)

__projecteuler: ruby 'problem_12a.rb' 300
2162160
  0.120000   0.000000   0.120000 (  0.122242) [MINE]
2162160
  0.260000   0.000000   0.260000 (  0.259200)

__projecteuler: ruby 'problem_12a.rb' 400
17907120
  0.730000   0.000000   0.730000 (  0.725883) [MINE]
17907120
  1.050000   0.000000   1.050000 (  1.057079)

__projecteuler: ruby 'problem_12a.rb' 500
76576500
  2.650000   0.010000   2.660000 (  2.657921) [MINE]
76576500
  2.790000   0.000000   2.790000 (  2.795859)

__projecteuler: ruby 'problem_12a.rb' 600
103672800
  3.470000   0.010000   3.480000 (  3.484551) [MINE]
103672800
  3.420000   0.000000   3.420000 (  3.419714)

__projecteuler: ruby 'problem_12a.rb' 700
236215980
  7.430000   0.010000   7.440000 (  7.438317) [MINE]
236215980
  6.020000   0.020000   6.040000 (  6.046869)

__projecteuler: ruby 'problem_12a.rb' 800
842161320
 24.040000   0.020000  24.060000 ( 24.062911) [MINE]
842161320
 14.780000   0.000000  14.780000 ( 14.781805)

问题

从基准测试结果中我可以看出,我的解决方案在N 500之前更快,但在&gt; N时被消灭。有人能帮助我理解为什么吗?


更新 对于那些认为瓶颈与递归有关的人,请再试一次。没有递归的factor方法和基准确实显示改进:

__projecteuler: ruby 'problem_12a.rb' 800
842161320
 24.960000   0.020000  24.980000 ( 24.973017) [MINE (w/o recursion)]
842161320
 14.780000   0.030000  14.810000 ( 14.807774)


def factor this_number
  number,arr=this_number,[]

  done = false
  until done
    m=2
    loop do 
      break arr << m if number % m == 0
      m+=1
    end
    arr.inject(:*) != this_number ? number = number/arr.last : done = true
  end

  arr.uniq.collect {|k| arr.count(k)+1 }.inject(:*)
end

2 个答案:

答案 0 :(得分:1)

Ruby以递归表现糟糕而臭名昭着。您也许可以阅读this post以获得有关如何解决它的一些指示。

答案 1 :(得分:1)

您的瓶颈在于您的分解方法。 (您的highly_divisible_triangular_number循环和他的triangles.detect循环基本相同。)

您的分解可以在计算上更快

(抱歉,我只需要重构你的代码。一切都在计算上都是一样的。)

def num_factors(number, dividend=nil, prime_factors_found=[])
  dividend = number if dividend.nil?

  smallest_prime_factor = (2..dividend).find { |divisor| dividend % divisor == 0 } # 1.
  prime_factors_found << smallest_prime_factor

  all_prime_factors_found = prime_factors_found.inject(:*) == number
  if !all_prime_factors_found
    return num_factors(number, dividend / prime_factors_found.last, prime_factors_found)
  end

  prime_factor_powerset_size = prime_factors_found.uniq.map do |prime_factor|
    prime_factors_found.count(prime_factor) + 1                                    # 2.
  end.inject(:*)

  return prime_factor_powerset_size
end
  1. 您无需检查每个号码。您可以检查2次,然后开始跳过偶数。然后,您可以在此之后检查3次,仅检查每6个数字中的2个 你也在这里重新计算了很多数字。例如,每次divisor为11(素数)时,您将完成整个循环,以便再次发现11确实是素数。最坏情况,对于n素数,您最多可以n^2验证所有素数。
  2. map是一个完整的素数因子。但是每次传递,count操作也需要通过数组。这意味着对于包含10个项目(O(n))的数组,您可以在数组中查找值100次(O(n^2))。检查事件可以在一次通过中完成。
  3. 他的因子分解可以节省计算(以及不计算的地方)

    您可能想知道为什么他的代码更快,因为它包含以下嵌套循环:

    divisors = exponents.shift.product(*exponents).map do |powers| 
      primes.zip(powers).map{|prime, power| prime ** power}.inject(:*) 
    end
    

    与#2一样,他对另一个线性传递中的每个元素进行了线性传递工作,即计算的这部分也是O(n^2)。当所有需要的是最终将会有的总数时,他通过实际计算所有除数来浪费计算。但无论如何,最终看来这一部分在渐近性能方面是相同的。

    事实证明,最终让他突破的版本之间的差异是#1 - 主要测试人员/发电机。虽然您有一个自定义循环,一遍又一遍地检查每个数字,但他使用了a prime tester/generator in the Ruby library。如果您想了解它是如何实现的,您可以在计算机上找到安装了ruby的源<ruby root>/lib/ruby/<version>/prime.rb

    以下是使用prime_division method

    的代码版本
    require 'prime'
    def num_factors2(number, dividend=nil, prime_factors_found=[])
      dividend = number if dividend.nil?
    
      smallest_prime_factor = dividend.prime_division.first.first    # this is the only change
      prime_factors_found << smallest_prime_factor
    
      all_prime_factors_found = prime_factors_found.inject(:*) == number
      if !all_prime_factors_found
        return num_factors2(number, dividend / prime_factors_found.last, prime_factors_found)
      end
    
      prime_factor_powerset_size = prime_factors_found.uniq.map do |prime_factor|
        prime_factors_found.count(prime_factor) + 1
      end.inject(:*)
    
      return prime_factor_powerset_size
    end
    

    我系统的基准测试:

    Benchmark.measure { (2..50000).map { |i| num_factors(i) } }
     =>  17.050000   0.020000  17.070000 ( 17.079428)
    Benchmark.measure { (2..50000).map { |i| num_factors2(i) } }
     =>   2.630000   0.010000   2.640000 (  2.672317)
    

    <强>附加

    使用两种方法的组合,我发现了一种很酷的方式来表达问题,就像红宝石中的单行一样! (那也很快!)

    n = (2..(1.0/0)).find { |i| (i*(i+1)/2).prime_division.inject(1) { |p,i| p*i[1]+p } > 500 }
    

    其中nn三角形数字,即triangle(n) = n * (n + 1) / 2