Шелдон из Algo Monster: «Освойте паттерны динамического программирования вместо зубрежки»

freeCodeCamp.org 119 тыс. 1 ч 41 мин 6 мин 21.01.2026
Главное

Динамическое программирование (ДП) часто считается одной из самых сложных тем при подготовке к техническим интервью, однако эксперты утверждают, что ключ к успеху кроется не в зазубривании сотен задач, а в освоении нескольких фундаментальных паттернов. Шелдон, бывший сотрудник Google с десятилетним стажем и наставник платформы Algo Monster, в детальном курсе разбирает, как превратить абстрактную логику оптимизации в понятные визуальные модели.

🪜 Основы: Задача о лестнице и магия рекурсии 1:03

Путь к пониманию ДП начинается с классической задачи о лестнице: нужно определить количество способов добраться до $n$-й ступеньки, если за один раз можно сделать шаг на одну или две ступени . На примере трех ступенек Шелдон показывает, что существует три пути: три одиночных шага, шаг плюс прыжок, или прыжок плюс шаг . Однако с ростом числа ступеней (например, до десяти) ручной подсчет становится невозможным.

Логика решения строится на принципе расширения существующих путей:

Следовательно, количество способов достичь ступени $n$ — это сумма способов для ступеней $(n-1)$ и $(n-2)$ . Это правило реализуется через рекурсию, где функция вызывает саму себя для подзадач. Однако Шелдон предупреждает: чистая рекурсия крайне неэффективна. Для $n=40$ вычисления займут очень много времени из-за гигантского дерева рекурсии, где одни и те же значения (например, для 3-й ступени) пересчитываются тысячи раз . Временная сложность такого алгоритма составляет $O(2^n)$, что при $n=30$ дает около миллиарда лишних операций .

🧠 Мемоизация против Табуляции 7:36

Для решения проблемы избыточных вычислений Шелдон предлагает использовать мемоизацию — технику сохранения результатов в кэше (обычно это хэш-таблица или массив) .

Процесс оптимизации выглядит так:

  1. Перед вычислением проверяем, есть ли результат в хэш-таблице .
  2. Если есть — возвращаем его мгновенно.
  3. Если нет — вычисляем, сохраняем и только потом возвращаем .

Мемоизация превращает экспоненциальную сложность в линейную $O(n)$, так как каждый шаг вычисляется ровно один раз . Шелдон называет этот подход «рекурсией с памятью» или нисходящим проектированием (top-down) .

Альтернативный метод — табуляция (bottom-up). Вместо рекурсии используется цикл, который последовательно заполняет массив от базовых случаев к финалу . Шелдон сравнивает эти подходы:

💰 Паттерн «Постоянный переход»: Минимизация затрат 20:22

Усложнение задачи: теперь каждая ступенька имеет свою стоимость (массив cost). Задача — достичь верха лестницы с минимальными затратами . Шелдон объясняет, что логика перехода остается прежней: попасть на ступень $n$ можно только с $(n-1)$ или $(n-2)$. Разница лишь в том, что мы выбираем не сумму способов, а min() от стоимостей предыдущих шагов, прибавляя стоимость текущей ступени .

Шелдон выделяет признаки паттерна «Постоянный переход»:

Этот же паттерн применяется в задаче «House Robber» (Грабитель домов), где нельзя грабить два соседних дома . Формула выбора: либо мы грабим текущий дом и прибавляем добычу к сумме через один дом назад, либо пропускаем текущий и берем максимум для предыдущего дома .

🗺️ Двумерное ДП: Сетки и пути 33:48

Когда движение возможно в нескольких направлениях (например, в сетке $M \times N$), Шелдон вводит понятие двумерного динамического программирования. В задаче «Unique Paths» робот движется из верхнего левого угла в нижний правый, имея право ходить только вправо или вниз .

Основные правила для сеток по мнению Шелдона:

  1. Базовые случаи: Для всех ячеек первой строки и первого столбца существует только 1 путь (движение только в одном направлении вдоль края) .
  2. Формула перехода: Количество путей в ячейку $(i, j)$ равно сумме путей из ячейки сверху и ячейки слева .
  3. Оптимизация: Для вычислений достаточно хранить только одну строку (текущую), обновляя её значения на основе предыдущих . Пространственная сложность сокращается до $O(min(M, N))$ .

Если на сетке появляются препятствия, в ячейки с ними просто записывается 0, так как путей через них не существует .

🧵 Две последовательности: Сравнение строк 45:25

Паттерн «Две последовательности» используется для сравнения двух строк или массивов. Классический пример — поиск самой длинной общей подпоследовательности (LCS) . Подпоследовательность отличается от подстроки тем, что символы не обязательно должны идти подряд, но их порядок должен сохраняться .

Шелдон описывает построение таблицы, где строки — это символы первого слова, а столбцы — второго :

Этот же принцип применим к задаче об «Расстоянии редактирования» (Edit Distance), где нужно найти минимальное количество вставок, удалений или замен для превращения одного слова в другое .

🔄 Интервальное ДП и палиндромы 1:01:36

Интервальное ДП применяется, когда результат для большого отрезка данных зависит от результатов его внутренних частей . В задаче о самой длинной палиндромной подпоследовательности Шелдон использует логику краев:

Главная особенность здесь — порядок заполнения таблицы. Мы идем не по строкам, а по длине интервала: сначала считаем все отрезки длиной 1, затем 2, и так далее .

📈 Непостоянный переход: LIS и разделение массива 1:15:00

В некоторых задачах текущее состояние зависит не от фиксированного числа соседей, а от всех предыдущих элементов. Яркий пример — поиск самой длинной возрастающей подпоследовательности (LIS) . Для каждого элемента массива нам приходится сканировать все элементы слева от него . Если текущий элемент больше какого-то предыдущего, мы можем расширить ту подпоследовательность. Шелдон отмечает, что из-за вложенного цикла временная сложность здесь составляет $O(n^2)$, а оптимизация памяти обычно невозможна, так как нам нужны все прошлые данные .

🎒 Задачи типа «Кнапсак» (Рюкзак) 1:27:10

Последний разобранный паттерн касается возможности набора определенной суммы из чисел массива . В задаче о разделении множества на две равные части (Subset Sum) мы создаем массив булевых значений, где индекс — это возможная сумма .

Критически важный технический нюанс от Шелдона: при обновлении массива сумм нужно двигаться справа налево (от больших сумм к меньшим) . Это гарантирует, что каждое число из исходного набора будет использовано только один раз. Если двигаться слева направо, мы рискуем прибавить одно и то же число к сумме несколько раз, что допустимо только в задачах с неограниченным количеством предметов (например, Coin Change) .

💬 Цитаты

«Вам не нужно запоминать сотни задач, чтобы преуспеть в динамическом программировании. Вам просто нужно освоить небольшой набор паттернов.»

«Мемоизация — это, по сути, рекурсия с памятью.»

👥 Спикер
🔗 Упомянутые сайты и проекты
📖 Термины
Мемоизация
Техника оптимизации, при которой результаты вызовов функций сохраняются для предотвращения повторных вычислений.
Табуляция
Подход в ДП, при котором решение строится итеративно снизу вверх, заполняя таблицу результатов.
Сложность O(2^n)
Экспоненциальная временная сложность, при которой время выполнения удваивается с каждым новым элементом входных данных.
Палиндром
Последовательность символов, которая читается одинаково в обоих направлениях.
LCS
Longest Common Subsequence — самая длинная общая подпоследовательность двух строк.
📊 Цифры
⚖️ Другая сторона
Образование Динамическое программирование Memoization Tabulation Algo Monster freeCodeCamp