2008-10-28

Proyecto Euler: Problema 12

Sigo enfermo con el proyecto Euler. Y ya hice el problema 12. El mismo consiste en encontrar el primer número triangular con más de 500 divisores. Un número triangular es de la forma 1 + 2 + 3 + ... + n. Y, se puede calcular rápidamente como n*(n+1)/2.
Mi primer intento de resolución (que anduvo pero fue lento) consistió en usar el generador de números primos del ejercicio 3 para calcular la cantidad de divisores del número, calculándolo directamente. Acá va el código:


class Integer
def triangle
(self * (self + 1)) / 2
end
def divisor_count
n = self
pg = Primes.new
count = 1
while n != 1
p = pg.next
pow = 0
while n % p == 0
pow += 1
n /= p
end
count *= (pow + 1)
end
count
end
end

class OddPrimes
def initialize
@current = 1
@primes = []
end
attr_reader :current
def next
while not prime?(@current += 2)
end
@primes << @current
@current
end

def prime?(n)
i = 0
p = 1
while true
break if i >= @primes.length
p = @primes[i]
break if p * p > n
break if n % p == 0
i += 1
end
return i >= @primes.length || p * p > n
end
end

class Primes < OddPrimes
def initialize
super
@first = true
end
def next
if @first
@first = false
2
else
super
end
end
def current
odd_super = super
odd_super == 1 ? 2 : odd_super
end
end

n = 1
while n.triangle.divisor_count <= 500
n += 1
end

puts "Triangle #{n} (#{n.triangle}) is the first with over 500 divisors"

Anduvo, pero la corrida tardó como 4 minutos. Este es el resultado:

$ time ruby euler12.rb
Triangle 12375 (76576500) is the first with over 500 divisors

real 4m17.556s
user 3m57.519s
sys 0m16.117s

Así que me puse a pensar como hacer para que ande más rápido. Se me ocurrieron varias cosas:
  • Puedo calcular la factorización de n*(n+1)/2 sabiendo la factorización de n y n+1
  • Puedo calcular la factorización de cada número una sola vez (guardando factorizaciones viejas)
  • La factorización la puedo escribir recursivamente para usar las factorizaciones que ya calculé

Usando todo esto, hice una nueva implementación:

class OddPrimes
def initialize
@current = 1
@primes = []
end
attr_reader :current
def next
while not prime?(@current += 2)
end
@primes << @current
@current
end

def prime?(n)
i = 0
p = 1
while true
break if i >= @primes.length
p = @primes[i]
break if p * p > n
break if n % p == 0
i += 1
end
return i >= @primes.length || p * p > n
end
end

class Primes < OddPrimes
def initialize
super
@first = true
end
def next
if @first
@first = false
2
else
super
end
end
def current
odd_super = super
odd_super == 1 ? 2 : odd_super
end
end

class MemoFactorizator
def initialize
@factored = {1 => {} }
end
def factorize(n, pg = Primes.new)
return @factored[n] if @factored.include?( n )
p = pg.current
while n % p != 0
p = p.next
end
factorization = factorize( n / p, pg ).dup
factorization[p] = factorization.include?(p) ? factorization[p] + 1 : 1
@factored[n] = factorization
factorization
end
end

n = 1
mf = MemoFactorizator.new
while true
fact_n = mf.factorize(n)
fact_nn = mf.factorize(n+1)
triangle_factorization = fact_n.merge(fact_nn) { |k,old,new| old + new }
triangle_factorization[2] -= 1

divisor_count = triangle_factorization.values.inject(1) { |c,pow| c*(pow+1) }
break if divisor_count > 500
n += 1
end

puts "First triangle: #{n} (#{n*(n+1)/2})"
puts "Factorization: #{triangle_factorization.inspect}"
puts "Divisor count: #{divisor_count}"

La nueva implementación tardó 6 segundos en correr.

$ time ruby euler12.rb
First triangle: 12375 (76576500)
Factorization: {5=>3, 11=>1, 17=>1, 7=>1, 13=>1, 2=>2, 3=>2}
Divisor count: 576

real 0m6.690s
user 0m6.568s
sys 0m0.100s

Ahora estoy contento porque dio el mismo resultado que la vez anterior pero mucho más rápido y con código más lindo.

Happy hacking,
Aureliano.

No hay comentarios.: