Рюкзак задачасы деген не?
Рюкзак задачасы — динамикалық программалаудың классикалық задачасы.
Бізде n зат бар. Әр затта екі мән болады:
- weight[i] — заттың weight-і
- value[i] — заттың value-ы
Рюкзактың сыйымдылығы W. Кейбір заттарды таңдау керек:
жалпы value максималды болу керек
Қарапайым мысал
Рюкзак сыйымдылығы:
Заттар:
| Зат | Weight | Value |
|---|---|---|
| 1 | 3 | 10 |
| 2 | 2 | 7 |
| 3 | 4 | 12 |
Ең жақсы таңдау:
weight = 3 + 2 = 5
value = 10 + 7 = 17
Жауап:
17
Негізгі идея
Әр зат үшін екі вариант бар:
- затты алмау
- егер орын жетсе, затты алу
затты алмайтын вариант,
затты алатын вариант
)
Рекурсивный метод
Алдымен рюкзакты рекурсия арқылы түсіну оңай.
Мына функцияны ойлаймыз:
f(i, w)
Бұл алғашқы i затты қарағанда және рюкзакта w орын болғанда алуға болатын максималды value.
w — рюкзактың қазіргі сыйымдылығы
Рекурсивный переход
i-ші затты қараймыз.
1-вариант: i-ші затты алмаймыз:
f(i - 1, w)
2-вариант: i-ші затты аламыз. Бұл тек оның weight-і w-дан аспаса ғана болады.
f(i - 1, w - weight[i]) + value[i]
Сондықтан формула:
f(i, w) = max(f(i - 1, w), f(i - 1, w - weight[i]) + value[i]);
Базалық жағдай
Егер зат жоқ болса, value 0 болады.
if (i == 0) {
return 0;
}
Егер рюкзакта орын қалмаса, енді ештеңе ала алмаймыз.
if (w == 0) {
return 0;
}
Обычный рекурсия
int solve(int i, int w) {
if (i == 0 || w == 0) {
return 0;
}
int ans = solve(i - 1, w);
if (weight[i] <= w) {
ans = max(ans, solve(i - 1, w - weight[i]) + value[i]);
}
return ans;
}
Бұл код идея бойынша дұрыс, бірақ баяу. Себебі бірдей state-терді көп рет қайта есептейді.
Рекурсия + memoization
Бірдей state-ті қайта есептемеу үшін жауапты сақтаймыз:
dp[i][w]
Бұл i, w state үшін answer.
Егер answer бұрын есептелген болса, оны қайтарамыз:
if (dp[i][w] != -1) {
return dp[i][w];
}
Рекурсия + memoization толық код
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = 10010;
int n, W;
int weight[N];
int value[N];
int dp[N][M];
int solve(int i, int w) {
if (i == 0 || w == 0) {
return 0;
}
if (dp[i][w] != -1) {
return dp[i][w];
}
int ans = solve(i - 1, w);
if (weight[i] <= w) {
ans = max(ans, solve(i - 1, w - weight[i]) + value[i]);
}
dp[i][w] = ans;
return dp[i][w];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> W;
for (int i = 1; i <= n; i++) {
cin >> weight[i] >> value[i];
}
memset(dp, -1, sizeof(dp));
cout << solve(n, W);
return 0;
}
Итеративный DP-ға қалай ауыстырамыз?
Рекурсияда:
solve(i, w)
Итеративный DP-да:
dp[i][w]
Мағынасы:
алғашқы i затты қарастырғанда
рюкзак сыйымдылығы w болса
Базалық мәндер
Егер зат саны 0 болса, answer 0:
dp[0][w] = 0;
Егер сыйымдылық 0 болса, answer 0:
dp[i][0] = 0;
Глобальный массив C++-та өзі 0 болады, сондықтан көбіне бөлек жазбаса да болады.
Итеративный переход
Әр зат i және әр сыйымдылық w үшін:
Затты алмаймыз:
dp[i][w] = dp[i - 1][w];
Егер затты алуға орын жетсе:
dp[i][w] = max(dp[i][w], dp[i - 1][w - weight[i]] + value[i]);
2D DP толық реализация
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = 10010;
int n, W;
int weight[N];
int value[N];
int dp[N][M];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> W;
for (int i = 1; i <= n; i++) {
cin >> weight[i] >> value[i];
}
for (int i = 1; i <= n; i++) {
for (int w = 0; w <= W; w++) {
dp[i][w] = dp[i - 1][w];
if (weight[i] <= w) {
dp[i][w] = max(dp[i][w], dp[i - 1][w - weight[i]] + value[i]);
}
}
}
cout << dp[n][W];
return 0;
}
Input / Output мысалы
Input
3 5
3 10
2 7
4 12
Output
17
Мұнда:
2-зат: weight 2, value 7
3-зат: weight 4, value 12
W = 5
Ең жақсы таңдау — 1-зат және 2-зат.
dp[i][w] қалай оқылады?
Мысалы:
dp[3][5]
Бұл дегеніміз:
рюкзак сыйымдылығы 5
максималды қандай value ала аламыз?
Финал answer:
dp[n][W]
Memory optimization
Формулада:
dp[i][w]
тек алдыңғы строкаға байланысты:
dp[i - 1][...]
Сондықтан бір ғана массив қолдануға болады:
dp[w]
Мұнда dp[w] — сыйымдылық w үшін ең жақсы answer.
Неге цикл оңнан солға жүреді?
0/1 knapsack-та әр затты тек бір рет аламыз.
Сондықтан dp[w] жаңартқанда циклді оңнан солға жүргіземіз:
for (int w = W; w >= weight[i]; w--)
Егер солдан оңға жүрсек, бір зат бірнеше рет қолданылып кетуі мүмкін.
1D DP толық реализация
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int n, W;
int dp[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> W;
for (int i = 1; i <= n; i++) {
int weight, value;
cin >> weight >> value;
for (int w = W; w >= weight; w--) {
dp[w] = max(dp[w], dp[w - weight] + value);
}
}
cout << dp[W];
return 0;
}
2D DP vs 1D DP
| Метод | Память | Плюс | Минус |
|---|---|---|---|
| 2D DP | O(nW) | түсіну оңай | память көп кетеді |
| 1D DP | O(W) | память үнемдейді | цикл бағыты маңызды |
Қай заттар таңдалғанын табу
Кейде тек max value емес, нақты қай заттар алынғанын шығару керек.
Ол үшін 2D DP қолданған ыңғайлы.
Артқа қарай жүреміз:
егер dp[i][w] == dp[i - 1][w], i-зат алынбаған
әйтпесе i-зат алынған
Заттарды қалпына келтіру коды
vector<int> items;
int w = W;
for (int i = n; i >= 1; i--) {
if (dp[i][w] == dp[i - 1][w]) {
continue;
}
items.push_back(i);
w -= weight[i];
}
reverse(items.begin(), items.end());
Қалпына келтірумен толық код
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = 10010;
int n, W;
int weight[N];
int value[N];
int dp[N][M];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> W;
for (int i = 1; i <= n; i++) {
cin >> weight[i] >> value[i];
}
for (int i = 1; i <= n; i++) {
for (int w = 0; w <= W; w++) {
dp[i][w] = dp[i - 1][w];
if (weight[i] <= w) {
dp[i][w] = max(dp[i][w], dp[i - 1][w - weight[i]] + value[i]);
}
}
}
cout << "Max value: " << dp[n][W] << '\n';
vector<int> items;
int w = W;
for (int i = n; i >= 1; i--) {
if (dp[i][w] == dp[i - 1][w]) {
continue;
}
items.push_back(i);
w -= weight[i];
}
reverse(items.begin(), items.end());
cout << "Items: ";
for (int x : items) {
cout << x << ' ';
}
return 0;
}
Unbounded Knapsack
Рюкзактың басқа түрі бар: әр затты қанша рет болса да алуға болады.
Бұл:
полный рюкзак
Мұнда цикл weight бойынша солдан оңға жүреді:
for (int w = weight; w <= W; w++)
Полный рюкзак реализация
#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int n, W;
int dp[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> W;
for (int i = 1; i <= n; i++) {
int weight, value;
cin >> weight >> value;
for (int w = weight; w <= W; w++) {
dp[w] = max(dp[w], dp[w - weight] + value);
}
}
cout << dp[W];
return 0;
}
0/1 vs полный рюкзак
| Тип | Затты неше рет алуға болады? | w циклі |
|---|---|---|
| 0/1 Knapsack | 0 немесе 1 рет | W-дан төмен |
| Unbounded Knapsack | қанша рет болса да | weight-тен жоғары |
Типтік қателер
- dp[i][w] нені білдіретінін түсінбеу
- weight пен value-ды шатастыру
- 0/1 knapsack-та циклді солдан оңға жүргізу
- полный рюкзакта циклді оңнан солға жүргізу
- weight[i] <= w тексеруді ұмыту
- заттарды қалпына келтіруде 1D DP қолдануға тырысу
- массив size-ын тым кішкентай қою
Асимптотика
| Метод | Уақыт | Память |
|---|---|---|
| Обычный рекурсия | O(2ⁿ) | O(n) |
| Memoization | O(nW) | O(nW) |
| 2D DP | O(nW) | O(nW) |
| 1D DP | O(nW) | O(W) |
Задачада қалай ойлау керек?
2. Weight деген не?
3. Value деген не?
4. Затты бір рет аламыз ба, көп рет аламыз ба?
5. Maximum, minimum немесе тәсіл саны керек пе?
6. W size қанша?
7. O(nW) жарай ма?
Қысқа шаблон 0/1 Knapsack
int dp[W + 1];
for (int i = 1; i <= n; i++) {
cin >> weight >> value;
for (int w = W; w >= weight; w--) {
dp[w] = max(dp[w], dp[w - weight] + value);
}
}
cout << dp[W];
Қысқа шаблон полного рюкзака
int dp[W + 1];
for (int i = 1; i <= n; i++) {
cin >> weight >> value;
for (int w = weight; w <= W; w++) {
dp[w] = max(dp[w], dp[w - weight] + value);
}
}
cout << dp[W];