From b4912b57289449048a71018815b2c967ca584816 Mon Sep 17 00:00:00 2001 From: Franz Diebold Date: Tue, 30 Jun 2020 11:03:59 +0200 Subject: [PATCH] Add solution for problem 58. --- README.md | 2 +- src/common/number_spiral.py | 37 ++++++++++++++ src/p028_number_spiral_diagonals.py | 18 ++----- src/p058_spiral_primes.py | 62 +++++++++++++++++++++++ test/common/test_number_spiral.py | 48 ++++++++++++++++++ test/test_p028_number_spiral_diagonals.py | 4 +- test/test_p058_spiral_primes.py | 42 +++++++++++++++ 7 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 src/common/number_spiral.py create mode 100644 src/p058_spiral_primes.py create mode 100644 test/common/test_number_spiral.py create mode 100644 test/test_p058_spiral_primes.py diff --git a/README.md b/README.md index 3414838..1120d5d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [Project Euler](https://projecteuler.net) Solutions -[![solved: 57 problems](https://img.shields.io/badge/solved-57_problems-f93.svg)](./src) +[![solved: 58 problems](https://img.shields.io/badge/solved-58_problems-f93.svg)](./src) ![Python: 3.8](https://img.shields.io/badge/Python-3.8-3776ab.svg) [![Lint and Test](https://github.com/FranzDiebold/project-euler-solutions/workflows/Lint%20and%20Test/badge.svg)](https://github.com/FranzDiebold/project-euler-solutions/actions?query=workflow%3A%22Lint+and+Test%22) [![license: MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](./LICENSE.md) diff --git a/src/common/number_spiral.py b/src/common/number_spiral.py new file mode 100644 index 0000000..679499a --- /dev/null +++ b/src/common/number_spiral.py @@ -0,0 +1,37 @@ +""" +Number spiral utilities. + +21 22 23 24 25 +20 7 8 9 10 +19 6 1 2 11 +18 5 4 3 12 +17 16 15 14 13 +""" + +from typing import Iterable, Tuple + + +def get_spiral_diagonal_values_with_size() -> Iterable[Tuple[int, int]]: + """ + Get the diagonal values of a number spiral. + Return tuples of `(, )`. + """ + value = 1 + current_half_size = 0 + yield value, (2 * current_half_size + 1) + while True: + current_half_size += 1 + for _ in range(4): + value += 2 * current_half_size + yield value, (2 * current_half_size + 1) + + +def get_spiral_diagonal_values_up_to_size(size: int) -> Iterable[int]: + """Get the diagonal values for a `size` by `size` spiral.""" + if size < 1 or size % 2 == 0: + raise ValueError('The size must be a positive odd number.') + + for current_value, current_size in get_spiral_diagonal_values_with_size(): + if current_size > size: + break + yield current_value diff --git a/src/p028_number_spiral_diagonals.py b/src/p028_number_spiral_diagonals.py index 0817585..716d5ae 100644 --- a/src/p028_number_spiral_diagonals.py +++ b/src/p028_number_spiral_diagonals.py @@ -16,26 +16,18 @@ What is the sum of the numbers on the diagonals in a 1001 by 1001 spiral formed in the same way? """ -from typing import Iterable +from src.common.number_spiral import get_spiral_diagonal_values_up_to_size -def get_spiral_diagonal_values(size: int) -> Iterable[int]: - """Get the diagonal values for a `size` by `size` spiral.""" - if size < 1 or size % 2 == 0: - raise ValueError('The size must be a positive odd number.') - - value = 1 - yield value - for current_size in range(1, (size // 2) + 1): - for _ in range(4): - value += 2*current_size - yield value +def get_spiral_diagonal_values_sum(spiral_size: int) -> int: + """Get the sum of the numbers on the diagonals in number spiral of size `spiral_size`.""" + return sum(get_spiral_diagonal_values_up_to_size(spiral_size)) def main() -> None: """Main function.""" spiral_size = 1001 - diagonal_sum = sum(get_spiral_diagonal_values(spiral_size)) + diagonal_sum = get_spiral_diagonal_values_sum(spiral_size) print(f'The sum of the numbers on the diagonals in a {spiral_size:,} by {spiral_size:,} ' \ f'spiral is {diagonal_sum:,}.') diff --git a/src/p058_spiral_primes.py b/src/p058_spiral_primes.py new file mode 100644 index 0000000..f2037a2 --- /dev/null +++ b/src/p058_spiral_primes.py @@ -0,0 +1,62 @@ +""" +Problem 58: Spiral primes +https://projecteuler.net/problem=58 + +Starting with 1 and spiralling anticlockwise in the following way, +a square spiral with side length 7 is formed. + +37 36 35 34 33 32 31 +38 17 16 15 14 13 30 +39 18 5 4 3 12 29 +40 19 6 1 2 11 28 +41 20 7 8 9 10 27 +42 21 22 23 24 25 26 +43 44 45 46 47 48 49 + +It is interesting to note that the odd squares lie along the bottom right diagonal, +but what is more interesting is that 8 out of the 13 numbers +lying along both diagonals are prime; that is, a ratio of 8/13 ≈ 62%. + +If one complete new layer is wrapped around the spiral above, +a square spiral with side length 9 will be formed. +If this process is continued, what is the side length of the square spiral +for which the ratio of primes along both diagonals first falls below 10%? +""" + +from typing import Iterable, Tuple + +from src.common.number_spiral import get_spiral_diagonal_values_with_size +from src.common.primes import is_prime + + +def get_spiral_diagonal_prime_ratios() -> Iterable[Tuple[int, int, int]]: + """ + For increasing number spiral sizes get the ratios of primes along the diagonals. + Returns tuples `(, , )`. + """ + number_of_primes = 0 + total_number = 0 + for value, size in get_spiral_diagonal_values_with_size(): + total_number += 1 + if value == size * size: + yield size, number_of_primes, total_number + else: + if is_prime(value): + number_of_primes += 1 + + +def main() -> None: + """Main function.""" + threshold_inverse = 10 + spiral_diagonal_prime_ratios_iter = get_spiral_diagonal_prime_ratios() + next(spiral_diagonal_prime_ratios_iter) + for size, number_of_primes, total_number in spiral_diagonal_prime_ratios_iter: + if number_of_primes * threshold_inverse < total_number: + print(f'The side length of the square spiral for which the ratio of primes ' \ + f'along both diagonals first falls below 10% is {size:,}.') + print(f'This ratio is {number_of_primes:,} / {total_number:,}.') + break + + +if __name__ == '__main__': + main() diff --git a/test/common/test_number_spiral.py b/test/common/test_number_spiral.py new file mode 100644 index 0000000..706f6eb --- /dev/null +++ b/test/common/test_number_spiral.py @@ -0,0 +1,48 @@ +""" +Number spiral utilities. + +21 22 23 24 25 +20 7 8 9 10 +19 6 1 2 11 +18 5 4 3 12 +17 16 15 14 13 +""" + + +def test_get_spiral_diagonal_values_with_size(): + # arrange + from src.common.number_spiral import get_spiral_diagonal_values_with_size + + # act + actual_result_iter = get_spiral_diagonal_values_with_size() + + # assert + expected_result = [ + (1, 1), + (3, 3), + (5, 3), + (7, 3), + (9, 3), + (13, 5), + (17, 5), + (21, 5), + (25, 5), + (31, 7), + (37, 7), + (43, 7), + (49, 7), + ] + for expected_value in expected_result: + assert next(actual_result_iter) == expected_value + + +def test_get_spiral_diagonal_values_up_to_size(): + # arrange + from src.common.number_spiral import get_spiral_diagonal_values_up_to_size + + # act + actual_result_iter = get_spiral_diagonal_values_up_to_size(7) + + # assert + expected_result = [1, 3, 5, 7, 9, 13, 17, 21, 25, 31, 37, 43, 49] + assert list(actual_result_iter) == expected_result diff --git a/test/test_p028_number_spiral_diagonals.py b/test/test_p028_number_spiral_diagonals.py index 524c114..515aae1 100644 --- a/test/test_p028_number_spiral_diagonals.py +++ b/test/test_p028_number_spiral_diagonals.py @@ -19,10 +19,10 @@ def test_get_spiral_diagonal_values_sum(): # arrange - from src.p028_number_spiral_diagonals import get_spiral_diagonal_values + from src.p028_number_spiral_diagonals import get_spiral_diagonal_values_sum # act - actual_result = sum(get_spiral_diagonal_values(5)) + actual_result = get_spiral_diagonal_values_sum(5) # assert expected_result = 101 diff --git a/test/test_p058_spiral_primes.py b/test/test_p058_spiral_primes.py new file mode 100644 index 0000000..750dd00 --- /dev/null +++ b/test/test_p058_spiral_primes.py @@ -0,0 +1,42 @@ +""" +Problem 58: Spiral primes +https://projecteuler.net/problem=58 + +Starting with 1 and spiralling anticlockwise in the following way, +a square spiral with side length 7 is formed. + +37 36 35 34 33 32 31 +38 17 16 15 14 13 30 +39 18 5 4 3 12 29 +40 19 6 1 2 11 28 +41 20 7 8 9 10 27 +42 21 22 23 24 25 26 +43 44 45 46 47 48 49 + +It is interesting to note that the odd squares lie along the bottom right diagonal, +but what is more interesting is that 8 out of the 13 numbers +lying along both diagonals are prime; that is, a ratio of 8/13 ≈ 62%. + +If one complete new layer is wrapped around the spiral above, +a square spiral with side length 9 will be formed. +If this process is continued, what is the side length of the square spiral +for which the ratio of primes along both diagonals first falls below 10%? +""" + + +def test_get_spiral_diagonal_prime_ratios(): + # arrange + from src.p058_spiral_primes import get_spiral_diagonal_prime_ratios + + # act + actual_result_iter = get_spiral_diagonal_prime_ratios() + + # assert + expected_result = [ + (1, 0, 1), + (3, 3, 5), + (5, 5, 9), + (7, 8, 13), + ] + for expected_value in expected_result: + assert next(actual_result_iter) == expected_value