Reputation: 13
I have made a function that finds all divisors that runs decently fast but for what I am trying to do I want it to run faster. I have also already created a primeFactors function. What I am doing is finding all possible powers that can be made and multiplying them.
from functools import reduce
from itertools import product
def divisors(n):
x = sorted(primeFactors(n))
r = []
for values in set(x):
t = list(range(0, x.count(values) + 1))
r.append(t)
comb = list(product(*r))
divisors = []
for sets in comb:
n = [a**b for a, b in zip(set(x), sets)]
divisors.append(reduce(lambda x, y: x*y, n))
return sorted(divisors)
Is there another approach that is faster or any improvements that can be made to this code to make it faster? For the sake of the problem lets assume my primeFactors is the fastest in the world.
Upvotes: 0
Views: 114
Reputation: 28636
Almost three times as fast as @kaya3's solutions and about 13 times as fast as your original (on kaya3's example number 3333960000):
def divisors(n):
divisors = [1]
for p in primeFactors(n):
if divisors[-1] % p:
divs = divisors
divs = [d * p for d in divs]
divisors += divs
return sorted(divisors)
For example if the prime factors are [2, 2, 2, 3, 3]
, then you can multiply the initial 1
with the prime factor 2
up to three times, giving you the divisors [1, 2, 4, 8]
. All of which you can then multiply with prime factor 3
up to two times. Multiplying with it once gives you [3, 6, 12, 24]
and multiplying with it twice you [9, 18, 36, 72]
. These additional divisor lists are my divs
. At each new prime factor, I set divs
to the divisors
producible by the previous prime factors, and then keep multiplying it with the current prime factor and adding to the overall result.
This needs equal prime factors to appear consecutively, which is highly likely (once the factorization finds a prime factor, it likely extracts it completely so that the remaining factorization is faster). If that's not the case, just use sorted(primeFactors(n))
.
Benchmark results (times in seconds for 10,000 calls with n=3333960000):
0.22 primeFactors
95.43 divisors_aayush
15.28 divisors_kaya3_1
17.91 divisors_kaya3_2
5.40 divisors_stefan
0.21 primeFactors
89.29 divisors_aayush
17.91 divisors_kaya3_1
18.79 divisors_kaya3_2
6.81 divisors_stefan
0.20 primeFactors
94.21 divisors_aayush
21.28 divisors_kaya3_1
22.48 divisors_kaya3_2
7.36 divisors_stefan
Btw, just in case the result doesn't really have to be sorted: Without sorting the result it takes about 2.4 seconds. So that sorting is responsible for most of the time.
Upvotes: 1
Reputation: 51122
There are quite a lot of ways to make small efficiency gains in your function:
.count
to count each factor separately; the .count
method loops over the list each time it's called. You can count occurrences of all factors in one pass using a Counter.append
in a loop.range
objects and the product
generator don't need to be converted to lists.a**b
many times for each b
in the range for the factor a
; you could take the product over a list of a**b
instead of just the range for b
.reduce
with operator.mul
is faster than with a lambda..sort
method should be a little bit faster than sorted
.Some of these improvements involve removing or changing enough of the code that the other improvements are moot. Here's the resulting code:
from functools import reduce
from itertools import product
from collections import Counter
from operator import mul
def divisors2(n):
factors = primeFactors(n)
r = [
[ a ** b for b in range(t + 1) ]
for a, t in Counter(factors).items()
]
d = [ reduce(mul, sets) for sets in product(*r) ]
d.sort()
return d
Overall this gives a modest to large improvement in running time, depending on the number of divisors:
1234567891
which is a prime number, the time saved is about 1%.1234567890 == 2 * 3 * 3 * 5 * 3607 * 3803
the time saved is about 10%.3333960000 == 2**6 * 3**5 * 5**4 * 7**3
the time saved is about 75%.To do a bit better, we can save multiplying the same numbers over and over in the reduce
stage by redesigning the implementation to use a recursive function instead of itertools.product
: this allows us to do fewer multiplications overall.
def divisors3(n):
factors = primeFactors(n)
factor_counts = list(Counter(factors).items())
def helper(x, i):
if i < 0:
d.append(x)
else:
a, t = factor_counts[i]
for b in range(t + 1):
helper(x, i - 1)
x *= a
d = []
helper(1, len(factor_counts) - 1)
d.sort()
return d
This improves the time saved from 75% to 80% for the number with many prime factors. The difference for the other test cases is too small to matter.
I compared all three functions using the following fairly basic primeFactors
implementation (from this post):
def primeFactors(n):
i = 2
factors = []
while i * i <= n:
if n % i:
i += 1
else:
n //= i
factors.append(i)
if n > 1:
factors.append(n)
return factors
Upvotes: 2