Разработка и оптимизация ИИС

Динамическое программирование

Разработка и оптимизация ИИС

План лекции

1. Введение в динамическое программирование

2. Принцип оптимальности Беллмана

3. Оптимальная подструктура и перекрывающиеся подзадачи

4. Сверху вниз (мемоизация) и снизу вверх (табуляция)

5. Задача о кратчайшем пути

6. Задача о рюкзаке

7. Программная реализация

Динамическое программирование
Разработка и оптимизация ИИС

1. Введение в динамическое программирование

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

Основатель: Ричард Беллман (1957)

Ключевая идея: если решение каждой подзадачи оптимально, то и решение всей задачи оптимально.

Динамическое программирование
Разработка и оптимизация ИИС

Пример

Найти кратчайший маршрут из AA в DD через промежуточные города:

Вместо перебора всех маршрутов — решаем подзадачи попарно.

Динамическое программирование
Разработка и оптимизация ИИС

2. Принцип оптимальности Беллмана

Принцип оптимальности (Беллман, 1957):

Каково бы ни было начальное состояние и начальное решение, последующие решения должны составлять оптимальное поведение относительно состояния, полученного после первого решения.

Формально: если x=(x1,x2,,xn)\vec{x}^* = (x_1^*, x_2^*, \ldots, x_n^*) — оптимальное решение, то для любого kk «хвост» (xk,,xn)(x_k^*, \ldots, x_n^*) оптимален для подзадачи, начинающейся с состояния после xk1x_{k-1}^*.

Динамическое программирование
Разработка и оптимизация ИИС

Два ключевых свойства

1. Оптимальная подструктура:

  • Оптимальное решение содержит оптимальные решения подзадач
  • Пример: кратчайший путь ADA \to D содержит кратчайшие пути для всех промежуточных точек

2. Перекрывающиеся подзадачи:

  • При разбиении одни и те же подзадачи решаются многократно
  • Пример: числа Фибоначчи — F(5)F(5) требует F(3)F(3) дважды
Динамическое программирование
Разработка и оптимизация ИИС

ДП vs другие подходы

Метод Сложность Гарантия оптимума Когда применять
Полный перебор O(kn)O(k^n) Да Малые nn
Жадный алгоритм O(nlogn)O(n \log n) Нет (обычно) Специальная структура
Разделяй и властвуй Зависит Да Непересекающиеся подзадачи
Динамическое программирование Полиномиальная Да Оптимальная подструктура + перекрытие
Динамическое программирование
Разработка и оптимизация ИИС

Перекрывающиеся подзадачи: числа Фибоначчи

F(n)=F(n1)+F(n2),F(0)=0,F(1)=1F(n) = F(n-1) + F(n-2), \quad F(0) = 0, \quad F(1) = 1

Дерево рекурсии для F(5)F(5):

Дублирующиеся вычисления: F(3)F(3) вычисляется 2 раза, F(2)F(2) — 3 раза.

Наивная рекурсия: O(2n)O(2^n). С мемоизацией: O(n)O(n).

Динамическое программирование
Разработка и оптимизация ИИС

3. Подходы к реализации ДП

Сверху вниз (мемоизация):

  • Рекурсия с кэшированием результатов
  • Вычисляем только нужные подзадачи
  • Риск переполнения стека

Снизу вверх (табуляция):

  • Итеративное заполнение таблицы
  • Порядок: от малых подзадач к большим
  • Обычно эффективнее по памяти
Динамическое программирование
Разработка и оптимизация ИИС

Пример: числа Фибоначчи — мемоизация

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(50))  # 12586269025

Характеристики:

  • Время: O(n)O(n) — каждая подзадача решается один раз
  • Память: O(n)O(n) — кэш + стек вызовов O(n)O(n)
Динамическое программирование
Разработка и оптимизация ИИС

Пример: числа Фибоначчи — табуляция

def fib_tab(n):
    if n <= 1:
        return 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]

print(fib_tab(50))  # 12586269025

Характеристики:

  • Время: O(n)O(n)
  • Память: O(n)O(n) — можно сократить до O(1)O(1), храня только два последних значения
Динамическое программирование
Разработка и оптимизация ИИС

Оптимизация памяти

def fib_opt(n):
    if n <= 1:
        return n
    prev, curr = 0, 1
    for _ in range(2, n + 1):
        prev, curr = curr, prev + curr
    return curr

print(fib_opt(50))  # 12586269025

Характеристики:

  • Время: O(n)O(n)
  • Память: O(1)O(1) — только две переменные

Приём: анализируйте, какие предыдущие состояния нужны для текущего шага, и храните только их.

Динамическое программирование
Разработка и оптимизация ИИС

4. Задача о кратчайшем пути

Постановка: найти путь минимальной стоимости от вершины ss до вершины tt во взвешенном графе.

Варианты:

  • От одного источника ко всем вершинам
  • Между всеми парами вершин
Динамическое программирование
Разработка и оптимизация ИИС

Алгоритм Беллмана-Форда

Идея: последовательно улучшать оценки кратчайших расстояний, проходя по всем рёбрам.

Рекуррентное соотношение (Беллмана):

d(k)[v]=min(d(k1)[v],  min(u,v)E{d(k1)[u]+w(u,v)})d^{(k)}[v] = \min \left( d^{(k-1)}[v],\; \min_{(u,v) \in E} \left\{ d^{(k-1)}[u] + w(u,v) \right\} \right)

d(k)[v]d^{(k)}[v] — кратчайшее расстояние от ss до vv, использующее не более kk рёбер.

Базовый случай: d(0)[s]=0d^{(0)}[s] = 0, d(0)[v]=d^{(0)}[v] = \infty для vsv \neq s.

Динамическое программирование
Разработка и оптимизация ИИС

Алгоритм Беллмана-Форда: шаги

Вход: граф G=(V,E)G = (V, E), веса ww, источник ss

  1. Инициализация: d[s]=0d[s] = 0, d[v]=d[v] = \infty для vsv \neq s
  2. Повторить V1|V| - 1 раз:
    • Для каждого ребра (u,v)E(u, v) \in E:
    • Если d[u]+w(u,v)<d[v]d[u] + w(u,v) < d[v]: d[v]=d[u]+w(u,v)d[v] = d[u] + w(u,v)
  3. Проверка отрицательных циклов:
    • Для каждого ребра (u,v)E(u, v) \in E:
    • Если d[u]+w(u,v)<d[v]d[u] + w(u,v) < d[v] → отрицательный цикл

Сложность: O(VE)O(|V| \cdot |E|)

Динамическое программирование
Разработка и оптимизация ИИС

Пример: алгоритм Беллмана-Форда

Итерация d(A)d(A) d(B)d(B) d(C)d(C) d(D)d(D) d(E)d(E) d(F)d(F)
0 0 \infty \infty \infty \infty \infty
1 0 2 6 \infty \infty \infty
2 0 2 4 6 3 \infty
3 0 2 4 5 3 8
4 0 2 4 5 3 8
Динамическое программирование
Разработка и оптимизация ИИС

Пример: результаты

Кратчайшие расстояния от AA:

Вершина Расстояние Путь
BB 2 ABA \to B
CC 4 ABECA \to B \to E \to C
DD 5 ABEDA \to B \to E \to D
EE 3 ABEA \to B \to E
FF 8 ABEDFA \to B \to E \to D \to F

Итерация 4: никаких изменений — ранняя остановка.

Динамическое программирование
Разработка и оптимизация ИИС

Алгоритм Флойда-Уоршелла

Идея: кратчайший путь через промежуточные вершины из множества {1,2,,k}\{1, 2, \ldots, k\}.

Рекуррентное соотношение:

dij(k)=min(dij(k1),  dik(k1)+dkj(k1))d_{ij}^{(k)} = \min \left( d_{ij}^{(k-1)},\; d_{ik}^{(k-1)} + d_{kj}^{(k-1)} \right)

  • dij(0)=w(i,j)d_{ij}^{(0)} = w(i,j) — вес ребра (или \infty, если ребра нет)
  • dij(k)d_{ij}^{(k)} — кратчайший путь от ii до jj через вершины {1,,k}\{1, \ldots, k\}
  • dij(n)d_{ij}^{(n)} — кратчайший путь вообще

Сложность: O(V3)O(|V|^3), память: O(V2)O(|V|^2)

Динамическое программирование
Разработка и оптимизация ИИС

Пример: алгоритм Флойда-Уоршелла

   A  B  C
A  0  3  ∞
B  2  0  1
C  ∞  5  0

Итерация k=1k = 1 (через AA):
dCB=min(,  +3)=d_{CB} = \min(\infty, \; \infty + 3) = \infty (нет изменений)

Итерация k=2k = 2 (через BB):
dAC=min(,  3+1)=4d_{AC} = \min(\infty, \; 3 + 1) = 4
dCA=min(,  5+2)=7d_{CA} = \min(\infty, \; 5 + 2) = 7

Итерация k=3k = 3 (через CC):
dBA=min(2,  1+7)=2d_{BA} = \min(2, \; 1 + 7) = 2 (нет изменений)

Результат:

AA BB CC
AA 0 3 4
BB 2 0 1
CC 7 5 0
Динамическое программирование
Разработка и оптимизация ИИС

5. Задача о рюкзаке

Постановка: есть nn предметов с весом wiw_i и стоимостью cic_i. Рюкзак вмещает вес не более WW. Какие предметы положить, чтобы суммарная стоимость была максимальной?

maxi=1ncixiприi=1nwixiW,xi{0,1}\max \sum_{i=1}^{n} c_i x_i \quad \text{при} \quad \sum_{i=1}^{n} w_i x_i \leq W, \quad x_i \in \{0, 1\}

Варианты:

  • 0/1 рюкзак — каждый предмет берётся целиком или не берётся
  • Неограниченный рюкзак — предмет можно брать многократно
Динамическое программирование
Разработка и оптимизация ИИС

Рекуррентное соотношение для задачи о рюкзаке

Пусть dp[i][w]dp[i][w] — максимальная стоимость, используя первые ii предметов при вместимости ww.

dp[i][w]={dp[i1][w]если wi>w (не помещается)max(dp[i1][w],  dp[i1][wwi]+ci)иначеdp[i][w] = \begin{cases} dp[i-1][w] & \text{если } w_i > w \text{ (не помещается)} \\ \max(dp[i-1][w],\; dp[i-1][w - w_i] + c_i) & \text{иначе} \end{cases}

Базовый случай: dp[0][w]=0dp[0][w] = 0 для всех ww.

Ответ: dp[n][W]dp[n][W]

Сложность: O(nW)O(n \cdot W) — псевдополиномиальная

Динамическое программирование
Разработка и оптимизация ИИС

Пример: задача о рюкзаке

Предмет Вес wiw_i Стоимость cic_i
1 2 3
2 3 4
3 4 5
4 5 8

Вместимость рюкзака: W=8W = 8

Динамическое программирование
Разработка и оптимизация ИИС

Пример: заполнение таблицы

dp[i][w]dp[i][w] — максимальная стоимость:

i\wi \backslash w 0 1 2 3 4 5 6 7 8
0 0 0 0 0 0 0 0 0 0
1 (w1=2,c1=3w_1{=}2, c_1{=}3) 0 0 3 3 3 3 3 3 3
2 (w2=3,c2=4w_2{=}3, c_2{=}4) 0 0 3 4 4 7 7 7 7
3 (w3=4,c3=5w_3{=}4, c_3{=}5) 0 0 3 4 5 7 8 9 9
4 (w4=5,c4=8w_4{=}5, c_4{=}8) 0 0 3 4 5 8 8 11 12

Ответ: dp[4][8]=12dp[4][8] = 12

Динамическое программирование
Разработка и оптимизация ИИС

Пример: восстановление решения

Обратный проход от dp[4][8]=12dp[4][8] = 12:

Шаг ii ww dp[i][w]dp[i][w] dp[i1][w]dp[i-1][w] dp[i1][wwi]+cidp[i-1][w-w_i]+c_i Берём?
1 4 8 12 9 7+8=157 + 8 = 15 Нет (121512 \neq 15)
2 3 8 9 7 3+5=83 + 5 = 8 Нет
3 2 8 7 3 3+4=73 + 4 = 7 Да
4 1 5 3 0 0+3=30 + 3 = 3 Да

Оптимальный набор: предметы 1 и 2, вес 2+3=582 + 3 = 5 \leq 8, стоимость 3+4=73 + 4 = 7.

Исправление: повторим — предмет 4 при w=8w = 8: dp[3][3]+8=4+8=12=dp[4][8]dp[3][3] + 8 = 4 + 8 = 12 = dp[4][8]

Оптимальный набор: предметы 2 и 4, вес 3+5=83 + 5 = 8, стоимость 4+8=124 + 8 = 12.

Динамическое программирование
Разработка и оптимизация ИИС

6. Программная реализация: задача о рюкзаке

def knapsack(weights, costs, W):
    n = len(weights)
    dp = [[0] * (W + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w in range(W + 1):
            dp[i][w] = dp[i - 1][w]
            if weights[i - 1] <= w:
                dp[i][w] = max(dp[i][w],
                    dp[i - 1][w - weights[i - 1]] + costs[i - 1])

    # Восстановление решения
    w, result = W, []
    for i in range(n, 0, -1):
        if dp[i][w] != dp[i - 1][w]:
            result.append(i - 1)
            w -= weights[i - 1]

    return dp[n][W], result

val, items = knapsack([2, 3, 4, 5], [3, 4, 5, 8], 8)
print(f"Стоимость: {val}, Предметы: {items}")
# Стоимость: 12, Предметы: [3, 1]
Динамическое программирование
Разработка и оптимизация ИИС

Программная реализация: Беллман-Форд

def bellman_ford(n, edges, s):
    d = [float('inf')] * n
    d[s] = 0
    p = [-1] * n

    for _ in range(n - 1):
        for u, v, w in edges:
            if d[u] + w < d[v]:
                d[v] = d[u] + w
                p[v] = u

    # Проверка отрицательных циклов
    for u, v, w in edges:
        if d[u] + w < d[v]:
            raise ValueError("Отрицательный цикл")

    # Восстановление пути
    def path(t):
        route = []
        while t != -1:
            route.append(t)
            t = p[t]
        return route[::-1]

    return d, path
Динамическое программирование
Разработка и оптимизация ИИС

Программная реализация: Флойда-Уоршелл

def floyd_warshall(n, dist):
    d = [row[:] for row in dist]
    p = [[i if dist[i][j] < float('inf') else -1
          for j in range(n)] for i in range(n)]

    for k in range(n):
        for i in range(n):
            for j in range(n):
                if d[i][k] + d[k][j] < d[i][j]:
                    d[i][j] = d[i][k] + d[k][j]
                    p[i][j] = p[k][j]

    return d, p
Динамическое программирование
Разработка и оптимизация ИИС

Заключение

Динамическое программирование
Разработка и оптимизация ИИС

Ключевые выводы лекции

  • ДП — разбиение задачи на перекрывающиеся подзадачи с запоминанием решений

  • Принцип оптимальности Беллмана — фундамент ДП

  • Беллман-Форд: O(VE)O(|V| \cdot |E|), отрицательные веса

  • Флойда-Уоршелл: O(V3)O(|V|^3), все пары вершин

  • Задача о рюкзаке: O(nW)O(n \cdot W), псевдополиномиальная

  • Два подхода: мемоизация (сверху вниз) и табуляция (снизу вверх)

  • Оптимизация памяти — хранить только необходимые предыдущие состояния

Динамическое программирование
Разработка и оптимизация ИИС

Вопросы для самоконтроля

  1. Сформулируйте принцип оптимальности Беллмана.
  2. Какие два свойства необходимы для применения ДП?
  3. В чём разница между мемоизацией и табуляцией?
  4. Запишите рекуррентное соотношение алгоритма Беллмана-Форда.
  5. Какова сложность алгоритма Флойда-Уоршелла?
  6. Запишите рекуррентное соотношение задачи о рюкзаке (0/1).
  7. Как восстановить решение по таблице ДП?
  8. Почему задача о рюкзаке имеет псевдополиномиальную сложность?
Динамическое программирование
Разработка и оптимизация ИИС

Рекомендуемые ресурсы

Основная литература:

  1. Алексеев, Д. С. Технологии интеллектуального анализа данных.
  2. Маккини, Уэс. Python и анализ данных. — pandas, NumPy.

Дополнительная литература:

  1. Серебряная, Л. В. Методы и алгоритмы принятия решений.
  2. Станкевич, Л. А. Интеллектуальные системы и технологии.

Онлайн-ресурсы:

  • Cormen, Leiserson, Rivest, Stein. Introduction to Algorithms (CLRS), гл. 14–15
  • https://pythontutor.com — визуализация рекурсии и ДП
Динамическое программирование

Кратко пробегитесь по плану: начнём с определения и ключевых понятий, затем принцип оптимальности, после — классические задачи (кратчайший путь, рюкзак), завершим программной реализацией.

Дайте определение: ДП — не конкретный алгоритм, а подход (парадигма). Приведите мотивирующий пример: маршрут с пересадками — каждый раз нужно выбирать лучший следующий шаг.

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

Подчеркните: ДП не работает для любых задач. Необходимы два свойства — оптимальная подструктура и перекрывающиеся подзадачи. Если их нет — нужен другой метод (жадный, перебор).

Разберите каждое свойство подробно с примерами. Подчеркните: оба свойства обязательны для применения ДП.

Сравните с другими подходами. Подчеркните: ДП занимает промежуточное положение между полным перебором и жадными алгоритмами.

Покажите дерево рекурсии для Фибоначчи и визуализируйте перекрывающиеся подзадачи. Это наглядная иллюстрация избыточных вычислений.

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

Приведите оба варианта кода для Фибоначчи. Студенты увидят разницу на практике.

Подчеркните: для многих задач избыточное хранение всей таблицы не нужно. Достаточно хранить «узкое окно» предыдущих состояний.

Перейдите к классическим задачам. Начните с кратчайшего пути — это основная задача в теории графов и в лабораторной работе.

Опишите алгоритм Беллмана-Форда: простой, работает с отрицательными весами, но медленнее Дейкстры.

Пошаговый алгоритм Беллмана-Форда. Подчеркните: $|V| - 1$ итерация достаточно, т.к. кратчайший путь содержит не более $|V| - 1$ рёбер.

Решите конкретный пример на доске. Покажите таблицу итераций — это то, что студенты будут делать на лабораторной.

Объясните: на итерации 4 ничего не изменилось — алгоритм сошёлся. Кратчайший путь: A→B→E→C (для C), A→B→E→D (для D).

Перейдите к Флойду-Уоршеллу — второму классическому алгоритму ДП для графов. Подчеркните: это ДП на подмножествах вершин.

Решите пример Флойда-Уоршелла. Покажите, как на каждой итерации $k$ рассматриваются пути через вершину $k$.

Перейдите ко второй ключевой задаче — задача о рюкзаке. Это классическая задача комбинаторной оптимизации с множеством практических приложений.

Выведите рекуррентное соотношение. Подчеркните: для каждого предмета есть два варианта — взять или не взять.

Решите конкретный пример шаг за шагом. Покажите заполнение таблицы — это наглядно и поможет на лабораторной.

Покажите код для задачи о рюкзаке — снизу вверх, с восстановлением решения. Студенты будут реализовывать это на лабораторной.

Покажите код Беллмана-Форда с восстановлением пути.

Покажите код Флойда-Уоршелла — компактный и элегантный.

Подведите итоги. Свяжите все темы: принцип Беллмана → конкретные задачи → код.

Акцентируйте внимание на связи принципа оптимальности с конкретными задачами и на том, что ДП — это не алгоритм, а парадигма.

Предложите студентам несколько вопросов для самопроверки.

Порекомендуйте литературу из учебной программы.