Caching polygonal numbers

Instead of iterating through all 4-digit numbers, pre-computing all polygonal numbers and then iterating through them is a better approach.

It does not change the find_cycle function by much, instead of trying every 4-digit number, it tries every polygonal number.

From solution2.py:

def find_cycle(first_two_digits, polygonals, cycle):
    if not polygonals:
        if cycle[0] // 100 == cycle[-1] % 100:
            return cycle
        return []

    for i in range(len(polygonals)):
        for p in polygonals[i]:
            if p // 100 == first_two_digits:
                new_cycle = find_cycle(p % 100, polygonals[:i] + polygonals[i + 1 :], cycle + [p])
                if new_cycle:
                    return new_cycle

    return []

The rest is also very similar, the difference is the list of polygon types which is now a list of sets of polygonal numbers and the iteration is done starting with the octogonals numbers.

From solution2.py:

def cyclical_figurate_numbers():
    triangles = set(n * (n + 1) // 2 for n in range(45, 141))
    squares = set(n**2 for n in range(32, 100))
    pentagonals = set(n * (3 * n - 1) // 2 for n in range(26, 82))
    hexagonals = set(n * (2 * n - 1) for n in range(23, 71))
    heptagonals = set(n * (5 * n - 3) // 2 for n in range(21, 64))
    octagonals = set(n * (3 * n - 2) for n in range(19, 59))

    polygonals = [triangles, squares, pentagonals, hexagonals, heptagonals]
    for p in octagonals:
        cycle = find_cycle(p, polygonals, [p])
        if cycle:
            return sum(cycle)

    return []