Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
388 changes: 388 additions & 0 deletions project_euler/problem_061/sol1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,388 @@
"""
Project Euler Problem 061: https://projecteuler.net/problem=61

Problem statement -
Triangle, square, pentagonal, hexagonal, heptagonal, and octagonal numbers are all
figurate (polygonal) numbers and are generated by the following formulae:

Triangle P(3,n) = n(n+1)/2 1,3,6,10,15,...
Square P(4,n) = n^2 1,4,9,16,25,...
Pentagonal P(5,n) = n(3n-1)/2 1,5,12,22,35,...
Hexagonal P(6,n) = n(2n-1) 1,6,15,28,45,...
Heptagonal P(7,n) = n(5n-3)/2 1,7,18,34,55,...
Octagonal P(8,n) = n(3n-2) 1,8,21,40,65,...

The ordered set of three 4-digit numbers: 8128, 2882, 8281,
has three interesting properties.

1. The set is cyclic, in that the last two digits of each number is the first two
digits of the next number (including the last number with the first).
2. Each polygonal type: triangle(P(3,127)), square(P(4,91)), and pentagonal(P(5,44)),
is represented by a different number in the set.
3. This is the only set of 4-digit numbers with this property.

Find the sum of the only ordered set of six cyclic -digit numbers for which each
polygonal type: triangle, square, pentagonal, hexagonal, heptagonal, and octagonal,
is represented by a different number in the set.


Solution explanation-
The solution is actually pretty simple using a brute force approach.
We first do some precomputations to get all the relevant sets of polygon numbers.
Individual helper functions have been made for the same.
We also have one more helper function to check if two given numbers are cyclic or not.

Now the approach is to pick one number from a set then try to find a number in another
set that is cyclic to the previous. Now consider this new number as previous and find
a new number in another set(set that has not been used yet).Continue doing this until
you either reach your last set, in which case you need only check for cyclic against
the first number, if it matches you have an answer. If you cannot find a cyclic number
for previous in your current set, you backtrack to the prev set and chose the next
number from it.

So code wise, we generate all possiblee permutations of the sets using itertools built
in permutations method. Then its justa bunch of for loops trying to find the answer.
Once we have a valid ordered set, we return its sum.

"""

import itertools


def fill_triangle(int_range: tuple[int, int] = (1000, 9999)) -> list[int]:
"""
Populates the triangle array with relevant integers i.e greater than
range[0] and less than range[1]+1.
The range provided should be inclusive.

>>> fill_triangle((1,20))
[1, 3, 6, 10, 15]

"""

def triangle(num: int) -> int:
"""
Gives the Nth triangle number.
>>> triangle(2)
3
>>> triangle(5)
15
"""
return num * (num + 1) // 2

st, end = int_range
arr = []
i = 1
while True:
k = triangle(i)
if k < st:
i += 1
continue
if k > end:
break
arr.append(k)
i += 1
return arr


def fill_square(int_range: tuple[int, int] = (1000, 9999)) -> list[int]:
"""
Populates the square array with relevant integers i.e greater than
range[0] and less than range[1]+1.
The range provided should be inclusive.

>>> fill_square((1,30))
[1, 4, 9, 16, 25]

"""

def square(num: int) -> int:
"""
Gives the Nth square number.
>>> square(2)
4
>>> square(5)
25
"""
return num**2

st, end = int_range
arr = []
i = 1
while True:
k = square(i)
if k < st:
i += 1
continue
if k > end:
break
arr.append(k)
i += 1
return arr


def fill_pentagonal(int_range: tuple[int, int] = (1000, 9999)) -> list[int]:
"""
Populates the pentagonal array with relevant integers i.e greater than
range[0] and less than range[1]+1.
The range provided should be inclusive.

>>> fill_pentagonal((1,40))
[1, 5, 12, 22, 35]

"""

def pentagon(num: int) -> int:
"""
Gives the Nth pentagon number.
>>> pentagon(2)
5
>>> pentagon(5)
35
"""
return num * (3 * num - 1) // 2

st, end = int_range
arr = []
i = 1
while True:
k = pentagon(i)
if k < st:
i += 1
continue
if k > end:
break
arr.append(k)
i += 1
return arr


def fill_hexagonal(int_range: tuple[int, int] = (1000, 9999)) -> list[int]:
"""
Populates the hexagonal array with relevant integers i.e greater than
range[0] and less than range[1]+1.
The range provided should be inclusive.

>>> fill_hexagonal((1,50))
[1, 6, 15, 28, 45]

"""

def hexagon(num: int) -> int:
"""
Gives the Nth hexagon number.
>>> hexagon(2)
6
>>> hexagon(5)
45
"""
return num * (2 * num - 1)

st, end = int_range
arr = []
i = 1
while True:
k = hexagon(i)
if k < st:
i += 1
continue
if k > end:
break
arr.append(k)
i += 1
return arr


def fill_heptagonal(int_range: tuple[int, int] = (1000, 9999)) -> list[int]:
"""
Populates the heptagonal array with relevant integers i.e greater than
range[0] and less than range[1]+1.
The range provided should be inclusive.

>>> fill_heptagonal((1,60))
[1, 7, 18, 34, 55]

"""

def heptagon(num: int) -> int:
"""
Gives the Nth heptagon number.
>>> heptagon(2)
7
>>> heptagon(5)
55
"""
return num * (5 * num - 3) // 2

st, end = int_range
arr = []
i = 1
while True:
k = heptagon(i)
if k < st:
i += 1
continue
if k > end:
break
arr.append(k)
i += 1
return arr


def fill_octagonal(int_range: tuple[int, int] = (1000, 9999)) -> list[int]:
"""
Populates the octagonal array with relevant integers i.e greater than
range[0] and less than range[1]+1.
The range provided should be inclusive.

>>> fill_octagonal((1,70))
[1, 8, 21, 40, 65]


"""

def octagon(num: int) -> int:
"""
Gives the Nth octagon number.
>>> octagon(2)
8
>>> octagon(5)
65
"""
return num * (3 * num - 2)

st, end = int_range
arr = []
i = 1
while True:
k = octagon(i)
if k < st:
i += 1
continue
if k > end:
break
arr.append(k)
i += 1
return arr


def check_cyclic(num1: int, num2: int) -> bool:
"""
This function checks if two 4 digit numbers are cyclic.
For this problem we are only concerned with 4 digit numbers.

Raises:
ValueError


>>> check_cyclic(8228, 2810)
True
>>> check_cyclic(8228, 2410)
False

"""

if num1 < 1000 or num2 < 1000:
raise ValueError("Both integers must be greater than 999")
if num1 > 9999 or num2 > 9999:
raise ValueError("Both integers must be less than 10000")

return (num1 % 100) == (num2 // 100)


def solution() -> int:
"""
The function gives a solution to problem 061 of project Euler.
The function does some precomputations first to get all the
relevant sets of polygon numbers then applies a for loop on
all possible permutations of the sets and check for a valid
answer.

>>> solution()
28684

"""

# Make initial arrays to store all types of numbers in individual arrays.
# This will make accessing them easier in the future.
# We will be fill each of these arrays only considering numbers greater
# than 999 and less than 10000.
# Since we will add them in a systematic manner, these will be present
# in a sorted order(increasing order).
triangle = fill_triangle()
square = fill_square()
pentagonal = fill_pentagonal()
hexagonal = fill_hexagonal()
heptagonal = fill_heptagonal()
octagonal = fill_octagonal()

# create a dictionary of all polygons for access later
polygon_dict = {
0: triangle,
1: square,
2: pentagonal,
3: hexagonal,
4: heptagonal,
5: octagonal,
}

answer = []

# The elements can be in any order from any of the sets,
# so we need to consider all possible permutations of the sets.
permutations = list(itertools.permutations(range(6)))

# Now we simply apply a for loop to consider all permutations and
# check if that permutation leads to a valid answer.
for perm in permutations:
first_set = polygon_dict[perm[0]]
second_set = polygon_dict[perm[1]]
third_set = polygon_dict[perm[2]]
fourth_set = polygon_dict[perm[3]]
fifth_set = polygon_dict[perm[4]]
sixth_set = polygon_dict[perm[5]]
for first in first_set:
for second in second_set:
prev = first
if not (check_cyclic(prev, second)):
continue
for third in third_set:
prev = second
if not (check_cyclic(prev, third)):
continue
for fourth in fourth_set:
prev = third
if not (check_cyclic(prev, fourth)):
continue
for fifth in fifth_set:
prev = fourth
if not (check_cyclic(prev, fifth)):
continue
for sixth in sixth_set:
prev = fifth
if not (check_cyclic(prev, sixth)):
continue
if check_cyclic(sixth, first):
answer = [
first,
second,
third,
fourth,
fifth,
sixth,
]
# print(answer)
return sum(answer)


if __name__ == "__main__":
# import time

# start_time = time.perf_counter()

print(f"{solution() = }")

# end_time = time.perf_counter()

# execution_time = end_time - start_time
# print(f"Execution time: {execution_time:.6f} seconds")