🔴 Сложный ⏱️ 25 минут

Динамическое программирование: от рекурсии к оптимальности

Динамическое программирование: от рекурсии к оптимальности

🎯 Зачем это нужно?

Представь: ты разрабатываешь навигатор для Яндекс.Карт 🗺️. Нужно найти кратчайший путь от дома до школы через весь город. Наивный подход: проверить ВСЕ возможные пути. Но в городе миллионы маршрутов - компьютер зависнет навсегда!

Динамическое программирование решает такие задачи за секунды. Вот где оно работает прямо сейчас:

  • 📱 YouTube рекомендации - какие видео показать следующими
  • 🎮 Игровой ИИ - как AlphaGo победил чемпиона мира в го
  • 💰 Криптовалюты - оптимальная стратегия торговли
  • 🧬 Биоинформатика - поиск похожих участков ДНК

📚 История вопроса

В 1940-х математик Ричард Беллман работал над задачами военного планирования. Ему нужно было оптимизировать сложные многошаговые решения. Термин “динамическое программирование” он выбрал специально, чтобы министр обороны не понял, чем он занимается! 😄

Беллман понял: многие сложные задачи можно разбить на простые подзадачи, решить их один раз и использовать результаты многократно.

💡 Интуиция

Основная идея: Не пересчитывай одно и то же дважды!

Допустим, ты идёшь в спортзал и хочешь набрать мышечную массу максимально эффективно 💪. У тебя есть разные упражнения с разной “стоимостью” (время) и “пользой” (результат).

Наивный подход: перебрать все возможные комбинации тренировок. Но многие подкомбинации повторяются! Например, “жим + приседания” встречается в тысячах вариантов.

DP говорит: “Один раз посчитай оптимальную программу для N упражнений и бюджета времени T. Сохрани результат. Когда понадобится - просто посмотри в таблицу!”

[МЕДИА: image_01] Описание: Схема сравнения наивной рекурсии и динамического программирования Промпт: “educational diagram comparing naive recursion tree with memoized dynamic programming, showing repeated subproblems being cached, modern infographic style, blue and green colors, arrows showing optimization”

📐 Формальное определение

Динамическое программирование применимо к задачам с двумя свойствами:

1️⃣ Оптимальная подструктура: оптимальное решение содержит оптимальные решения подзадач

2️⃣ Перекрывающиеся подзадачи: одни и те же подзадачи возникают многократно

Принцип оптимальности Беллмана: Если путь A→C→B оптимален, то путь A→C тоже оптимален.

Уравнение Беллмана: V(s) = max{R(s,a) + γV(s′)} для всех действий a

где:

  • V(s) - оптимальная ценность состояния s
  • R(s,a) - награда за действие a в состоянии s
  • γ - коэффициент дисконтирования
  • s′ - следующее состояние

🔍 Примеры с разбором

Пример 1: Числа Фибоначчи

Задача: Найти F(50), где F(n) = F(n-1) + F(n-2)

Наивная рекурсия:

def fib_naive(n):
    if n <= 1: return n
    return fib_naive(n-1) + fib_naive(n-2)
Сложность: O(2ⁿ) - экспоненциальная! F(50) считается часами.
Мемоизация (top-down DP):
code
Python
memo = {}
def fib_memo(n):
    if n in memo: return memo[n]
    if n <= 1: result = n
    else: result = fib_memo(n-1) + fib_memo(n-2)
    memo[n] = result
    return result
Табулирование (bottom-up DP):
code
Python
def fib_dp(n):
    dp = [0] * (n+1)
    dp[1] = 1
    for i in range(2, n+1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]
Сложность: O(n) - линейная! F(50) за миллисекунды.
[МЕДИА: image_02]
Описание: Визуализация дерева вызовов для наивной рекурсии vs мемоизированной версии
Промпт: "side-by-side comparison of fibonacci recursion trees, left showing exponential naive approach with many repeated calls, right showing memoized version with cached results, educational programming visualization"
Пример 2: Задача о рюкзаке
Контекст: Ты собираешься в поход. Рюкзак вмещает 10 кг. Есть предметы с весом и ценностью. Что взять?
Предметы: {(вес=2, польза=3), (вес=3, польза=4), (вес=4, польза=5), (вес=5, польза=6)}
Рекурентное соотношение:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i]] + value[i])
"Для i-го предмета и веса w: либо не берём (предыдущий результат), либо берём (если помещается) и добавляем к оптимуму оставшегося места"
code
Python
def knapsack(weights, values, W):
    n = len(weights)
    dp = [[0 for _ in range(W+1)] for _ in range(n+1)]
    
    for i in range(1, n+1):
        for w in range(1, W+1):
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i-1][w], 
                              dp[i-1][w-weights[i-1]] + values[i-1])
            else:
                dp[i][w] = dp[i-1][w]
    
    return dp[n][W]
Пример 3: Редакционное расстояние (Левенштейна)
Задача: Превратить слово "кот" в "код" минимальным числом операций (замена, вставка, удаление).
Это основа проверки орфографии в Google Docs! 📝
dp[i][j] = минимальное число операций для превращения первых i символов первого слова в первые j символов второго
code
Python
def edit_distance(word1, word2):
    m, n = len(word1), len(word2)
    dp = [[0] * (n+1) for _ in range(m+1)]
    
    # Инициализация базовых случаев
    for i in range(m+1): dp[i][0] = i
    for j in range(n+1): dp[0][j] = j
    
    for i in range(1, m+1):
        for j in range(1, n+1):
            if word1[i-1] == word2[j-1]:
                dp[i][j] = dp[i-1][j-1]  # Символы совпадают
            else:
                dp[i][j] = 1 + min(dp[i-1][j],    # Удаление
                                  dp[i][j-1],     # Вставка
                                  dp[i-1][j-1])   # Замена
    
    return dp[m][n]
🎮 Практика
Базовый уровень 🟢
Задание 1: Лестница из n ступенек. За раз можно подняться на 1 или 2 ступеньки. Сколько способов подняться на вершину?
Задание 2: Есть монеты номиналами [1, 3, 4]. Найди минимальное количество монет для суммы 6.
Задание 3: Максимальная сумма элементов массива, если нельзя брать два соседних элемента. Массив: [2, 1, 4, 9].
Задание 4: Сколько способов разложить число n в сумму 1, 3 и 4? Для n = 5.
Продвинутый уровень 🟡
Задание 5: Самая длинная возрастающая подпоследовательность в массиве [10, 9, 2, 5, 3, 7, 101, 18].
Задание 6: Палиндромное разбиение: минимальное число разрезов строки "aab", чтобы все части были палиндромами.
Задание 7: Максимальная прибыль от покупки-продажи акций. Цены: [7,1,5,3,6,4]. Можно совершить максимум 2 операции.
Задание 8: Подсчитать количество уникальных путей в сетке m×n из левого верхнего в правый нижний угол (можно идти только вправо или вниз).
Челлендж 🔴
Задание 9: Задача о разрезании стержня: стержень длины n, массив цен для каждой длины. Найти максимальную выручку.
Задание 10: Regex matching: строка s соответствует шаблону p с поддержкой '.' (любой символ) и '*' (ноль или более предыдущих символов).
Задание 11: Burst Balloons: есть массив чисел (шарики). При "лопании" шарика i получаешь nums[i-1] × nums[i] × nums[i+1] очков. Найти максимальный счёт.
⚠️ Частые ошибки
 Ошибка: Пытаются применить DP везде, даже где он не нужен
 Правильно: Сначала проверить наличие оптимальной подструктуры и перекрывающихся подзадач
💡 Почему: DP эффективен только при повторных вычислениях одинаковых подзадач
 Ошибка: Неправильное определение состояния dp[i]
 Правильно: Состояние должно однозначно характеризовать подзадачу
💡 Почему: Плохо определённое состояние приводит к неверному результату
 Ошибка: Забывают инициализировать базовые случаи
 Правильно: Всегда явно определять dp[0], dp[1] и т.д.
💡 Почему: Базовые случаи - фундамент для построения решения
 Ошибка: Используют O(n²) памяти там, где достаточно O(n)
 Правильно: Анализировать зависимости и оптимизировать пространство
💡 Почему: Экономия памяти критична для больших задач
 Ошибка: Путают top-down (мемоизация) и bottom-up (табулирование)
 Правильно: Top-down идёт от ответа к базе, bottom-up - наоборот
💡 Почему: Разные подходы имеют разную сложность и применимость
🎓 Главное запомнить
 Принцип: Решаем каждую подзадачу только один раз и сохраняем результат
 Условия применимости: Оптимальная подструктура + перекрывающиеся подзадачи
 Два подхода: Мемоизация (сверху вниз) и табулирование (снизу вверх)
 Временная сложность: Обычно O(количество состояний × время перехода)
🔗 Связь с другими темами
Откуда пришли: Рекурсия и математическая индукция (урок 269) заложили основы для понимания подзадач и их композиции.
Куда ведёт:
Жадные алгоритмы - альтернативный подход к оптимизации
Теория игр - где каждый ход влияет на будущие возможности
Машинное обучение - reinforcement learning использует уравнения Беллмана
Алгоритмы на графах - кратчайшие пути, потоки
Практическое применение:
В олимпиадном программировании DP встречается в 30-40% задач. В промышленности - основа для рекомендательных систем, планирования ресурсов, финансового моделирования.

Понял тему? Закрепи в боте! 🚀

Попрактикуйся на задачах и получи персональные рекомендации от AI

💪 Начать тренировку
💬 Есть вопрос? Спроси бота!