Задача о рюкзаке: динамическое программирование
Задача о рюкзаке - классический пример, которым открывают курс динамического программирования в дискретной математике и алгоритмах. Постановка проста: есть предметов с весами и ценностями , и рюкзак вместимостью . Нужно выбрать подмножество предметов так, чтобы суммарный вес не превысил , а суммарная ценность была максимальной. Перебор всех подмножеств здесь не подходит уже при , и именно ДП сводит задачу к операциям.
Идея динамического программирования
Ключевой принцип ДП - разбить большую задачу на перекрывающиеся подзадачи и сохранить их результаты. Для рюкзака подзадача - это «максимальная ценность при первых предметах и вместимости ». Обозначим её .
Чтобы найти , у нас ровно два варианта:
- Не брать -й предмет - тогда .
- Взять -й предмет (возможно только если ) - тогда .
Отсюда рекуррентное соотношение:
Начальные условия: для всех (нет предметов - нет ценности) и для всех (рюкзак пуст - нечего класть).
Пример заполнения dp-таблицы
Возьмём учебный набор из 4 предметов:
| # | Предмет | Вес | Ценность |
|---|---|---|---|
| 1 | книга | 2 | 3 |
| 2 | ноутбук | 4 | 5 |
| 3 | камера | 3 | 4 |
| 4 | часы | 1 | 2 |
Вместимость . Начинаем с нулевой строки (все нули) и заполняем строку за строкой:
Для строки «часы» (вес 1, ценность 2) при : . Берём часы.
Итоговая таблица (строки - предметы, столбцы - вместимость от 0 до 7):
Это и есть оптимальная ценность при вместимости 7.
Восстановление оптимального набора (traceback)
Таблица хранит только значения, а не сами выборы. Чтобы узнать, какие предметы вошли в набор, идём по таблице с конца:
- Начинаем с , .
- Если - предмет взят. Переходим к , .
- Иначе - предмет не взят. Переходим к , не меняем.
- Повторяем до .
Для нашего примера traceback при : берём часы (, ), берём камеру (, ), не берём ноутбук, берём книгу (). Итого: книга + камера + часы, ценность , вес .
Канонический inline-кадр: traceback в таблице

На inline-кадре видно, что traceback идёт по «синим» ячейкам (где ) - это строки, в которых предмет был включён в рюкзак. Золотые рамки отмечают конкретный путь при .
Сложность алгоритма
Временная сложность - : два вложенных цикла по предметам и значениям вместимости. Пространственная - тоже для хранения всей таблицы.
Оптимизация памяти: поскольку строка $i$ зависит только от строки $i-1$, таблицу можно свести к двум строкам или даже к одному массиву (обход справа налево). Тогда пространственная сложность падает до $O(W)$.
При обходе справа налево для одного массива dp[w]:
Направление справа налево гарантирует, что ещё не обновлялось на текущей итерации - то есть берётся значение из «предыдущей строки».
Ограничения метода: псевдополиномиальность
Важный нюанс: хотя сложность выглядит полиномиальной, на самом деле - числовое значение (не количество битов для его записи). Алгоритм называют псевдополиномиальным: при он неприменим. Для таких задач используют ветви и границы или аппроксимационные схемы (FPTAS).
Дробный рюкзак (fractional knapsack) - жадный алгоритм, а не ДП. Если можно брать дробные части предметов, достаточно отсортировать по удельной ценности v/w и жадно заполнять рюкзак.
Вариации задачи
Неограниченный рюкзак (unbounded knapsack): каждый предмет можно брать сколько угодно раз. Переход меняется: - обратите внимание, что теперь строка , а не в варианте «взять предмет».
Рюкзак с точной вместимостью: нужно заполнить рюкзак ровно до . Инициализируем , для .
Мультидимензиональный рюкзак: несколько ограничений (вес и объём). Таблица становится трёхмерной: .
Задача о сумме подмножества (subset sum): частный случай, где и нужно набрать ровно .
Частые ошибки
- Обход слева направо при оптимизации памяти - предмет может войти несколько раз (превращается в unbounded knapsack). Нужен обход справа налево.
- Инициализация ненулевыми значениями - нулевая строка и нулевой столбец должны быть нулями; иначе traceback ломается.
- Путаница индексов - в коде удобно хранить с 1-индексацией предметов, но при трансляции из 0-индексированных массивов языка легко ошибиться.
- Попытка применить жадный алгоритм - сортировка по удельной ценности не даёт оптимума для целочисленного рюкзака. Стандартный контрпример: предметы и при : жадный возьмёт предмет 2 () и получит ценность 2, а ДП возьмёт предмет 1 (... нет, , берётся предмет 2). Возьмём другой: , , при : жадный выберет невлезет и ; ДП выберет .
- Забыть traceback - таблица даёт только оптимальную ценность; набор предметов нужно восстанавливать отдельно.
FAQ
Чем ДП-решение рюкзака отличается от жадного? Жадный алгоритм принимает локально оптимальное решение на каждом шаге (берём предмет с наибольшим ), и для целочисленного рюкзака это может дать субоптимальный ответ. ДП перебирает все комбинации неявно через рекуррентное соотношение и гарантирует глобальный оптимум.
Почему алгоритм называют псевдополиномиальным? Потому что сложность зависит от числового значения , а не от длины его двоичной записи (). Если задаётся битами, то «реальная» сложность - , что является экспоненциальной. При малых или умеренных алгоритм очень быстр на практике.
Как решить задачу о рюкзаке для дробных объёмов? Если и - рациональные числа, умножаем все веса на наименьшее общее кратное знаменателей, чтобы перейти к целым числам, и применяем стандартный алгоритм ДП. Если точность не критична - округляем веса до нужного числа знаков и масштабируем.
Коротко
Задача о рюкзаке в постановке 0/1 решается динамическим программированием за времени: строим таблицу снизу вверх по рекуррентному соотношению, а затем восстанавливаем оптимальный набор обратным проходом (traceback). Оптимизация памяти до возможна при обходе столбцов справа налево. Алгоритм псевдополиномиален - применим, пока не слишком велико.
Читайте также

Алгоритм Беллмана-Форда: пути с отрицательными весами
Разбираем алгоритм Беллмана-Форда: как искать кратчайшие пути в графе с отрицательными рёбрами, ловить отрицательные циклы и чем он отличается от Дейкстры.

Алгоритм Прима - как построить остовное дерево по шагам
Разбираем, как алгоритм Прима шаг за шагом строит минимальное остовное дерево графа: идея жадного выбора, лемма о разрезе и трассировка на конкретном примере.

Алгоритм Эдмондса-Карпа: поиск максимального потока
Алгоритм Эдмондса-Карпа находит максимальный поток в сети: BFS ищет дополняющий путь, что даёт полиномиальную сложность. Разбираем идею, реализацию и оценку.