Skip to content

Performance and Refactor: 83% - 86% faster Project Euler 104 #10615

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
247 changes: 169 additions & 78 deletions project_euler/problem_104/sol1.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,124 +18,215 @@
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
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

>>> check(123456789987654321)
>>> is_pandigital_both(123456789987654321)
True

>>> check(120000987654321)
>>> is_pandigital_both(120000987654321)
False

>>> check(1234567895765677987654321)
>>> is_pandigital_both(1234567895765677987654321)
True

"""

check_last = [0] * 11
check_front = [0] * 11
return is_pandigital_end(number) and is_pandigital_start(number)

# 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
def is_pandigital_end(number: int) -> bool:
"""
Checks if the last 9 digits of a number are `1-9 pandigital`.

for x in range(9):
if not check_last[x + 1]:
f = False
if not f:
return f
Returns:
bool - True if the last 9 digits contain all the digits 1 to 9, False otherwise

# mark first 9 numbers
number = int(str(number)[:9])
>>> is_pandigital_end(123456789987654321)
True

for _ in range(9):
check_front[int(number % 10)] = 1
number = number // 10
>>> is_pandigital_end(120000987654321)
True

# check first 9 numbers for pandigitality
>>> is_pandigital_end(12345678957656779870004321)
False
"""
digit_count = [True] + [False] * 9

# Count the occurrences of each digit[0-9]
for _ in range(9):
number, mod = divmod(number, 10)
if digit_count[mod]:
return False
digit_count[mod] = True

for x in range(9):
if not check_front[x + 1]:
f = False
return f
# Return False if any digit is missing
return all(digit_count[1:])


def check1(number: int) -> bool:
def is_pandigital_start(number: int) -> bool:
"""
Takes a number and checks if it is pandigital from END
Checks if the first 9 digits of a number are `1-9 pandigital`.

>>> check1(123456789987654321)
True
Returns:
bool - True if the first 9 digits contain all the digits 1 to 9, False otherwise

>>> check1(120000987654321)
>>> is_pandigital_start(123456789987654321)
True

>>> check1(12345678957656779870004321)
>>> is_pandigital_start(120000987654321)
False

>>> is_pandigital_start(1234567895765677987654321)
True
"""

check_last = [0] * 11
number = int(str(number)[:9])
return is_pandigital_end(number)

# 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
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

for x in range(9):
if not check_last[x + 1]:
f = False
return f
# 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

def solution() -> int:
# 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:
"""
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
"""

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 check1(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 check(b):
return x + 3 # first 2 already done
# 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
# 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

# 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_start(fk):
return k

# Not found
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()