The question I\'m working on is:
Find which sum of squared factors are a perfect square given a specific range. So if the range was (1..10) you woul
I would start by introducing some helper methods (factors
and square?
) to make your code more readable.
Furthermore, I would reduce the number of ranges and arrays to improve memory usage.
require 'prime'
def factors(number)
[1].tap do |factors|
primes = number.prime_division.flat_map { |p, e| Array.new(e, p) }
(1..primes.size).each do |i|
primes.combination(i).each do |combination|
factor = combination.inject(:*)
factors << factor unless factors.include?(factor)
end
end
end
end
def square?(number)
square = Math.sqrt(number)
square == square.floor
end
def list_squared(m, n)
(m..n).map do |number|
sum = factors(number).inject { |sum, x| sum + x ** 2 }
[number, sum] if square?(sum)
end.compact
end
list_squared(1, 250)
A benchmark with a narrow range (up to 250
) shows only a minor improvement:
require 'benchmark'
n = 1_000
Benchmark.bmbm(15) do |x|
x.report("original_list_squared :") { n.times do; original_list_squared(1, 250); end }
x.report("improved_list_squared :") { n.times do; improved_list_squared(1, 250); end }
end
# Rehearsal -----------------------------------------------------------
# original_list_squared : 2.720000 0.010000 2.730000 ( 2.741434)
# improved_list_squared : 2.590000 0.000000 2.590000 ( 2.604415)
# -------------------------------------------------- total: 5.320000sec
# user system total real
# original_list_squared : 2.710000 0.000000 2.710000 ( 2.721530)
# improved_list_squared : 2.620000 0.010000 2.630000 ( 2.638833)
But a benchmark with a wider range (up to 10000
) shows a much better performance than the original implementation:
require 'benchmark'
n = 10
Benchmark.bmbm(15) do |x|
x.report("original_list_squared :") { n.times do; original_list_squared(1, 10000); end }
x.report("improved_list_squared :") { n.times do; improved_list_squared(1, 10000); end }
end
# Rehearsal -----------------------------------------------------------
# original_list_squared : 36.400000 0.160000 36.560000 ( 36.860889)
# improved_list_squared : 2.530000 0.000000 2.530000 ( 2.540743)
# ------------------------------------------------- total: 39.090000sec
# user system total real
# original_list_squared : 36.370000 0.120000 36.490000 ( 36.594130)
# improved_list_squared : 2.560000 0.010000 2.570000 ( 2.581622)
tl;dr: The bigger the N
the better my code performs compared to the original implementation...
The trick that frequently solves questions like this is to switch from trial division to a sieve. In Python (sorry):
def list_squared(m, n):
factor_squared_sum = {i: 0 for i in range(m, n + 1)}
for factor in range(1, n + 1):
i = n - n % factor # greatest multiple of factor less than or equal to n
while i >= m:
factor_squared_sum[i] += factor ** 2
i -= factor
return {i for (i, fss) in factor_squared_sum.items() if isqrt(fss) ** 2 == fss}
def isqrt(n):
# from http://stackoverflow.com/a/15391420
x = n
y = (x + 1) // 2
while y < x:
x = y
y = (x + n // x) // 2
return x
The next optimization is to step factor
only to isqrt(n)
, adding the factor squares in pairs (e.g., 2
and i // 2
).
One way to make it more efficient is to use Ruby's built-in method Prime::prime_division.
For any number n
, if prime_division
returns an array containing a single element, that element will be [n,1]
and n
will have been shown to be prime. That prime number has factors n
and 1
, so must be treated differently than numbers that are not prime.
require 'prime'
def list_squared(range)
range.each_with_object({}) do |i,h|
facs = Prime.prime_division(i)
ssq =
case facs.size
when 1 then facs.first.first**2 + 1
else facs.inject(0) { |tot,(a,b)| tot + b*(a**2) }
end
h[i] = facs if (Math.sqrt(ssq).to_i)**2 == ssq
end
end
list_squared(1..10_000)
#=> { 1=>[], 48=>[[2, 4], [3, 1]], 320=>[[2, 6], [5, 1]], 351=>[[3, 3], [13, 1]],
# 486=>[[2, 1], [3, 5]], 1080=>[[2, 3], [3, 3], [5, 1]],
# 1260=>[[2, 2], [3, 2], [5, 1], [7, 1]], 1350=>[[2, 1], [3, 3], [5, 2]],
# 1375=>[[5, 3], [11, 1]], 1792=>[[2, 8], [7, 1]], 1836=>[[2, 2], [3, 3], [17, 1]],
# 2070=>[[2, 1], [3, 2], [5, 1], [23, 1]], 2145=>[[3, 1], [5, 1], [11, 1], [13, 1]],
# 2175=>[[3, 1], [5, 2], [29, 1]], 2730=>[[2, 1], [3, 1], [5, 1], [7, 1], [13, 1]],
# 2772=>[[2, 2], [3, 2], [7, 1], [11, 1]], 3072=>[[2, 10], [3, 1]],
# 3150=>[[2, 1], [3, 2], [5, 2], [7, 1]], 3510=>[[2, 1], [3, 3], [5, 1], [13, 1]],
# 4104=>[[2, 3], [3, 3], [19, 1]], 4305=>[[3, 1], [5, 1], [7, 1], [41, 1]],
# 4625=>[[5, 3], [37, 1]], 4650=>[[2, 1], [3, 1], [5, 2], [31, 1]],
# 4655=>[[5, 1], [7, 2], [19, 1]], 4998=>[[2, 1], [3, 1], [7, 2], [17, 1]],
# 5880=>[[2, 3], [3, 1], [5, 1], [7, 2]], 6000=>[[2, 4], [3, 1], [5, 3]],
# 6174=>[[2, 1], [3, 2], [7, 3]], 6545=>[[5, 1], [7, 1], [11, 1], [17, 1]],
# 7098=>[[2, 1], [3, 1], [7, 1], [13, 2]], 7128=>[[2, 3], [3, 4], [11, 1]],
# 7182=>[[2, 1], [3, 3], [7, 1], [19, 1]], 7650=>[[2, 1], [3, 2], [5, 2], [17, 1]],
# 7791=>[[3, 1], [7, 2], [53, 1]], 7889=>[[7, 3], [23, 1]],
# 7956=>[[2, 2], [3, 2], [13, 1], [17, 1]],
# 9030=>[[2, 1], [3, 1], [5, 1], [7, 1], [43, 1]],
# 9108=>[[2, 2], [3, 2], [11, 1], [23, 1]], 9295=>[[5, 1], [11, 1], [13, 2]],
# 9324=>[[2, 2], [3, 2], [7, 1], [37, 1]]}
This calculation took approximately 0.15 seconds.
For i = 6174
(2**1) * (3**2) * (7**3) #=> 6174
and
1*(2**2) + 2*(3**2) + 3*(7**2) #=> 169 == 13*13