Фибоначчи: цена повторных вычислений
Вычислим F(5): F(4)+F(3). Для F(4) снова нужны F(3)+F(2), так что F(3) решается дважды. Его ветви тоже повторяются. На рисунке нажмите F(3), чтобы увидеть одинаковые подзадачи. Наша цель — выполнять работу для каждого аргумента один раз.
Динамическое программирование (Dynamic programming) — Метод решения задачи через сохранение ответов на повторяющиеся подзадачи и переходы между ними.
Карта решения: к этим вопросам возвращайтесь в каждой новой задаче на динамическое программирование.
Наивная рекурсия F(n) = F(n−1) + F(n−2) повторно вычисляет одни и те же подзадачи. Дерево вызовов разрастается экспоненциально.
Это соглашение используется в тексте, моделях и коде всей основной части.
Во всей основной части F(0)=0, F(1)=1. Для F(5) получаем F(4)+F(3), а F(4) снова вызывает F(3). Нажмите F(k): все повторения того же аргумента выделятся.
Одинаковый аргумент обозначает ту же подзадачу. Кэш заполняется не больше одного раза для каждого аргумента.
fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)Многократно выполняются вызовы с одинаковым аргументом.
Для F(5) дерево содержит 15 вызовов, но всего 6 разных аргументов: 0…5. Три вызова F(2) решают одну и ту же подзадачу. Запоминание ответов сокращает повторную работу. Теперь, после примера, соберём вопросы, с которых будем начинать собственное ДП. В задачах о путях база будет другой: её нельзя копировать из Фибоначчи.
| Шаг проектирования | Что нужно сформулировать |
|---|---|
| Состояние | Какую подзадачу обозначает d[i] или d[i][j]; достаточно ли параметров? |
| База | Где ответ известен без переходов; чем обозначена недостижимость? |
| Переход | Какие предшественники нужны; складываем количества или выбираем оптимум? |
| Порядок | Все зависимости вычислены раньше текущего состояния. |
| Оценка | Число состояний × работа переходов; дополнительно память, стек и размер чисел. |
| Восстановление | Нужен ли сам путь и какие данные о предке следует сохранить? |
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Рекурсивный перебор одинаковых префиксов
Для n = 40 наивный Фибоначчи уже делает сотни миллионов вызовов. Уникальных аргументов около сорока: разрыв показывает, что кэшировать нужно состояние, а не пытаться ускорять один вызов.
Проблема не в рекурсии как таковой, а в повторном решении одних состояний. Сохраним их ответы.
Мемоизация сверху вниз
Этот раздел откроется после предыдущих проверок.
Продолжить: Фибоначчи: цена повторных вычислений →Дерево вызовов содержит повторы, а множество разных аргументов мало. Запомним результат первого вычисления каждого аргумента и будем возвращать его при повторном запросе.
Мемоизация (Memoization) — Кэширование результатов рекурсивных подзадач при первом обращении.
Перед рекурсивным вычислением fib(n) проверяем кэш: если ответ есть, возвращаем его сразу. Так получаем O(n) состояний вместо экспоненциального числа рекурсивных вызовов.
memo = array(n+1, UNSET)
fib(k):
if memo[k] != UNSET: return memo[k]
if k < 2: ans = k
else: ans = fib(k-1) + fib(k-2)
memo[k] = ans
return ansO(n) времени, O(n) памяти на кэш и стек вызовов.
Базовые F(0), F(1) уже в кэше. Сравните первое и повторное обращение к F(3).
→ вычисление / сохранение
Это top-down DP: дерево рекурсивных запросов вычисляет только действительно нужные состояния и больше не пересчитывает их.
Пограничные случаи
Ключ кэша должен полностью описывать подзадачу: одного номера клетки не хватит, если ещё важен оставшийся ресурс. Отсутствующий результат лучше отмечать None/Option или отдельным флагом, поскольку ноль может быть корректным ответом. Кэш не устраняет глубину рекурсии: цепочка из 200 000 состояний всё ещё опасна для стека Python.
Одни состояния: кэш или порядок вычисления
F(0)=0, F(1)=1. Мемоизация использует O(n) памяти и глубину рекурсии O(n); итеративный вариант — O(1) дополнительной памяти. Оба выполняют O(n) сложений. Для точного Rust-результата ограничиваем n ≤ 93, чтобы F(n) помещалось в u64; Python использует большие целые.
def fib_memo(n):
memo = [None] * (n + 1)
def solve(k):
if memo[k] is not None: return memo[k]
memo[k] = k if k < 2 else solve(k - 1) + solve(k - 2)
return memo[k]
return solve(n)
def fib_iter(n):
if n == 0: return 0
a, b = 0, 1
for _ in range(1, n): a, b = b, a + b
return b
print(fib_memo(10), fib_iter(10))fn fib_memo(n:usize)->u64{
assert!(n<=93);
fn solve(k:usize,memo:&mut[Option<u64>])->u64{if let Some(value)=memo[k]{return value;}let value=if k<2{k as u64}else{solve(k-1,memo)+solve(k-2,memo)};memo[k]=Some(value);value}
solve(n,&mut vec![None;n+1])
}
fn fib_iter(n:usize)->u64{assert!(n<=93);if n==0{return 0;}let(mut a,mut b)=(0,1);for _ in 1..n{(a,b)=(b,a+b);}b}
fn main(){println!("{} {}",fib_memo(10),fib_iter(10));}55 55
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Задача с ресурсом
Если путь зависит от клетки и остатка энергии, ключ состояния должен содержать оба параметра. Кэш только по клетке смешает разные подзадачи и даст неверный ответ даже при быстром выполнении.
Каждое состояние теперь вычисляется один раз. Попробуем задать порядок вычислений явно и уменьшить память.
Табуляция снизу вверх и уменьшение памяти
Этот раздел откроется после предыдущих проверок.
Продолжить: Фибоначчи: цена повторных вычислений →Мемоизация сохраняет форму рекурсивного решения. Но если порядок зависимостей известен заранее, можно вообще отказаться от рекурсивного стека и заполнить ответы от маленьких состояний к большим.
Табуляция (Bottom-up DP) — Вычисление состояний в таком порядке, чтобы все зависимости уже были известны.
Сначала создадим dp[0…n]: dp[0]=0, при n≥1 dp[1]=1. Для i от 2 до n запишем dp[i]=dp[i−1]+dp[i−2]. На n=6 таблица получается [0,1,1,2,3,5,8]. Только после заполнения заметим, что для следующего значения нужны две последние ячейки.
Теперь a=F(4)=3, b=F(5)=5. Сначала new=a+b=8, затем a=b, b=new. Одновременное обновление предотвращает потерю прежнего b.
if n == 0: return 0
a, b = 0, 1
for i = 2..n:
a, b = b, a + b
return bСначала вычисляется новое значение, затем обновляются обе переменные.
| Метод | Время | Память |
|---|---|---|
| Наивная рекурсия | Экспоненциальное | O(n) стек |
| Мемоизация | O(n) | O(n) |
| Табуляция | O(n) | O(n) |
| Две переменные | O(n) | O(1) |
Перед итерацией i переменные a и b должны хранить F(i−2) и F(i−1). Сначала считаем новое значение из старых, затем сдвигаем пару. Память O(1) достаточна для самого числа, но не для выдачи всех значений или восстановления маршрута. Оценка O(n) времени предполагает арифметику фиксированной стоимости; точные большие целые требуют учитывать длину чисел.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Ответ без всей таблицы
Для Climbing Stairs достаточно двух предыдущих количеств. Но если условие требует вывести сами значения для всех ступеней, одно возвращаемое число уже не выражает нужный результат.
Механизм ДП понятен. Дальше научимся получать состояние и формулу из условия задачи, а не из готовой последовательности.
Кенгуру: подсчёт путей
Этот раздел откроется после предыдущих проверок.
Продолжить: Фибоначчи: цена повторных вычислений →Числа Фибоначчи были примером готовой формулы. Теперь выведем рекуррентность из условия задачи: кенгуру прыгает по клеткам на один или два шага. Начнём с точного смысла одного состояния.
Есть ряд клеток. Из каждой можно перепрыгнуть на одну или две клетки; задача — посчитать, сколькими способами можно попасть в последнюю. Каждый путь в i оканчивается прыжком из i−1 или из i−2.
База d[0]=1: один пустой путь уже находится на старте. Отсутствующие индексы дают вклад 0.
Нажмите на клетку, чтобы закрыть или открыть её.
Способов попасть в 7: 21. База d[0] = 1.
Если разрешены прыжки 1…k, формула обобщается до суммы k предыдущих состояний, что даёт прямую реализацию за O(nk). При необходимости скользящая сумма снижает время до O(n).
Что именно считается способом
Пусть d[i] — число способов попасть ровно в i. В нулевой клетке есть один пустой маршрут, поэтому d[0]=1. Последний прыжок из i−1 и последний прыжок из i−2 образуют непересекающиеся классы путей — их количества складываются. Для запретной клетки ставим 0. При вычислении по модулю берём остаток после каждого сложения.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Закрытые клетки и число маршрутов
Вариант с запретными клетками меняет базу или обнуляет конкретное состояние, но не требует нового рекурсивного перебора. Для прыжков на 1…k сначала оцените O(nk), затем подумайте о скользящей сумме.
Сложение объединило количества маршрутов. Для оптимизации заменим его минимумом и сохраним лучший переход.
Оптимизация вместо подсчёта
Этот раздел откроется после предыдущих проверок.
Продолжить: Фибоначчи: цена повторных вычислений →Вместо количества путей попросим самый дешёвый маршрут. Граф допустимых прыжков тот же; меняется операция, которая объединяет ответы предшественников.
Если у каждой клетки есть цена c[i], надо найти маршрут с минимальной суммой цен. Вместо суммы количества способов используем минимум стоимости.
d[i] — минимальная стоимость попадания в i при прыжках на 1 или 2.
Оптимальные стоимости: 3, 10, 5, 10, 6, 10
Ответ: 10. Маршрут: 0 → 2 → 4 → 5. Стартовая клетка оплачивается.
Для восстановления ответа храним p[i] — предшествующую клетку, из которой получен оптимум. Начав с конечной клетки, переходим по p до начала и разворачиваем маршрут.
В нашем примере старт находится в клетке 0 и его цена оплачивается: d[0]=c[0], d[1]=c[1]+d[0]. Это отличается от задач, разрешающих начать с клетки 1 бесплатно. Храним p[i], выбранного предшественника, затем идём от финиша до старта и разворачиваем путь. При равенстве стоимостей можно выбрать любого оптимального предка, если условие не требует особого порядка.
Минимальная стоимость и маршрут
Нужно дойти из клетки 0 в последнюю прыжками на 1 или 2. Цена старта включена. На каждой клетке сохраняем оптимального предка; равенство разрешаем в пользу меньшего индекса. Время O(n), память O(n) для восстановления. Пустой вход возвращает ноль и пустой маршрут. Для Rust предполагаем n ≤ 2·10⁵ и |c[i]| ≤ 10⁹.
def mincost_path(cost):
if not cost: return 0, []
dp, parent = cost.copy(), [-1] * len(cost)
for i in range(1, len(cost)):
p = i - 2 if i >= 2 and dp[i-2] <= dp[i-1] else i - 1
dp[i], parent[i] = cost[i] + dp[p], p
path, i = [], len(cost) - 1
while i != -1:
path.append(i)
i = parent[i]
return dp[-1], path[::-1]
value, path = mincost_path([3, 7, 2, 5, 1, 4])
print(value)
print(*path)fn mincost_path(cost:&[i64])->(i64,Vec<usize>){
if cost.is_empty(){return(0,vec![]);}
let mut dp=cost.to_vec();let mut parent=vec![None;cost.len()];
for i in 1..cost.len(){let p=if i>=2&&dp[i-2]<=dp[i-1]{i-2}else{i-1};dp[i]=cost[i]+dp[p];parent[i]=Some(p);}
let mut path=Vec::new();let mut current=Some(cost.len()-1);
while let Some(i)=current{path.push(i);current=parent[i];}path.reverse();(dp[cost.len()-1],path)
}
fn main(){let(value,path)=mincost_path(&[3,7,2,5,1,4]);println!("{}",value);println!("{}",path.iter().map(|x|x.to_string()).collect::<Vec<_>>().join(" "));}10 0 2 4 5
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Не перепутайте оплату старта
В LeetCode 746 можно начать с нулевой или первой ступени; пример лекции обязан стартовать в нуле и оплачивает его. Перед переносом рекуррентности выпишите базу конкретной задачи.
Оптимальный маршрут восстановлен. Теперь отделим сам граф переходов от направления, в котором выполняются обновления.
Динамика назад и вперёд
Этот раздел откроется после предыдущих проверок.
Продолжить: Фибоначчи: цена повторных вычислений →d = array(n+1, 0)
d[0] = 1
for i = 1..n:
d[i] = d[i-1]
if i >= 2: d[i] += d[i-2]Смысл d[i] тот же: число способов дойти ровно до i. Заполнение слева направо.
До сих пор мы собирали ответ клетки из уже посчитанных предков. Можно взглянуть на те же рёбра в обратную сторону: из готового состояния раздать вклад будущим. Смысл состояния и зависимости при этом не меняются.
Переход состояния (Transition) — Правило, вычисляющее вклад других состояний в текущее или будущие состояния.
Просмотр назад (gather): для текущего i перебрать все возможные состояния-предшественники и собрать ответ. Просмотр вперёд (scatter): для уже посчитанного i обновить все достижимые следующие состояния.
В обоих случаях нужен корректный порядок обработки, обеспечивающий отсутствие зависимостей от ещё не вычисленных ответов. Для направленного ациклического графа это топологический порядок.
d = array(n, 0)
d[0] = 1
for i = 0..n-1:
if i+1 < n: d[i+1] += d[i]
if i+2 < n: d[i+2] += d[i]Каждый исходящий переход переносит число путей из i в получателя.
В переходе вперёд нельзя обновить получателя и сразу обработать его раньше всех остальных предков: иначе часть вклада потеряется. На линии и в таблице естественный порядок очевиден; в общем ациклическом графе нужен топологический. При циклах однократного обхода может быть недостаточно: это уже другая постановка.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Перенос способов по рёбрам
В динамике вперёд каждое ребро обновляет ответ получателя. Если состояния обрабатываются в топологическом порядке, все нужные вклады приходят до обработки получателя.
Корректный порядок оказался частью доказательства. Перенесём этот принцип с линии на таблицу.
Динамика на таблице
Этот раздел откроется после предыдущих проверок.
Продолжить: Фибоначчи: цена повторных вычислений →Добавим вторую координату. Робот идёт по таблице только вправо и вниз, а некоторые клетки закрыты. Номер состояния теперь пара (строка, столбец), но порядок рассуждения остаётся прежним.
В двумерной решётке, где можно ходить только вправо и вниз, число способов попасть в клетку равно сумме способов попасть в её верхнего и левого соседей.
Начните с сетки 3×3 без препятствий: [i][j] — строка i и столбец j. Верхняя и левая границы получают по одному пути. Три ячейки скрыты: сначала вычислите их сами. Нажатие переключает препятствие; формула остаётся прежней.
Маршрутов до правого нижнего угла: 6.
Для незаблокированной клетки. Начало d[0][0]=1; за пределами поля значения считаются нулевыми.
Обход строками сверху вниз и столбцами слева направо гарантирует, что оба предшественника уже известны. Время и память — O(rows · cols).
Как улучшить память
Закрытая стартовая клетка даёт ноль маршрутов, открытая таблица 1×1 — один. В строковом обходе вектор dp[j] до обновления хранит ответ сверху, а dp[j−1] — уже обновлённый ответ слева. Так память сокращается до O(cols), но восстановление конкретного пути потребует дополнительных данных или повторного вычисления.
Маршруты в таблице с памятью на одну строку
0 означает открытую клетку, 1 — препятствие. Идём вправо и вниз, ответ считаем по модулю 1 000 000 007. До обновления dp[j] содержит ответ сверху, dp[j−1] — ответ слева. Время O(rows·cols), память O(cols). Пустая таблица даёт ноль; закрытый старт тоже даёт ноль.
def grid_paths(blocked):
if not blocked or not blocked[0]: return 0
cols = len(blocked[0])
dp = [0] * cols
dp[0] = 1
for row in blocked:
for j in range(cols):
if row[j]: dp[j] = 0
elif j: dp[j] = (dp[j] + dp[j-1]) % 1_000_000_007
return dp[-1]
print(grid_paths([[0,0,0],[0,1,0],[0,0,0]]))fn grid_paths(blocked:&[Vec<bool>])->u64{
if blocked.is_empty()||blocked[0].is_empty(){return 0;}
let cols=blocked[0].len();let mut dp=vec![0;cols];dp[0]=1;
for row in blocked{for j in 0..cols{if row[j]{dp[j]=0;}else if j>0{dp[j]=(dp[j]+dp[j-1])%1_000_000_007;}}}dp[cols-1]
}
fn main(){println!("{}",grid_paths(&[vec![false,false,false],vec![false,true,false],vec![false,false,false]]));}2
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Количество или минимальная сумма
LeetCode 62 считает маршруты, 64 оптимизирует стоимость. Смысл состояния и правило объединения различны: сложение количеств нельзя заменить минимумом без изменения базы и недостижимости.
Основной инструмент готов: состояние, база, переход и порядок заполнения. Теперь создайте собственное ДП в практике. Для очень далёких одинаковых шагов ниже доступно дополнительное исследование матриц.
Справочник терминов
Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.
Общий справочник всех лекций ↗
Найдено: 13 из 13
Ограничения и выбор алгоритма
Этот раздел откроется после предыдущих проверок.
Продолжить: Фибоначчи: цена повторных вычислений →Сначала подставьте максимальные размеры из условия в оценку времени и памяти. Оценка числа действий — ориентир: константы, язык и ввод проверяются отдельно.
Состояний 1000000; с двумя предками — до 2000000 сложений. Плотная таблица i64: 8.00 МБ; одна строка: 0.008 МБ. Оценки для массива чисел фиксированной ширины; объекты и списки Python требуют дополнительной памяти.
Число состояний × работа переходов
Для линии с n = 200 000 и двумя предками получается O(n). Для n = 200 000 и k = 200 000 полный перебор k прыжков на каждом состоянии даст до 4·10¹⁰ переходов. Маленькое тело цикла не компенсирует такое число повторений.
Таблица и память
Для 1000×1000 клеток нужен миллион состояний. Один массив i64 занимает примерно 8 МБ, четыре таких массива — 32 МБ без дополнительного хранения поля. В Python список списков с отдельными int имеет гораздо больший расход. Если нужны только значения соседних строк, переходите к O(cols) памяти.
В задаче Working out нужны ответы из четырёх направлений, поэтому одной строки на всё решение недостаточно. Сначала выпишите, какие промежуточные значения понадобятся на этапе соединения маршрутов.
Разрядность, рекурсия и большое n
Число путей растёт быстро. Если условие требует модуль, применяйте его к каждому обновлению; если точный ответ — оцените длину больших целых. Сумма цен до 10⁹ на пути из 200 000 клеток требует i64. Нельзя использовать 0 как недостижимость, когда ответ 0 допустим.
Top-down с глубиной 200 000 опасен для Python. Bottom-up убирает этот риск. Для n до 10¹⁸ фиксированная матрица размера s требует O(s³ log n), но только при действительно одинаковом линейном переходе. Проверьте нулевой шаг, закрытый старт, одну клетку, равные оптимумы и отсутствие маршрута.
Практика: 3 уровня
Этот раздел откроется после предыдущих проверок.
Продолжить: Фибоначчи: цена повторных вычислений →Постройте своё DP: состояние → база → переход → код
Кенгуру прыгает на 1 или 3 клетки, некоторые клетки закрыты. Нужно посчитать пути ровно из 0 в n. Сначала сформулируйте подзадачу. Следующий шаг откроется после правильного ответа на предыдущий; решения сохраняются вместе с прогрессом курса.
Шаг 2 откроется после предыдущего решения.
Шаг 3 откроется после предыдущего решения.
Шаг 4 откроется после предыдущего решения.
Основное применение
После конструктора решите Climbing Stairs и Min Cost Climbing Stairs: одинаковые направления прыжков дают разные смысл состояния и базу. Затем перенесите вывод на таблицу. Сложное испытание объединяет несколько направлений и остаётся дополнительным.
Примените разобранные приёмы самостоятельно. Для каждой задачи сначала сформулируйте требования к ответу и предложите простое решение. Затем подставьте максимальные ограничения, оцените время и память и решите, какое улучшение требуется. Подсказки с идеей и оценкой раскрываются отдельно.
Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.
Лёгкие
Три задачи для закрепления основных приёмов.
- LeetCode · 509
Fibonacci Number
Задать базу и убрать повторные вычисления.
Ограничения. 0 ≤ n ≤ 30; F(0)=0, F(1)=1.
Идея и оценка сложности
Используйте то же соглашение, что и в лекции: F(0)=0, F(1)=1. Две переменные дают O(n) времени и O(1) памяти, memo — O(n) памяти.
- LeetCode · 70
Climbing Stairs
Получить сумму способов по последнему шагу.
Ограничения. От 1 до 45 ступеней.
Идея и оценка сложности
Один или два последних шага задают два непересекающихся класса. База для количества путей: один пустой путь в 0, а не F(0)=0. O(n) времени, O(1) памяти.
- LeetCode · 746
Min Cost Climbing Stairs
Вывести базу из возможности выбрать старт.
Ограничения. От 2 до 1000 цен, каждое значение до 999.
Идея и оценка сложности
Можно начать с 0 или 1, а финиш находится за последней ступенью. Сформулируйте, когда платится цена, и не копируйте базу примера лекции без изменений. O(n), O(1) памяти без восстановления.
Средние
Две задачи на сочетание идей и анализ ограничений.
- LeetCode · 62
Unique Paths
Упорядочить двумерные состояния.
Ограничения. Размеры от 1 до 100; ответ в тестах не больше 2·10⁹.
Идея и оценка сложности
Ответ в клетке — сумма сверху и слева. Начало имеет один путь, вне поля — ноль. Память можно сократить до одной строки. O(rows·cols) времени, O(cols) памяти.
- LeetCode · 64
Minimum Path Sum
Заменить подсчёт путей оптимизацией стоимости.
Ограничения. До 200×200 клеток, цены от 0 до 200.
Идея и оценка сложности
Прибавляйте цену клетки к минимуму достижимых предшественников. Ноль — допустимая стоимость, поэтому не обозначайте им отсутствие пути. O(rows·cols); при восстановлении сохраните предков.
Тяжёлая
Одна задача для переноса идеи на новую постановку.
- Codeforces · 429B
Working out
Сочетать четыре направления ДП и локальные варианты встречи.
Ограничения. 3 ≤ n,m ≤ 1000; цены до 10⁵.
Идея и оценка сложности
Посчитайте максимальные суммы из четырёх углов в разрешённых направлениях. Для каждой внутренней клетки проверьте два способа провести маршруты вокруг единственной встречи; сама клетка встречи не приносит цену. Итого O(nm) времени и четыре таблицы памяти. Уровень повышен в курсе из-за доказательства соединения маршрутов.
Дальше применяйте тот же конструктор к рюкзаку и подпоследовательностям: начните с вопроса, какие параметры полностью определяют подзадачу. Матричное ускорение выше пригодится отдельно для постоянных линейных переходов.
Дополнительные материалыДля углублённого разбора · не влияют на прохождение
Линейные переходы и матричное ускорение
Линейное ДП хорошо, когда число шагов умеренное. При n до 10¹⁸ последовательно пройти все состояния невозможно. Если переход постоянно задаётся одной небольшой матрицей, много одинаковых шагов можно объединить.
Если переход имеет вид линейного преобразования фиксированного вектора состояния, его можно представить матрицей M и переходить сразу через много шагов как Mᵏ.
Степень матрицы вычисляется двоичным возведением за O(log n) умножений.
n в двоичном виде: 10100. Все числа по модулю 1 000 000 007.
| 10946 | 6765 |
| 6765 | 4181 |
F(20) = 6765. Матричных умножений: 6; последовательных шагов было бы 20.
Основание: [1, 1; 1, 0].
Накопитель: [1, 0; 0, 1].
Для последовательности Фибоначчи достаточно матрицы 2×2. При постоянном числе состояний это уменьшает число шагов с O(n) до O(log n) при принятой модели стоимости арифметики.
Цена больших чисел
Для Фибоначчи удобно Vₖ=(F(k+1),F(k)), M=[[1,1],[1,0]], V₀=(1,0). Тогда Vₙ=MⁿV₀. Умножение матриц размера s стоит O(s³), весь алгоритм — O(s³ log n), память O(s²). В примере считаем по модулю, чтобы не скрывать рост чисел. Произвольный нелинейный переход или меняющиеся по шагам условия не ускоряются одной фиксированной матрицей автоматически.
Матричное ускорение одинакового перехода
Фиксированная матрица 2×2 поднимается в степень двоичным методом. Получаем F(n) по модулю 1 000 000 007 за O(log(n+1)) умножений матриц и O(1) дополнительной памяти. При n ≤ 10¹⁸ степень помещается в u64; произведения двух остатков и их сумма тоже безопасны для u64.
MOD = 1_000_000_007
def multiply(a, b):
return [[sum(a[i][k] * b[k][j] for k in range(2)) % MOD
for j in range(2)] for i in range(2)]
def matrix_fib(n):
answer, base = [[1,0],[0,1]], [[1,1],[1,0]]
while n:
if n & 1: answer = multiply(answer, base)
base = multiply(base, base)
n >>= 1
return answer[1][0]
print(matrix_fib(50))const MOD:u64=1_000_000_007;
type Matrix=[[u64;2];2];
fn multiply(a:Matrix,b:Matrix)->Matrix{let mut result=[[0;2];2];for i in 0..2{for j in 0..2{for k in 0..2{result[i][j]=(result[i][j]+a[i][k]*b[k][j])%MOD;}}}result}
fn matrix_fib(mut n:u64)->u64{let mut answer=[[1,0],[0,1]];let mut base=[[1,1],[1,0]];while n>0{if n&1==1{answer=multiply(answer,base);}base=multiply(base,base);n>>=1;}answer[1][0]}
fn main(){println!("{}",matrix_fib(50));}586268941
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Очень далёкий шаг
Когда n достигает 10¹⁸, линейный проход невозможен даже при O(1) памяти. Для постоянного линейного перехода двоичная степень маленькой матрицы сокращает число этапов примерно до 60.
Теперь пройдите весь путь решения: смысл состояния, база, переход, порядок, оценка и восстановление при необходимости. Практика ниже проверит каждый из этих шагов.