Skip to main content
4 of 4
deleted 348 characters in body; deleted 16 characters in body
Graipher
  • 41.1k
  • 7
  • 69
  • 133

Another way to speed this up is to keep a record of all the sum of divisors seen so far (by using a caching decorator on the function, like functools.lru_cache) and realize that you only need a single loop if you make this a generator and use the fact that a number is amicable if \$d(d(a)) = a\$ and \$d(a) \neq a\$:

import math
from functools import lru_cache

@lru_cache(None)
def sum_div(n):
    # Taken from AJNeufeld's answer
    total = 1
    for x in range(2, int(math.sqrt(n) + 1)):
        if n % x == 0:
            total += x
            y = n // x
            if y > x:
                total += y
    return total

def amicable_numbers(limit):
    for a in range(limit):
        b = sum_div(a)
        if a != b and sum_div(b) == a:
            yield a

print(sum(amicable_numbers(10000)))
    

This runs in a bit more than 4 milli-seconds on my computer.


As for timing the runtime, I usually either prefer using ipythons magic command %timeit, or writing a small context manager:

from time import perf_counter

class Timer:
    def __init__(self, name=""):
        self.name = name

    def __enter__(self):
        self.start = perf_counter()

    def __exit__(self, *args):
        runtime = perf_counter() - self.start

        # get it in nice units
        units = ["s", "ms", "μs"]
        for unit in units:
            if runtime > 1:
                break
            runtime *= 1000

        if self.name:
            print(f"{self.name}: {runtime:.1f}{unit}")
        else:
            print(f"{runtime:.1f}{unit}")

Which you can use like this:

with Timer("amicable numbers"):
    print(sum(set(amicable_numbers(10000))))
# XXXXX  # I don't want to give away the correct answer
# amicable numbers: 4.1ms

Note that this will not be more precise than micro-seconds due to the time it takes to run the context manager.

Graipher
  • 41.1k
  • 7
  • 69
  • 133