EssayAI
Блог
Блог
Математика и алгоритмы

Задача о рюкзаке: динамическое программирование

17 июня 2026Время чтения: 7 минут
#динамическое программирование#задача о рюкзаке#алгоритмы#дискретная математика

Задача о рюкзаке - классический пример, которым открывают курс динамического программирования в дискретной математике и алгоритмах. Постановка проста: есть nn предметов с весами w1,,wnw_1, \ldots, w_n и ценностями v1,,vnv_1, \ldots, v_n, и рюкзак вместимостью WW. Нужно выбрать подмножество предметов так, чтобы суммарный вес не превысил WW, а суммарная ценность была максимальной. Перебор всех 2n2^n подмножеств здесь не подходит уже при n=30n = 30, и именно ДП сводит задачу к O(nW)O(n \cdot W) операциям.

Идея динамического программирования

Ключевой принцип ДП - разбить большую задачу на перекрывающиеся подзадачи и сохранить их результаты. Для рюкзака подзадача - это «максимальная ценность при первых ii предметах и вместимости ww». Обозначим её dp[i][w]dp[i][w].

Чтобы найти dp[i][w]dp[i][w], у нас ровно два варианта:

  1. Не брать ii-й предмет - тогда dp[i][w]=dp[i1][w]dp[i][w] = dp[i-1][w].
  2. Взять ii-й предмет (возможно только если wiww_i \le w) - тогда dp[i][w]=dp[i1][wwi]+vidp[i][w] = dp[i-1][w - w_i] + v_i.

Отсюда рекуррентное соотношение:

dp[i][w]={dp[i1][w],wi>w,max ⁣(dp[i1][w],  dp[i1][wwi]+vi),wiw.dp[i][w] = \begin{cases} dp[i-1][w], & w_i > w, \\ \max\!\bigl(dp[i-1][w],\; dp[i-1][w - w_i] + v_i\bigr), & w_i \le w. \end{cases}

Начальные условия: dp[0][w]=0dp[0][w] = 0 для всех ww (нет предметов - нет ценности) и dp[i][0]=0dp[i][0] = 0 для всех ii (рюкзак пуст - нечего класть).

Пример заполнения dp-таблицы

Возьмём учебный набор из 4 предметов:

#ПредметВес wiw_iЦенность viv_i
1книга23
2ноутбук45
3камера34
4часы12

Вместимость W=7W = 7. Начинаем с нулевой строки (все нули) и заполняем строку за строкой:

Анимация заполнения dp-таблицы: золотой маркер проходит по каждой строке

Для строки «часы» (вес 1, ценность 2) при w=3w = 3: dp[4][3]=max(dp[3][3],  dp[3][2]+2)=max(4,  3+2)=5dp[4][3] = \max(dp[3][3],\; dp[3][2] + 2) = \max(4,\; 3 + 2) = 5. Берём часы.

Итоговая таблица (строки - предметы, столбцы - вместимость от 0 до 7):

dp[4][7]=9dp[4][7] = 9

Это и есть оптимальная ценность при вместимости 7.

Восстановление оптимального набора (traceback)

Таблица dpdp хранит только значения, а не сами выборы. Чтобы узнать, какие предметы вошли в набор, идём по таблице с конца:

  1. Начинаем с i=ni = n, w=Ww = W.
  2. Если dp[i][w]dp[i1][w]dp[i][w] \ne dp[i-1][w] - предмет ii взят. Переходим к i1i-1, wwwiw \leftarrow w - w_i.
  3. Иначе - предмет ii не взят. Переходим к i1i-1, ww не меняем.
  4. Повторяем до i=0i = 0.
Traceback по dp-таблице: для последней строки маркер показывает, когда предмет берётся, а когда - нет

Для нашего примера traceback при W=7W = 7: берём часы (dp[4][7]dp[3][7]dp[4][7] \ne dp[3][7], w=6w = 6), берём камеру (dp[3][6]dp[2][6]dp[3][6] \ne dp[2][6], w=3w = 3), не берём ноутбук, берём книгу (dp[1][3]dp[0][3]dp[1][3] \ne dp[0][3]). Итого: книга + камера + часы, ценность 3+4+2=93 + 4 + 2 = 9, вес 2+3+1=672 + 3 + 1 = 6 \le 7.

Канонический inline-кадр: traceback в таблице

Traceback-путь в dp-таблице: золотые рамки показывают оптимальный набор, синие ячейки - где предмет был взят
Traceback-путь в dp-таблице: золотые рамки показывают оптимальный набор, синие ячейки - где предмет был взят

На inline-кадре видно, что traceback идёт по «синим» ячейкам (где dp[i][w]>dp[i1][w]dp[i][w] > dp[i-1][w]) - это строки, в которых предмет был включён в рюкзак. Золотые рамки отмечают конкретный путь при W=7W = 7.

Сложность алгоритма

Временная сложность - O(nW)O(n \cdot W): два вложенных цикла по nn предметам и W+1W + 1 значениям вместимости. Пространственная - тоже O(nW)O(n \cdot W) для хранения всей таблицы.

Оптимизация памяти: поскольку строка $i$ зависит только от строки $i-1$, таблицу можно свести к двум строкам или даже к одному массиву (обход справа налево). Тогда пространственная сложность падает до $O(W)$.

При обходе справа налево для одного массива dp[w]:

dp[w]max(dp[w],  dp[wwi]+vi),w=W,W1,,wi.dp[w] \leftarrow \max(dp[w],\; dp[w - w_i] + v_i), \quad w = W, W-1, \ldots, w_i.

Направление справа налево гарантирует, что dp[wwi]dp[w - w_i] ещё не обновлялось на текущей итерации - то есть берётся значение из «предыдущей строки».

Ограничения метода: псевдополиномиальность

Важный нюанс: хотя сложность O(nW)O(n \cdot W) выглядит полиномиальной, на самом деле WW - числовое значение (не количество битов для его записи). Алгоритм называют псевдополиномиальным: при W=264W = 2^{64} он неприменим. Для таких задач используют ветви и границы или аппроксимационные схемы (FPTAS).

Дробный рюкзак (fractional knapsack) - жадный алгоритм, а не ДП. Если можно брать дробные части предметов, достаточно отсортировать по удельной ценности v/w и жадно заполнять рюкзак.

Вариации задачи

Неограниченный рюкзак (unbounded knapsack): каждый предмет можно брать сколько угодно раз. Переход меняется: dp[i][w]=max(dp[i1][w],  dp[i][wwi]+vi)dp[i][w] = \max(dp[i-1][w],\; dp[i][w - w_i] + v_i) - обратите внимание, что теперь строка ii, а не i1i-1 в варианте «взять предмет».

Рюкзак с точной вместимостью: нужно заполнить рюкзак ровно до WW. Инициализируем dp[0][0]=0dp[0][0] = 0, dp[0][w]=dp[0][w] = -\infty для w>0w > 0.

Мультидимензиональный рюкзак: несколько ограничений (вес и объём). Таблица становится трёхмерной: dp[i][w][v]dp[i][w][v].

Задача о сумме подмножества (subset sum): частный случай, где vi=wiv_i = w_i и нужно набрать ровно WW.

Частые ошибки

  • Обход слева направо при оптимизации памяти - предмет может войти несколько раз (превращается в unbounded knapsack). Нужен обход справа налево.
  • Инициализация ненулевыми значениями - нулевая строка и нулевой столбец должны быть нулями; иначе traceback ломается.
  • Путаница индексов - в коде удобно хранить dp[i][w]dp[i][w] с 1-индексацией предметов, но при трансляции из 0-индексированных массивов языка легко ошибиться.
  • Попытка применить жадный алгоритм - сортировка по удельной ценности не даёт оптимума для целочисленного рюкзака. Стандартный контрпример: предметы (3,4)(3, 4) и (1,2)(1, 2) при W=2W = 2: жадный возьмёт предмет 2 (v/w=2v/w = 2) и получит ценность 2, а ДП возьмёт предмет 1 (v/w=4/3v/w = 4/3... нет, v=4>W=2v=4 > W=2, берётся предмет 2). Возьмём другой: (5,10)(5,10), (4,8)(4,8), (3,7)(3,7) при W=7W=7: жадный выберет (5,10)+(2(5,10)+(2 невлезет)) и v=10v=10; ДП выберет (4,8)+(3,7)=15(4,8)+(3,7)=15.
  • Забыть traceback - таблица даёт только оптимальную ценность; набор предметов нужно восстанавливать отдельно.

FAQ

Чем ДП-решение рюкзака отличается от жадного? Жадный алгоритм принимает локально оптимальное решение на каждом шаге (берём предмет с наибольшим vi/wiv_i/w_i), и для целочисленного рюкзака это может дать субоптимальный ответ. ДП перебирает все комбинации неявно через рекуррентное соотношение и гарантирует глобальный оптимум.

Почему алгоритм называют псевдополиномиальным? Потому что сложность O(nW)O(n \cdot W) зависит от числового значения WW, а не от длины его двоичной записи (logW\log W). Если WW задаётся kk битами, то «реальная» сложность - O(n2k)O(n \cdot 2^k), что является экспоненциальной. При малых или умеренных WW алгоритм очень быстр на практике.

Как решить задачу о рюкзаке для дробных объёмов? Если wiw_i и WW - рациональные числа, умножаем все веса на наименьшее общее кратное знаменателей, чтобы перейти к целым числам, и применяем стандартный алгоритм ДП. Если точность не критична - округляем веса до нужного числа знаков и масштабируем.

Коротко

Задача о рюкзаке в постановке 0/1 решается динамическим программированием за O(nW)O(n \cdot W) времени: строим таблицу dp[i][w]dp[i][w] снизу вверх по рекуррентному соотношению, а затем восстанавливаем оптимальный набор обратным проходом (traceback). Оптимизация памяти до O(W)O(W) возможна при обходе столбцов справа налево. Алгоритм псевдополиномиален - применим, пока WW не слишком велико.

Доверьте текст нейросети EssayAI

Открыть EssayAI

Бесплатно, на русском языке и без VPN

Читайте также