Перейти до основного вмісту

Задача про рюкзак

Необхідні попередні знання: Вступ до динамічного програмування

Вступ

Розгляньмо такий приклад:

[USACO07 Dec] Charm Bracelet

Є nn різних предметів і рюкзак місткості WW. Кожен предмет має 2 атрибути: вагу (wiw_{i}) і цінність (viv_{i}). Потрібно вибрати підмножину предметів, які покласти в рюкзак так, щоб сумарна вага не перевищувала місткість WW, а сумарна цінність була максимальною.

У наведеному вище прикладі кожен предмет має лише два можливі стани (взятий або не взятий), що відповідає двійковим 0 і 1. Тому цей тип задачі називають «задачею про рюкзак 0-1».

Коли підходить цей алгоритм?
  • Чи потрібно вибрати підмножину предметів, що максимізує сумарну цінність за обмеженням на сумарну вагу (місткість WW)?
  • Чи вага WW (та ваги предметів) — цілі й достатньо малі, щоб таблиця розміру O(nW)O(nW) вмістилася в пам'ять і час?
  • Чи кожен предмет береться щонайбільше один раз (0-1)? (якщо предмети повторюються необмежено — це варіант «необмеженого рюкзака» з прямим, а не зворотним порядком за jj)

Рюкзак 0-1

Пояснення

У наведеному вище прикладі вхідними даними задачі є: вага ii-го предмета wiw_{i}, цінність ii-го предмета viv_{i} і загальна місткість рюкзака WW.

Нехай fi,jf_{i, j}стан динамічного програмування, що зберігає максимальну сумарну цінність, яку може нести рюкзак місткості jj, коли ми розглядаємо лише перші ii предметів.

Припускаючи, що всі стани для перших i1i-1 предметів вже опрацьовано, які варіанти ми маємо для ii-го предмета?

  • Коли його не кладемо в рюкзак, залишкова місткість не змінюється і сумарна цінність теж не змінюється. Тому максимальна цінність у цьому випадку дорівнює fi1,jf_{i-1, j}
  • Коли його кладемо в рюкзак, залишкова місткість зменшується на wiw_{i}, а сумарна цінність зростає на viv_{i}, тож максимальна цінність у цьому випадку дорівнює fi1,jwi+vif_{i-1, j-w_i} + v_i

Звідси ми можемо вивести рівняння переходу ДП:

fi,j=max(fi1,j,fi1,jwi+vi)f_{i, j} = \max(f_{i-1, j}, f_{i-1, j-w_i} + v_i)

Далі, оскільки fif_{i} залежить лише від fi1f_{i-1}, ми можемо позбутися першого виміру. Отримуємо правило переходу

fjmax(fj,fjwi+vi)f_j \gets \max(f_j, f_{j-w_i}+v_i)

яке має виконуватися у порядку спадання jj (щоб fjwif_{j-w_i} неявно відповідало fi1,jwif_{i-1,j-w_i}, а не fi,jwif_{i,j-w_i}).

Важливо зрозуміти це правило переходу, бо більшість переходів у задачах про рюкзак виводяться подібним чином.

Реалізація

Описаний алгоритм можна реалізувати за O(nW)O(nW) так:

for (int i = 1; i <= n; i++)
for (int j = W; j >= w[i]; j--)
f[j] = max(f[j], f[j - w[i]] + v[i]);

Знову ж таки, зверніть увагу на порядок виконання. Його слід суворо дотримуватися, щоб забезпечити такий інваріант: безпосередньо перед опрацюванням пари (i,j)(i, j) значення fkf_k відповідає fi,kf_{i,k} для k>jk > j, але fi1,kf_{i-1,k} для k<jk < j. Це гарантує, що fjwif_{j-w_i} береться з (i1)(i-1)-го кроку, а не з ii-го.

Повний рюкзак

Модель повного рюкзака схожа на рюкзак 0-1, єдина відмінність від рюкзака 0-1 полягає в тому, що предмет можна вибрати необмежену кількість разів, а не лише один раз.

Ми можемо скористатися ідеєю рюкзака 0-1, щоб означити стан: fi,jf_{i, j} — максимальна цінність, яку може отримати рюкзак, використовуючи перші ii предметів за максимальної місткості jj.

Слід зауважити, що хоча означення стану схоже на означення для рюкзака 0-1, його правило переходу відрізняється від правила для рюкзака 0-1.

Пояснення

Тривіальний підхід полягає в тому, щоб для перших ii предметів перебрати, скільки разів узяти кожен предмет. Часова складність цього — O(n2W)O(n^2W).

Це дає таке рівняння переходу:

fi,j=maxk=0(fi1,jkwi+kvi)f_{i, j} = \max\limits_{k=0}^{\infty}(f_{i-1, j-k\cdot w_i} + k\cdot v_i)

Водночас воно спрощується до «плоского» рівняння:

fi,j=max(fi1,j,fi,jwi+vi)f_{i, j} = \max(f_{i-1, j},f_{i, j-w_i} + v_i)

Причина, чому це працює, полягає в тому, що fi,jwif_{i, j-w_i} вже було оновлено через fi,j2wif_{i, j-2\cdot w_i} і так далі.

Аналогічно до рюкзака 0-1, ми можемо позбутися першого виміру, щоб оптимізувати складність за пам'яттю. Це дає нам те саме правило переходу, що й для рюкзака 0-1.

fjmax(fj,fjwi+vi)f_j \gets \max(f_j, f_{j-w_i}+v_i)

Реалізація

Описаний алгоритм можна реалізувати за O(nW)O(nW) так:

for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= W; j++)
f[j] = max(f[j], f[j - w[i]] + v[i]);

Попри те, що правило переходу те саме, наведений вище код є неправильним для рюкзака 0-1.

Уважно придивившись до коду, ми бачимо, що для предмета ii, який зараз опрацьовуємо, і поточного стану fi,jf_{i,j}, коли jwij\geqslant w_{i}, на fi,jf_{i,j} впливатиме fi,jwif_{i,j-w_{i}}. Це рівносильно можливості покласти предмет ii у рюкзак кілька разів, що відповідає задачі про повний рюкзак, а не задачі про рюкзак 0-1.

Кратний рюкзак

Кратний рюкзак — також варіант рюкзака 0-1. Головна відмінність у тому, що кожного предмета є kik_i штук, а не лише 11.

Пояснення

Дуже проста ідея така: «вибрати кожен предмет kik_i разів» рівносильно тому, що «kik_i однакових предметів вибираються по одному». Таким чином ми зводимо це до моделі рюкзака 0-1, яку можна описати функцією переходу:

fi,j=maxk=0ki(fi1,jkwi+kvi)f_{i, j} = \max_{k=0}^{k_i}(f_{i-1,j-k\cdot w_i} + k\cdot v_i)

Часова складність цього процесу — O(Wi=1nki)O(W\sum\limits_{i=1}^{n}k_i)

Оптимізація бінарним групуванням

Ми так само розглядаємо зведення моделі кратного рюкзака до моделі рюкзака 0-1 для оптимізації. Часову складність O(Wn)O(Wn) не можна оптимізувати далі описаним вище підходом, тож зосередимося на складовій O(ki)O(\sum k_i).

Нехай Ai,jA_{i, j} позначає jj-й предмет, виділений із ii-го предмета. У тривіальному підході, розглянутому вище, Ai,jA_{i, j} позначає той самий предмет для всіх jkij \leq k_i. Головна причина нашої низької ефективності в тому, що ми робимо багато повторюваної роботи. Наприклад, розгляньмо вибір {Ai,1,Ai,2}\{A_{i, 1},A_{i, 2}\} і вибір {Ai,2,Ai,3}\{A_{i, 2}, A_{i, 3}\}. Ці дві ситуації цілком рівносильні. Тому оптимізація способу виділення значно зменшить часову складність.

Групування роблять ефективнішим, застосовуючи бінарне групування.

А саме, Ai,jA_{i, j} містить 2j2^j окремих предметів (j[0,log2(ki+1)1]j\in[0,\lfloor \log_2(k_i+1)\rfloor-1]). Якщо ki+1k_i + 1 не є цілим степенем 22, то для компенсації використовують ще один набір розміру ki(2log2(ki+1)1)k_i-(2^{\lfloor \log_2(k_i+1)\rfloor}-1).

За допомогою описаного способу виділення можна отримати будь-яку суму ki\leq k_i предметів, вибравши кілька Ai,jA_{i, j}. Після виділення кожного предмета описаним способом достатньо застосувати метод рюкзака 0-1, щоб розв'язати нове формулювання задачі.

Ця оптимізація дає нам часову складність O(Wi=1nlogki)O(W\sum\limits_{i=1}^{n}\log k_i).

Реалізація

index = 0;
for (int i = 1; i <= n; i++) {
int c = 1, p, h, k;
cin >> p >> h >> k;
while (k > c) {
k -= c;
list[++index].w = c * p;
list[index].v = c * h;
c *= 2;
}
list[++index].w = p * k;
list[index].v = h * k;
}

Оптимізація монотонною чергою

У цій оптимізації ми прагнемо звести задачу про рюкзак до задачі про чергу з максимумом.

Для зручності опису покладемо gx,y=fi,xwi+y, gx,y=fi1,xwi+yg_{x, y} = f_{i, x \cdot w_i + y} ,\space g'_{x, y} = f_{i-1, x \cdot w_i + y}. Тоді правило переходу можна записати так:

gx,y=maxk=0ki(gxk,y+vik)g_{x, y} = \max_{k=0}^{k_i}(g'_{x-k, y} + v_i \cdot k)

Далі покладемо Gx,y=gx,yvixG_{x, y} = g'_{x, y} - v_i \cdot x. Тоді правило переходу можна виразити так:

gx,ymaxk=0ki(Gxk,y)+vixg_{x, y} \gets \max_{k=0}^{k_i}(G_{x-k, y}) + v_i \cdot x

Це перетворюється на класичну форму оптимізації монотонною чергою. Gx,yG_{x, y} можна обчислити за O(1)O(1), тож для фіксованого yy ми можемо обчислити gx,yg_{x, y} за час O(Wwi)O(\lfloor \frac{W}{w_i} \rfloor). Отже, складність знаходження всіх gx,yg_{x, y} дорівнює O(Wwi)×O(wi)=O(W)O(\lfloor \frac{W}{w_i} \rfloor) \times O(w_i) = O(W). У такий спосіб загальна складність алгоритму зменшується до O(nW)O(nW).

Змішаний рюкзак

Задача про змішаний рюкзак поєднує три описані вище задачі. Тобто деякі предмети можна взяти лише раз, деякі — необмежено, а деякі — не більше ніж kk разів.

Задача може видаватися складною, але доки ви розумієте основні ідеї попередніх задач про рюкзак і поєднуєте їх разом, ви впораєтеся. Псевдокод розв'язку такий:

for (each item) {
if (0-1 knapsack)
Apply 0-1 knapsack code;
else if (complete knapsack)
Apply complete knapsack code;
else if (multiple knapsack)
Apply multiple knapsack code;
}

Задачі для практики

Відеоматеріали