Анализ исходного кода - Рекуррентное соотношение времени: T(n)=T(n−1)+T(n−2)+Θ(1) \;T(n)=T(n-1)+T(n-2)+\Theta(1)\;T(n)=T(n−1)+T(n−2)+Θ(1). Решение даёт экспоненциальный рост: T(n)=Θ(φn)\;T(n)=\Theta(\varphi^n)T(n)=Θ(φn), где φ=1+52\varphi=\dfrac{1+\sqrt5}{2}φ=21+5. - По памяти: глубина рекурсии и стек вызовов S(n)=Θ(n)\;S(n)=\Theta(n)S(n)=Θ(n). (Количество вызовов примерно 2Fn+1−12F_{n+1}-12Fn+1−1.) Способы оптимизации (минимум три), с идеей, сложностью и кодом. 1) Мемоизация (топ‑down DP) - Идея: запоминать результаты подзадач, чтобы не пересчитывать. - Время: O(n)\mathrm{O}(n)O(n) (число уникальных вызовов), память: O(n)\mathrm{O}(n)O(n) для кеша (плюс рекурс. стек O(n)\mathrm{O}(n)O(n)). - Код: from functools import lru_cache @lru_cache(None) def fib_memo(n): if n <= 1: return n return fib_memo(n-1) + fib_memo(n-2) 2) Итеративный (bottom‑up) с O(1) памяти - Идея: вычислять снизу вверх, храня только два последних значения. - Время: O(n)\mathrm{O}(n)O(n), память: O(1)\mathrm{O}(1)O(1). - Код: def fib_iter(n): a, b = 0, 1 for _ in range(n): a, b = b, a + b return a 3) Быстрое удвоение (fast doubling) — O(log n) - Идея: использовать рекурсивные формулы F2k=Fk(2Fk+1−Fk),F2k+1=Fk+12+Fk2
F_{2k}=F_k(2F_{k+1}-F_k),\qquad F_{2k+1}=F_{k+1}^2+F_k^2 F2k=Fk(2Fk+1−Fk),F2k+1=Fk+12+Fk2
и вычислять по двоичному разложению n за логарифм шагов. - Время: O(logn)\mathrm{O}(\log n)O(logn) (считая арифметику за O(1)); память: рекурс. стек O(logn)\mathrm{O}(\log n)O(logn) (можно сделать итеративно для O(1)\mathrm{O}(1)O(1)). - Код: def fib_fast_doubling(n): def fd(n): if n == 0: return (0, 1) a, b = fd(n // 2) c = a * (2*b - a) d = a*a + b*b if n % 2 == 0: return (c, d) else: return (d, c + d) return fd(n)[0] Доп. вариант: матричное возведение в степень (матрица (1110)n\begin{pmatrix}1&1\\1&0\end{pmatrix}^n(1110)n) даёт тоже O(logn)\mathrm{O}(\log n)O(logn) времени через возведение в степень по бинарному возведению; полезно, если реализовать на целых числах или с оптимизированной матричной арифметикой. Примечания - Для очень больших nnn учитывать стоимость операций с большими целыми числами: реальная стоимость сложения/умножения влияет на асимптотику (вместо «констант» появляется сложность арифметики). - Самые практичные решения: итеративный (простота, O(1)\mathrm{O}(1)O(1) памяти) и fast doubling (лучшее асимпт. время O(logn)\mathrm{O}(\log n)O(logn)).
- Рекуррентное соотношение времени: T(n)=T(n−1)+T(n−2)+Θ(1) \;T(n)=T(n-1)+T(n-2)+\Theta(1)\;T(n)=T(n−1)+T(n−2)+Θ(1). Решение даёт экспоненциальный рост: T(n)=Θ(φn)\;T(n)=\Theta(\varphi^n)T(n)=Θ(φn), где φ=1+52\varphi=\dfrac{1+\sqrt5}{2}φ=21+5 .
- По памяти: глубина рекурсии и стек вызовов S(n)=Θ(n)\;S(n)=\Theta(n)S(n)=Θ(n). (Количество вызовов примерно 2Fn+1−12F_{n+1}-12Fn+1 −1.)
Способы оптимизации (минимум три), с идеей, сложностью и кодом.
1) Мемоизация (топ‑down DP)
- Идея: запоминать результаты подзадач, чтобы не пересчитывать.
- Время: O(n)\mathrm{O}(n)O(n) (число уникальных вызовов), память: O(n)\mathrm{O}(n)O(n) для кеша (плюс рекурс. стек O(n)\mathrm{O}(n)O(n)).
- Код:
from functools import lru_cache
@lru_cache(None)
def fib_memo(n):
if n <= 1:
return n
return fib_memo(n-1) + fib_memo(n-2)
2) Итеративный (bottom‑up) с O(1) памяти
- Идея: вычислять снизу вверх, храня только два последних значения.
- Время: O(n)\mathrm{O}(n)O(n), память: O(1)\mathrm{O}(1)O(1).
- Код:
def fib_iter(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
3) Быстрое удвоение (fast doubling) — O(log n)
- Идея: использовать рекурсивные формулы
F2k=Fk(2Fk+1−Fk),F2k+1=Fk+12+Fk2 F_{2k}=F_k(2F_{k+1}-F_k),\qquad F_{2k+1}=F_{k+1}^2+F_k^2
F2k =Fk (2Fk+1 −Fk ),F2k+1 =Fk+12 +Fk2 и вычислять по двоичному разложению n за логарифм шагов.
- Время: O(logn)\mathrm{O}(\log n)O(logn) (считая арифметику за O(1)); память: рекурс. стек O(logn)\mathrm{O}(\log n)O(logn) (можно сделать итеративно для O(1)\mathrm{O}(1)O(1)).
- Код:
def fib_fast_doubling(n):
def fd(n):
if n == 0:
return (0, 1)
a, b = fd(n // 2)
c = a * (2*b - a)
d = a*a + b*b
if n % 2 == 0:
return (c, d)
else:
return (d, c + d)
return fd(n)[0]
Доп. вариант: матричное возведение в степень (матрица (1110)n\begin{pmatrix}1&1\\1&0\end{pmatrix}^n(11 10 )n) даёт тоже O(logn)\mathrm{O}(\log n)O(logn) времени через возведение в степень по бинарному возведению; полезно, если реализовать на целых числах или с оптимизированной матричной арифметикой.
Примечания
- Для очень больших nnn учитывать стоимость операций с большими целыми числами: реальная стоимость сложения/умножения влияет на асимптотику (вместо «констант» появляется сложность арифметики).
- Самые практичные решения: итеративный (простота, O(1)\mathrm{O}(1)O(1) памяти) и fast doubling (лучшее асимпт. время O(logn)\mathrm{O}(\log n)O(logn)).