Skip to content
Open
164 changes: 155 additions & 9 deletions maths/lucas_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
https://en.wikipedia.org/wiki/Lucas_number
"""

import functools
import math


def recursive_lucas_number(n_th_number: int) -> int:
"""
Expand All @@ -21,14 +24,17 @@ def recursive_lucas_number(n_th_number: int) -> int:
"""
if not isinstance(n_th_number, int):
raise TypeError("recursive_lucas_number accepts only integer arguments.")
if n_th_number == 0:
return 2
if n_th_number == 1:
return 1

return recursive_lucas_number(n_th_number - 1) + recursive_lucas_number(
n_th_number - 2
)
# Use memoization to cache results and avoid redundant calculations
@functools.cache
def _recursive_lucas(n: int) -> int:
if n == 0:
return 2
if n == 1:
return 1
return _recursive_lucas(n - 1) + _recursive_lucas(n - 2)

return _recursive_lucas(n_th_number)


def dynamic_lucas_number(n_th_number: int) -> int:
Expand All @@ -49,10 +55,144 @@ def dynamic_lucas_number(n_th_number: int) -> int:
"""
if not isinstance(n_th_number, int):
raise TypeError("dynamic_lucas_number accepts only integer arguments.")

if n_th_number == 0:
return 2
if n_th_number == 1:
return 1

a, b = 2, 1
for _ in range(n_th_number):
for _ in range(2, n_th_number + 1):
a, b = b, a + b
return a

return b


def matrix_power_lucas_number(n_th_number: int) -> int:
"""
Returns the nth lucas number using matrix exponentiation
>>> matrix_power_lucas_number(1)
1
>>> matrix_power_lucas_number(20)
15127
>>> matrix_power_lucas_number(0)
2
>>> matrix_power_lucas_number(25)
167761
>>> matrix_power_lucas_number(-1.5)
Traceback (most recent call last):
...
TypeError: matrix_power_lucas_number accepts only integer arguments.
"""
if not isinstance(n_th_number, int):
raise TypeError("matrix_power_lucas_number accepts only integer arguments.")

if n_th_number == 0:
return 2
if n_th_number == 1:
return 1

def matrix_mult(a: list[list[int]], b: list[list[int]]) -> list[list[int]]:
return [
[
a[0][0] * b[0][0] + a[0][1] * b[1][0],
a[0][0] * b[0][1] + a[0][1] * b[1][1],
],
[
a[1][0] * b[0][0] + a[1][1] * b[1][0],
a[1][0] * b[0][1] + a[1][1] * b[1][1],
],
]

def matrix_power(matrix: list[list[int]], power: int) -> list[list[int]]:
# Start with identity matrix
result: list[list[int]] = [[1, 0], [0, 1]]
base = matrix

while power > 0:
if power % 2 == 1:
result = matrix_mult(result, base)
base = matrix_mult(base, base)
power //= 2

return result

# Lucas number matrix form: [[1, 1], [1, 0]]
base_matrix = [[1, 1], [1, 0]]
powered_matrix = matrix_power(base_matrix, n_th_number - 1)

# L(n) = powered_matrix[0][0] * L(1) + powered_matrix[0][1] * L(0)
# Where L(1) = 1, L(0) = 2
return powered_matrix[0][0] * 1 + powered_matrix[0][1] * 2


def closed_form_lucas_number(n_th_number: int) -> int:
"""
Returns the nth lucas number using the closed-form formula (Binet's formula)
>>> closed_form_lucas_number(1)
1
>>> closed_form_lucas_number(20)
15127
>>> closed_form_lucas_number(0)
2
>>> closed_form_lucas_number(25)
167761
>>> closed_form_lucas_number(-1.5)
Traceback (most recent call last):
...
TypeError: closed_form_lucas_number accepts only integer arguments.
"""
if not isinstance(n_th_number, int):
raise TypeError("closed_form_lucas_number accepts only integer arguments.")

if n_th_number == 0:
return 2
if n_th_number == 1:
return 1

# Golden ratio
phi = (1 + math.sqrt(5)) / 2
# Conjugate of golden ratio
psi = (1 - math.sqrt(5)) / 2

# Lucas number closed form: L(n) = phi^n + psi^n
return round(phi**n_th_number + psi**n_th_number)


# Global cache for performance optimization
_lucas_cache: dict[int, int] = {0: 2, 1: 1}


def cached_lucas_number(n_th_number: int) -> int:
"""
Returns the nth lucas number using a cached approach for optimal performance
>>> cached_lucas_number(1)
1
>>> cached_lucas_number(20)
15127
>>> cached_lucas_number(0)
2
>>> cached_lucas_number(25)
167761
>>> cached_lucas_number(-1.5)
Traceback (most recent call last):
...
TypeError: cached_lucas_number accepts only integer arguments.
"""
if not isinstance(n_th_number, int):
raise TypeError("cached_lucas_number accepts only integer arguments.")

if n_th_number in _lucas_cache:
return _lucas_cache[n_th_number]

# Calculate using the fastest method for uncached values
if n_th_number < 70: # For smaller values, closed form is efficient
result = closed_form_lucas_number(n_th_number)
else: # For larger values, matrix exponentiation is more stable
result = matrix_power_lucas_number(n_th_number)

_lucas_cache[n_th_number] = result
return result


if __name__ == "__main__":
Expand All @@ -64,3 +204,9 @@ def dynamic_lucas_number(n_th_number: int) -> int:
print(" ".join(str(recursive_lucas_number(i)) for i in range(n)))
print("\nUsing dynamic function to calculate lucas series:")
print(" ".join(str(dynamic_lucas_number(i)) for i in range(n)))
print("\nUsing matrix exponentiation to calculate lucas series:")
print(" ".join(str(matrix_power_lucas_number(i)) for i in range(n)))
print("\nUsing closed-form formula to calculate lucas series:")
print(" ".join(str(closed_form_lucas_number(i)) for i in range(n)))
print("\nUsing cached function to calculate lucas series:")
print(" ".join(str(cached_lucas_number(i)) for i in range(n)))