Euler Problem 12

The sequence of triangle numbers is generated by adding the natural numbers. So the 7th triangle number would be $1 + 2 + 3 + 4 + 5 + 6 + 7 = 28$. The first ten terms would be:

$1, 3, 6, 10, 15, 21, 28, 36, 45, 55, \dots$

Let us list the factors of the first seven triangle numbers:

 1: 1
 3: 1,3
 6: 1,2,3,6
10: 1,2,5,10
15: 1,3,5,15
21: 1,3,7,21
28: 1,2,4,7,14,28

We can see that 28 is the first triangle number to have over five divisors.

What is the value of the first triangle number to have over five hundred divisors?

Let's try brute forcing.

In [5]:
def get_divisors(n):
    return [i for i in range(1, int(n/2) + 1) if n % i == 0] + [n]

assert(get_divisors(28) == [1,2,4,7,14,28])

def triangle_number_generator_function():
    c = 0
    for i in range(1, 100000000000):
        c += i
        yield c

#ts = triangle_number_generator_function()
#for t in ts:
#    if len(get_divisors(t)) > 500:
#        print(t)
#        break

That failed miserably. We have to come up with something more efficient. I looked at my old Haskell solution which looks like this:

divisor_count' x = product . map (succ . length) . group $ prim_factors x

Add first I did not understand what it does at all. But the algorithm is as follows:

# get prime factors, for example for 28
2 * 2 * 7
# group the primes
[[2, 2], [7]]
# get the length of each group and increment it by one
[3, 2]
# get the product of all values
6

Honestly, I have no idea why this gives as the number of divisors. I will implement the solution and then try to come up with an explanation.

In [6]:
def is_prime(n, smaller_primes):
    for s in smaller_primes:
        if n % s == 0:
            return False
        if s * s > n:
            return True
    return True

def prime_generator_function():
    primes = [2, 3, 5, 7]
    for p in primes:
        yield p
    while True:
        p += 2
        if is_prime(p, primes):
            primes.append(p)
            yield p
            
def get_prime_factors(number):
    prime_generator = prime_generator_function()
    remainder = number
    factors = []
    for p in prime_generator:
        while remainder % p == 0:
            remainder /= p
            factors.append(p)
        if remainder == 1 or p * p > number:
            break
    if remainder != 1:
        factors.append(remainder)
    return factors

This are the prime numbers related functions we already now. Now we implement a group function, the product function we already know, and based on that the algorithm to get the number of divisors mentioned above.

In [7]:
def group(xs):
    from functools import reduce
    def f(xss, x):
        if xss and x in xss[-1]:
            xss[-1].append(x)
        else:
            xss.append([x])
        return xss
    return reduce(f, xs, [])

def product(xs):
    from functools import reduce
    from operator import mul
    return reduce(mul, xs, 1)

assert(group([1, 1, 3, 2, 2]) == [[1, 1], [3], [2, 2]])

def get_number_of_divisors(n):
    # get prime factors, for example for 28
    ps = get_prime_factors(n)
    # group the primes
    ps = group(ps)
    # get the length of each group and increment it by one
    ps = map(lambda x: len(x) + 1, ps)
    # get the product of all values
    return product(ps)
            
assert(get_number_of_divisors(28) == 6)

Now we are ready to do another brute force attempt.

In [8]:
ts = triangle_number_generator_function()
for t in ts:
    if get_number_of_divisors(t) > 500:
        print(t)
        break
76576500

Now the only question is why this crazy algorithm works. Okay, I got it with the help of this page. The problem is actually an instance of the multiplication principle for counting things. Each prime can be used to calculate (n + 1) other divisors. The incrementation by one is required because we can also choose to not use a certain prime. Of course, to get the potential combinations we have to get the product of the potential divisors for all primes. The web page explains it better. The only question is whether I came up with this algorithm myself back then.

In [ ]: