From 367f9b351729b64aad6109c0f40c896b60429b63 Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:28:08 +0530 Subject: [PATCH 01/11] Rename pandigital functions --- project_euler/problem_104/sol1.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index 60fd6fe99adb..abc2d6d0a0e1 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -18,18 +18,18 @@ sys.set_int_max_str_digits(0) # type: ignore -def check(number: int) -> bool: +def is_pandigital_both(number: int) -> bool: """ Takes a number and checks if it is pandigital both from start and end - >>> check(123456789987654321) + >>> is_pandigital_both(123456789987654321) True - >>> check(120000987654321) + >>> is_pandigital_both(120000987654321) False - >>> check(1234567895765677987654321) + >>> is_pandigital_both(1234567895765677987654321) True """ @@ -67,17 +67,17 @@ def check(number: int) -> bool: return f -def check1(number: int) -> bool: +def is_pandigital_end(number: int) -> bool: """ Takes a number and checks if it is pandigital from END - >>> check1(123456789987654321) + >>> is_pandigital_end(123456789987654321) True - >>> check1(120000987654321) + >>> is_pandigital_end(120000987654321) True - >>> check1(12345678957656779870004321) + >>> is_pandigital_end(12345678957656779870004321) False """ @@ -124,7 +124,7 @@ def solution() -> int: c1 = (a1 + b1) % m a1 = b1 % m b1 = c1 % m - if check1(b1): + if is_pandigital_end(b1): tocheck[x + 3] = 1 for x in range(1000000): @@ -132,7 +132,7 @@ def solution() -> int: a = b b = c # perform check only if in tocheck - if tocheck[x + 3] and check(b): + if tocheck[x + 3] and is_pandigital_both(b): return x + 3 # first 2 already done return -1 From f58875dff20fda5a99a7101764b0badcf890ed08 Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:37:30 +0530 Subject: [PATCH 02/11] Merge duplicated code in is_pandigital_both --- project_euler/problem_104/sol1.py | 34 +++++-------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index abc2d6d0a0e1..0e8bf793301b 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -34,37 +34,13 @@ def is_pandigital_both(number: int) -> bool: """ - check_last = [0] * 11 - check_front = [0] * 11 - - # mark last 9 numbers - for _ in range(9): - check_last[int(number % 10)] = 1 - number = number // 10 - # flag - f = True - - # check last 9 numbers for pandigitality - - for x in range(9): - if not check_last[x + 1]: - f = False - if not f: - return f + # Check end + if not is_pandigital_end(number): + return False - # mark first 9 numbers + # Check start number = int(str(number)[:9]) - - for _ in range(9): - check_front[int(number % 10)] = 1 - number = number // 10 - - # check first 9 numbers for pandigitality - - for x in range(9): - if not check_front[x + 1]: - f = False - return f + return is_pandigital_end(number) def is_pandigital_end(number: int) -> bool: From 417ba28dc41899d4be820feed6624c7d6ad3d16a Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:48:38 +0530 Subject: [PATCH 03/11] Refactor is_pandigital_end Renamed check_last -> digit_count Removed redundant element from digit_count Removed redundant flag variable --- project_euler/problem_104/sol1.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index 0e8bf793301b..2933f047a285 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -58,21 +58,18 @@ def is_pandigital_end(number: int) -> bool: """ - check_last = [0] * 11 + digit_count = [0] * 10 - # mark last 9 numbers + # Count the occurrences of each digit[0-9] for _ in range(9): - check_last[int(number % 10)] = 1 + digit_count[int(number % 10)] = 1 number = number // 10 - # flag - f = True - - # check last 9 numbers for pandigitality + # Return False if any digit is missing for x in range(9): - if not check_last[x + 1]: - f = False - return f + if not digit_count[x + 1]: + return False + return True def solution() -> int: From 6aa5b2a40381499551030fcb8a0bffc6e7b22e8a Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:46:46 +0530 Subject: [PATCH 04/11] Refactor solution Algorithm remains the same, only variables are renamed # Variable names: a, b, c -> fk_2, fk_1, fk # fk for k'th Fibonacci number a1, b1. c1 -> mk_2, mk_1, mk to_check -> end_pandigital m -> billion x -> k # k as in fk and mk --- project_euler/problem_104/sol1.py | 63 +++++++++++++++++-------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index 2933f047a285..5ef6c2a024bb 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -79,34 +79,41 @@ def solution() -> int: 329468 """ - a = 1 - b = 1 - c = 2 - # temporary Fibonacci numbers - - a1 = 1 - b1 = 1 - c1 = 2 - # temporary Fibonacci numbers mod 1e9 - - # mod m=1e9, done for fast optimisation - tocheck = [0] * 1000000 - m = 1000000000 - - for x in range(1000000): - c1 = (a1 + b1) % m - a1 = b1 % m - b1 = c1 % m - if is_pandigital_end(b1): - tocheck[x + 3] = 1 - - for x in range(1000000): - c = a + b - a = b - b = c - # perform check only if in tocheck - if tocheck[x + 3] and is_pandigital_both(b): - return x + 3 # first 2 already done + max_k: int = 10_00_000 + + # Fibonacci numbers + fk_2 = 1 # fk - 2 + fk_1 = 1 # fk - 1 + # fk # fk_1 + fk_2 + + # Fibonacci numbers mod billion + mk_2 = 1 % billion # (fk - 2) % billion + mk_1 = 1 % billion # (fk - 1) % billion + # mk # (fk ) % billion + + end_pandigital = [0] * max_k + billion = 1_000_000_000 # Equivalent to 10**9 + + # Check fibonacci numbers % 10**9 + for k in range(max_k): + mk = (mk_2 + mk_1) % billion + mk_2 = mk_1 + mk_1 = mk + + if is_pandigital_end(mk): + end_pandigital[k + 3] = 1 + + # Check fibonacci numbers + for k in range(max_k): + fk = fk_2 + fk_1 + fk_2 = fk_1 + fk_1 = fk + + # perform check only if k is in end_pandigital + if end_pandigital[k + 3] and is_pandigital_both(fk): + return k + 3 + + # Not found return -1 From 14c7f080bc1ee8e902506550a958a5f9fad87bca Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:58:48 +0530 Subject: [PATCH 05/11] Fix error prone range and element access --- project_euler/problem_104/sol1.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index 5ef6c2a024bb..00da0f82df6c 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -95,23 +95,23 @@ def solution() -> int: billion = 1_000_000_000 # Equivalent to 10**9 # Check fibonacci numbers % 10**9 - for k in range(max_k): + for k in range(3, max_k): mk = (mk_2 + mk_1) % billion mk_2 = mk_1 mk_1 = mk if is_pandigital_end(mk): - end_pandigital[k + 3] = 1 + end_pandigital[k] = 1 # Check fibonacci numbers - for k in range(max_k): + for k in range(3, max_k): fk = fk_2 + fk_1 fk_2 = fk_1 fk_1 = fk # perform check only if k is in end_pandigital - if end_pandigital[k + 3] and is_pandigital_both(fk): - return k + 3 + if end_pandigital[k] and is_pandigital_both(fk): + return k # Not found return -1 From 8f845ab03a0ac30933340d357eaed2ad128c11e9 Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Mon, 16 Oct 2023 22:01:10 +0530 Subject: [PATCH 06/11] Add function parameters in solution --- project_euler/problem_104/sol1.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index 00da0f82df6c..f66fb57e16d2 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -72,30 +72,28 @@ def is_pandigital_end(number: int) -> bool: return True -def solution() -> int: +def solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int: """ Outputs the answer is the least Fibonacci number pandigital from both sides. >>> solution() 329468 """ - max_k: int = 10_00_000 - # Fibonacci numbers - fk_2 = 1 # fk - 2 - fk_1 = 1 # fk - 1 + fk_2 = a # fk - 2 + fk_1 = b # fk - 1 # fk # fk_1 + fk_2 # Fibonacci numbers mod billion - mk_2 = 1 % billion # (fk - 2) % billion - mk_1 = 1 % billion # (fk - 1) % billion + mk_2 = a % billion # (fk - 2) % billion + mk_1 = b % billion # (fk - 1) % billion # mk # (fk ) % billion end_pandigital = [0] * max_k billion = 1_000_000_000 # Equivalent to 10**9 # Check fibonacci numbers % 10**9 - for k in range(3, max_k): + for k in range(ck, max_k): mk = (mk_2 + mk_1) % billion mk_2 = mk_1 mk_1 = mk @@ -104,7 +102,7 @@ def solution() -> int: end_pandigital[k] = 1 # Check fibonacci numbers - for k in range(3, max_k): + for k in range(ck, max_k): fk = fk_2 + fk_1 fk_2 = fk_1 fk_1 = fk From bdfc5a6a02a34d099bb72bb9ae1fd67f64e68edb Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Mon, 16 Oct 2023 22:48:27 +0530 Subject: [PATCH 07/11] Update docstrings --- project_euler/problem_104/sol1.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index f66fb57e16d2..357d57cc4314 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -20,8 +20,11 @@ def is_pandigital_both(number: int) -> bool: """ - Takes a number and checks if it is pandigital both from start and end + Checks if the first 9 and last 9 digits of a number are `1-9 pandigital`. + Returns: + bool - True if the first 9 and last 9 digits contain all the digits 1 to 9, + False otherwise >>> is_pandigital_both(123456789987654321) True @@ -31,7 +34,6 @@ def is_pandigital_both(number: int) -> bool: >>> is_pandigital_both(1234567895765677987654321) True - """ # Check end @@ -45,7 +47,10 @@ def is_pandigital_both(number: int) -> bool: def is_pandigital_end(number: int) -> bool: """ - Takes a number and checks if it is pandigital from END + Checks if the last 9 digits of a number are `1-9 pandigital`. + + Returns: + bool - True if the last 9 digits contain all the digits 1 to 9, False otherwise >>> is_pandigital_end(123456789987654321) True @@ -55,7 +60,6 @@ def is_pandigital_end(number: int) -> bool: >>> is_pandigital_end(12345678957656779870004321) False - """ digit_count = [0] * 10 @@ -74,11 +78,25 @@ def is_pandigital_end(number: int) -> bool: def solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int: """ - Outputs the answer is the least Fibonacci number pandigital from both sides. + Returns index `k` of the least Fibonacci number `F(k)` that is `1-9 pandigital` + from both sides. Here `ck <= k < max_k`. + + Parameters: + a: int - First fibonacci number `F(k)-2` + b: int - Second fibonacci number `F(k)-1` + ck: int - Initial index `k` of the Fibonacci number `F(k)` + max_k: int - Maximum index `k` of the Fibonacci number `F(k)` + + Returns: + int - index `k` of the least `1-9 pandigital` Fibonacci number `F(k)` + >>> solution() 329468 """ + # Equivalent to 10**9, for getting no higher then 9 digit numbers + billion = 1_000_000_000 + # Fibonacci numbers fk_2 = a # fk - 2 fk_1 = b # fk - 1 @@ -90,7 +108,6 @@ def solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int # mk # (fk ) % billion end_pandigital = [0] * max_k - billion = 1_000_000_000 # Equivalent to 10**9 # Check fibonacci numbers % 10**9 for k in range(ck, max_k): From 7d0a079d9798f5db53121bc25d8b287c0572fb57 Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:27:33 +0530 Subject: [PATCH 08/11] Fix ruff error. replace for loop with all() --- project_euler/problem_104/sol1.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index 357d57cc4314..2699c82e2c33 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -70,10 +70,7 @@ def is_pandigital_end(number: int) -> bool: number = number // 10 # Return False if any digit is missing - for x in range(9): - if not digit_count[x + 1]: - return False - return True + return all(digit_count[x + 1] for x in range(9)) def solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int: From 87b67ffaf3c2ec3bbea326157b64d3442ff12c56 Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:10:13 +0530 Subject: [PATCH 09/11] Separate is_pandigital_start from is_pandigital_both --- project_euler/problem_104/sol1.py | 40 +++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index 2699c82e2c33..a03d10b8ccf2 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -36,13 +36,7 @@ def is_pandigital_both(number: int) -> bool: True """ - # Check end - if not is_pandigital_end(number): - return False - - # Check start - number = int(str(number)[:9]) - return is_pandigital_end(number) + return is_pandigital_end(number) and is_pandigital_start(number) def is_pandigital_end(number: int) -> bool: @@ -61,16 +55,38 @@ def is_pandigital_end(number: int) -> bool: >>> is_pandigital_end(12345678957656779870004321) False """ - - digit_count = [0] * 10 + digit_count = [True] + [False] * 9 # Count the occurrences of each digit[0-9] for _ in range(9): - digit_count[int(number % 10)] = 1 - number = number // 10 + number, mod = divmod(number, 10) + if digit_count[mod]: + return False + digit_count[mod] = True # Return False if any digit is missing - return all(digit_count[x + 1] for x in range(9)) + return all(digit_count[1:]) + + +def is_pandigital_start(number: int) -> bool: + """ + Checks if the first 9 digits of a number are `1-9 pandigital`. + + Returns: + bool - True if the first 9 digits contain all the digits 1 to 9, False otherwise + + >>> is_pandigital_start(123456789987654321) + True + + >>> is_pandigital_start(120000987654321) + False + + >>> is_pandigital_start(1234567895765677987654321) + True + """ + + number = int(str(number)[:9]) + return is_pandigital_end(number) def solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int: From c2e2af8713f763c46ff25d97a7f30b44867231e0 Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:12:00 +0530 Subject: [PATCH 10/11] Performance: Add fast solution --- project_euler/problem_104/sol1.py | 68 ++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index a03d10b8ccf2..0a22e5ae411e 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -89,6 +89,62 @@ def is_pandigital_start(number: int) -> bool: return is_pandigital_end(number) +def slow_solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int: + """ + Returns index `k` of the least Fibonacci number `F(k)` that is `1-9 pandigital` + from both sides. Here `ck <= k < max_k`. + + Parameters: + a: int - First fibonacci number `F(k)-2` + b: int - Second fibonacci number `F(k)-1` + ck: int - Initial index `k` of the Fibonacci number `F(k)` + max_k: int - Maximum index `k` of the Fibonacci number `F(k)` + + Returns: + int - index `k` of the least `1-9 pandigital` Fibonacci number `F(k)` + + >>> slow_solution() + 329468 + """ + + # Equivalent to 10**9, for getting no higher then 9 digit numbers + billion = 1_000_000_000 + + # Fibonacci numbers + fk_2 = a # fk - 2 + fk_1 = b # fk - 1 + # fk # fk_1 + fk_2 + + # Fibonacci numbers mod billion + mk_2 = a % billion # (fk - 2) % billion + mk_1 = b % billion # (fk - 1) % billion + # mk # (fk ) % billion + + end_pandigital = [0] * max_k + + # Check fibonacci numbers % 10**9 + for k in range(ck, max_k): + mk = (mk_2 + mk_1) % billion + mk_2 = mk_1 + mk_1 = mk + + if is_pandigital_end(mk): + end_pandigital[k] = 1 + + # Check fibonacci numbers + for k in range(ck, max_k): + fk = fk_2 + fk_1 + fk_2 = fk_1 + fk_1 = fk + + # perform check only if k is in end_pandigital + if end_pandigital[k] and is_pandigital_both(fk): + return k + + # Not found + return -1 + + def solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int: """ Returns index `k` of the least Fibonacci number `F(k)` that is `1-9 pandigital` @@ -110,6 +166,9 @@ def solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int # Equivalent to 10**9, for getting no higher then 9 digit numbers billion = 1_000_000_000 + # For reserving 9 digits (and a few more digits for carry) from the start + billion_plus = billion * 1_000_000 + # Fibonacci numbers fk_2 = a # fk - 2 fk_1 = b # fk - 1 @@ -137,8 +196,15 @@ def solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int fk_2 = fk_1 fk_1 = fk + # We don't care about the digits after the 9'th one + # But still we need to keep some digits after after the 9'th + # Because of carry + if fk_2 > billion_plus: + fk_1 //= 10 + fk_2 //= 10 + # perform check only if k is in end_pandigital - if end_pandigital[k] and is_pandigital_both(fk): + if end_pandigital[k] and is_pandigital_start(fk): return k # Not found From 3ffa801056db380b31673165facd7acb3b63a3ea Mon Sep 17 00:00:00 2001 From: Manpreet Singh <63737630+ManpreetSingh2004@users.noreply.github.com> Date: Wed, 18 Oct 2023 23:18:24 +0530 Subject: [PATCH 11/11] Add performance benchmarks --- project_euler/problem_104/sol1.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/project_euler/problem_104/sol1.py b/project_euler/problem_104/sol1.py index 0a22e5ae411e..643a17db7a9d 100644 --- a/project_euler/problem_104/sol1.py +++ b/project_euler/problem_104/sol1.py @@ -211,5 +211,22 @@ def solution(a: int = 1, b: int = 1, ck: int = 3, max_k: int = 10_00_000) -> int return -1 +def benchmark() -> None: + """ + Benchmark + """ + # Running performance benchmarks... + # Solution : 8.59146850000252 to 9.774559199999203 + # Slow Sol : 57.75938980000137 to 61.15365279999969 + + from timeit import timeit + + print("Running performance benchmarks...") + + print(f"Solution : {timeit('solution()', globals=globals(), number=10)}") + print(f"Slow Sol : {timeit('slow_solution()', globals=globals(), number=10)}") + + if __name__ == "__main__": print(f"{solution() = }") + benchmark()