开发者

Optimize non-abundant sums algorithm

I am trying to solve this Project Euler question:

A perfect number is a number for which the sum of its proper divisors is exactly equal to the number. For example, the sum of the proper divisors of 28 would be 1 + 2 + 4 + 7 + 14 = 28, which means that 28 is a perfect number.

A number n is called deficient if the sum of its proper divisors is less than n and it is called abundant if this sum exceeds n.

As 12 is the smallest abundant number, 1 + 2 + 3 + 4 + 6 = 16, the smallest number that can be written as the sum of two abundant numbers is 24. By mathematical analysis, it can be shown that all integers greater than 28123 can be written as the sum of two abundant numbers. However, this upper limit cannot be reduced any further by analysis even though it is known that the greatest number that cannot be expressed as the sum of two abundant numbers is less than this limit.

Find the sum of all the positive integers which cannot be written as the sum of two abundant numbers.

My solution:

#returns a list of the divisors of a given number
def Divs(Number):
    Divisors = []

    for i in range(2 , int(Number**0.5) + 1):
        if Number % i == 0:
            Divisors.append(i)

    for q in range(len(Divisors)):
        if Divisors[q] != (Number / Divisors[q]):
            Divisors.append(Number / Divisors[q])

    Divisors.insert(0,1)
    return Divisors

#returns a list of abundant numbers up to and including the limit
def AbList(limit):
    Abundant = []

    for i in range(11,limit + 1):
        if sum(Divs(i)) > i:
            Abundant.append(i)

    return Abundant

#Finds the sum of all positive integers that cannot be written as the
#sum of two abundant numbers...
def AbSum(limit):
    Abundant = AbList(limit)
    NoAbSum = 0
    for i in range(1 , limit):
        AbSum = 0
        x = 0
        for x in Abundant:
            if i - x in Abundant[:i]:
                AbSum = 1
                break
        if AbSum == 0:
            NoAbSum += i
    return NoAbSum

This took my 3.4 GhZ processor about 15 minutes to solve and I am searching for a better way. I'm not concerned with the first two functions because together they take less than a second to run. The third function is the kicker here. It runs through the range of numbers up to the limit (in this case, 20000-something), and each time, it runs through the list of abundant numbers, subtracting each from the current number, then checking that answer against the list of开发者_高级运维 abundant numbers. If there is a match, the loop breaks and tries again with the next number, all the way up to the limit.

I know there has got to be a better way of doing this but I'm somewhat new to programming. How can I speed up this algorithm?


You're testing every number between 1 and the limit (let's say 30000) against every abundant number, so you're doing roughly 30000 * 7428 iterations; and you're checking if the result is in a list, which is a very slow operation -- it checks every item on the list until it finds a match!

Instead, you should generate every number that is a sum of two abundant numbers. At the most, that would take 7428 * 7428 iterations -- fewer if properly executed (hint: avoid checking both a + b and b + a by ensuring that b is always >= a; and as someone else suggested, be sure to stop when sums get too large). Mark those numbers off a list of numbers below limit and sum the remaining numbers.

In other words:

[... 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43 ...]

becomes

[... 31, 0, 33, 34, 35, 0, 37, 0, 39, 0, 41, 0, 43 ...]

Edit: After playing with implementations for a few minutes, I can confidently say that if i - x in Abundant[:i]: is the problem. The first python solution posted to Project Euler's p23 forum is essentially a clever implementation of your algorithm, the only major difference being that it uses a set of abundant numbers instead of a list. It solves the problem on an Atom processor in 15 seconds; when I changed it to use a list, after fifteen minutes, it still hadn't solved the problem.

Moral of the story: x in list is SLOW.

Still, generating the sums directly is faster than subtracting and checking. :)


    for x in Abundant:
        if i - x in Abundant[:i]:
            AbSum = 1
            break

Note that the in expression here takes O(i) time, and thus the loop is O(n²). You can improve this to O(n) if you use a set instead of a list.


You can use a simple mathematical trick: The sum of all numbers that can not be written as a sum of two abundant numbers is the sum of all numbers minus the numbers that can be written as a sum of two abundant numbers:

 solution = sum(range(limit)) - sum(all_two_sums(abundant_numbers))

(sum(range(limit)) can also be simplified with math, but you might not find it unless you're Gauss ;-))

You already have a list of abundant numbers, so it's relatively easy to create the set of numbers that can be written as the sum of two abundant numbers and where the sum is smaller than the limit. Just make sure you have no duplicate numbers, a Python set does that.


Let's start by searching a little bit, and find out the largest number not expressible as the sum of two abundant numbers is actually 20161. Then, we can solve the problem with a simple set membership test. Plus, it runs pretty fast. :-)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from math import sqrt

def d(n):
    sum = 1
    t = sqrt(n)
    # only proper divisors; start from 2.
    for i in range(2, int(t)+1):
        if n % i == 0:
            sum += i + n / i
    # don't count the square root twice!
    if t == int(t):
        sum -= t
    return sum

limit = 20162
sum = 0
# it's a set, after all. sets are faster than lists for our needs.
abn = set()
for n in range(1, limit):
    if d(n) > n:
        abn.add(n)
    # if the difference of the number we're examining and every number in the set
    # is in the set, then the number is the sum of two abundant numbers.
    # otherwise, we must add it to our sum in question.
    if not any( (n-a in abn) for a in abn ):
        sum += n

Runs in 0.6463340939061518 seconds on average on an i5, based on timeit.


One thing that would help is bailing out of your inner loop once the abundant numbers go larger than the one you're testing.

I also don't understand this bit of your code:

 for q in range(len(Divisors)):
    if Divisors[q] != (Number / Divisors[q]):
        Divisors.append(Number / Divisors[q])

Once you have verified that the modulo is 0, it's a divisor. I don't know why you're doing an identity check essentially.


Your code looks like it might benefit from map, filter or list comprehension in favour of those for loops.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜