Skip to content
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()