Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Optimise the solution to Project Euler 12 (Python)

I have the following code for Project Euler Problem 12. However, it takes a very long time to execute. Does anyone have any suggestions for speeding it up?

n = input("Enter number: ")
def genfact(n):
    t = []
    for i in xrange(1, n+1):
        if n%i == 0:
            t.append(i)
    return t

print "Numbers of divisors: ", len(genfact(n))
print

m = input("Enter the number of triangle numbers to check: ")
print
for i in xrange (2, m+2):
    a = sum(xrange(i))
    b = len(genfact(a))
    if b > 500:
        print a

For n, I enter an arbitrary number such as 6 just to check whether it indeed returns the length of the list of the number of factors. For m, I enter entered 80 000 000

It works relatively quickly for small numbers. If I enter b > 50 ; it returns 28 for a, which is correct.

like image 924
TopGun Avatar asked Feb 16 '23 21:02

TopGun


2 Answers

My answer here isn't pretty or elegant, it is still brute force. But, it simplifies the problem space a little and terminates successfully in less than 10 seconds.

Getting factors of n: Like @usethedeathstar mentioned, it is possible to test for factors only up to n/2. However, we can do better by testing only up to the square root of n:

let n = 36
=> factors(n) : (1x36, 2x18, 3x12, 4x9, 6x6, 9x4, 12x3, 18x2, 36x1)

As you can see, it loops around after 6 (the square root of 36). We also don't need to explicitly return the factors, just find out how many there are... so just count them off with a generator inside of sum():

import math

def get_factors(n):
    return sum(2 for i in range(1, round(math.sqrt(n)+1)) if not n % i)

Testing the triangular numbers

I have used a generator function to yield the triangular numbers:

def generate_triangles(limit):
    l = 1
    while l <= limit:
        yield sum(range(l + 1))
        l += 1

And finally, start testing:

def test_triangles():
    triangles = generate_triangles(100000)
    for i in triangles:
        if get_factors(i) > 499:
            return i

Running this with the profiler, it completes in less than 10 seconds:

$ python3 -m cProfile euler12.py 

361986 function calls in 8.006 seconds

The BIGGEST time saving here is get_factors(n) testing only up to the square root of n - this makes it heeeaps quicker and you save heaps of memory overhead by not generating a list of factors.

As I said, it still isn't pretty - I am sure there are more elegant solutions. But, it fits the bill of being faster :)

like image 126
Nick Burns Avatar answered Feb 27 '23 12:02

Nick Burns


I got my answer to run in 1.8 seconds with Python.

import time
from math import sqrt


def count_divisors(n):
    d = {}
    count = 1

    while n % 2 == 0:
        n = n / 2
        try:
            d[2] += 1
        except KeyError:
            d[2] = 1

    for i in range(3, int(sqrt(n+1)), 2):
        while n % i == 0 and i != n:
            n = n / i
            try:
                d[i] += 1
            except KeyError:
                d[i] = 1

    d[n] = 1

    for _,v in d.items():
        count = count * (v + 1)

    return count

def tri_number(num):
  next = 1 + int(sqrt(1+(8 * num)))
  return num + (next/2)


def main():
    i = 1
    while count_divisors(i) < 500:
        i = tri_number(i)
    return i


start = time.time()
answer = main()
elapsed = (time.time() - start)

print("result %s returned in %s seconds." % (answer, elapsed))

Here is the output showing the timedelta and correct answer:

$ python ./project012.py
result 76576500 returned in 1.82238006592 seconds.

Factoring

For counting the divisors, I start by initializing an empty dictionary and a counter. For each factor found, I create key of d[factor] with value of 1 if it does not exist, otherwise, I increment the value d[factor].

For example, if we counted the factors 100, we would see d = {25: 1, 2: 2}

The first while loop, I factor out all 2's, dividing n by 2 each time. Next, I begin factoring at 3, skipping two each time (since we factored all even numbers already), and stopping once I get to the square root of n+1.

We stop at the square_root of n because if there's a pair of factors with one of the numbers bigger than square_root of n, the other of the pair has to be less than 10. If the smaller one doesn't exist, there is no matching larger factor. https://math.stackexchange.com/questions/1343171/why-only-square-root-approach-to-check-number-is-prime

while n % 2 == 0:
        n = n / 2
        try:
            d[2] += 1
        except KeyError:
            d[2] = 1

    for i in range(3, int(sqrt(n+1)), 2):
        while n % i == 0 and i != n:
            n = n / i
            try:
                d[i] += 1
            except KeyError:
                d[i] = 1
d[n] = 1

Now that I have gotten each factor, and added it to the dictionary, we have to add the last factor (which is just n).


Counting Divisors

Now that the dictionary is complete, we loop through each of the items, and apply the following formula: d(n)=(a+1)(b+1)(c+1)... https://www.wikihow.com/Determine-the-Number-of-Divisors-of-an-Integer

All this formula means is taking all of the counts of each factor, adding 1, then multiplying them together. Take 100 for example, which has factors 25, 2, and 2. We would calculate d(n)=(a+1)(b+1) = (1+1)(2+1) = (2)(3) = 6 total divisors

for _,v in d.items():
    count = count * (v + 1)

return count

Calculate Triangle Numbers

Now, taking a look at tri_number(), you can see that I opted to calculate the next triangle number in a sequence without manually adding each whole number together (saving me millions of operations). Instead I used T(n) = n (n+1) / 2 http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/runsums/triNbProof.html

We are providing a whole number to the function as an argument, so we need to solve for n, which is going to be the whole number to add next. Once we have the next number (n), we simply add that single number to num and return

S=n(n+1)2

S=n2+n2

2S=n2+n

n2+n−2S=0

At this point, we use the quadratic formula for : ax2+bx+c=0.

n=−b±√b2−4ac / 2a

n=−1±√1−4(1)(−2S) / 2

n=−1±√1+8S / 2

https://socratic.org/questions/how-do-you-solve-for-n-in-s-n-n-1-2

So all tri_number() does is evaluate n=1+√1+8S / 2 (we ignore the negative equation here). The answer that is returned is the next triangle number in the sequence.

def tri_number(num):
  next = 1 + int(sqrt(1+(8 * num)))
  return num + (next/2)

Main Loop

Finally, we can look at main(). We start at whole number 1. We count the divisor of 1. If it is less than 500, we get the next triangle number, then try again and again until we get a number with > 500 divisors.

def main():
    i = 1
    while count_divisors(i) < 500:
        i = tri_number(i)
    return i

I am sure there are additional ways to optimize but I am not smart enough to understand those ways. If you find any better ways to optimize python, let me know! I originally solved project 12 in Golang, and that run in 25 milliseconds!

$ go run project012.go
76576500
2018/07/12 01:56:31 TIME: main() took 23.581558ms
like image 43
Brandon Avatar answered Feb 27 '23 12:02

Brandon