--- title: Quy hoạch động (Phần 3: Quy hoạch động knapsack) --- Quy hoạch động (Phần 3: Quy hoạch động knapsack) === --- ###### ✍️ Author: 2School Guideline ###### 📋 Content: [TOC] --- # Kiến thức cần có Để hiểu hơn về **Quy hoạch động cái túi (knapsack)**, trước hết bạn hãy tìm hiểu: - Kĩ thuật [Quy hoạch động](https://hackmd.io/@2SchoolGuideline/r1PSIpVcT) - Kĩ thuật [Đệ quy và quay lui](https://hackmd.io/zq12mu3IScqbYy9a9HtSRw) # Quy hoạch động cái túi (knapsack) ## 1. Bài toán Có $n$ đồ vật được đánh số từ $1$ đến $n$. Đồ vật thứ $i$ $(1 \le i \le n)$ có khối lượng $w_i$ và giá trị $v_i$. Có một chiếc túi có sức chứa tối đa là $W$ - tổng khối lượng tối đa của các vật đựng trong túi là $W$. Biết mỗi đồ vật chỉ được chọn một lần. Tính tổng giá trị $V$ lớn nhất của các đồ vật đựng trong túi. (Giả sử chọn cách đánh số mảng bắt đầu từ $1$) ## 2. Thuật toán Với bài toán này, chúng ta cần quan tâm tới các yếu tố: - **Đồ vật thứ bao nhiêu (1)** - **Tổng khối lượng các đồ vật (2)** - **Tổng giá trị các đồ vật (3)** Ta sẽ chọn 1 trong số 3 yếu tố này để tính toán, và sử dụng 2 yếu tố còn lại làm trạng thái quy hoạch động. Ta nhận thấy, để thuận lợi trong việc giải bài toán này, nên lựa chọn tính theo **(2)** hoặc **(3)**. ### 2.1. Cách 1 (tính theo (3)) - **Sử dụng:** Bài toán có giới hạn $n * W$ vừa đủ chạy trong thời gian cho phép. - **Gọi** `dp[i][x]` là tổng giá trị lớn nhất của túi khi xét từ vật 1 đến vật `i` và tổng trọng lượng các vật trong túi chưa vượt quá `x`. - **Trường hợp suy biến:** - `dp[i][0] = 0` - `dp[0][x] = 0` - **Công thức truy hồi:** - **Nếu** `w[i] > x` *Không* chọn vật thứ `i` => `dp[i][x] = dp[i - 1][x]` - **Nếu** `w[i] <= x`, ta có 2 lựa chọn: - Chọn vật thứ `i` => Cần lấy từ `i - 1` vật còn lại sao cho tổng khối lượng của chúng không vượt quá `x - w[i]`: `dp[i - 1][x - w[i]] + v[i]` - *Không* chọn vật thứ `i` => Cần lấy từ `i - 1` vật còn lại sao cho tổng của chúng không vượt quá `x`: `dp[i - 1][x]` $\Rightarrow$ **Phương án tối ưu:** Chọn cách cho ra giá trị lớn hơn trong 2 cách nêu trên: `dp[i][x] = max(dp[i - 1][x - w[i]] + v[i], dp[i - 1][x])` - **Đáp số:** `dp[n][W]` - **Độ phức tạp thời gian:** $O(n * W)$ - **Luyện tập cách cài đặt:** [Atcoder Educational DP Contest D - Knapsack 1](https://oj.vnoi.info/problem/atcoder_dp_d) :::spoiler **Code mẫu**: ```cpp= #include<bits/stdc++.h> using namespace std; int n; long long W; long long w[102]; long long v[102]; long long dp[102][100002]; int main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> W; for(int i = 1; i <= n; ++i) cin >> w[i] >> v[i]; for(int i = 1; i <= n; ++i) dp[i][0] = 0; for(int x = 0; x <= W; ++x) dp[0][x] = 0; for(int i = 1; i <= n; ++i){ for(int x = 1; x <= W; ++x){ if(w[i] > x) dp[i][x] = dp[i - 1][x]; else dp[i][x] = max(dp[i - 1][x - w[i]] + v[i], dp[i - 1][x]); } } cout << dp[n][W]; } ``` ::: - Tối ưu 1 chiều: - **Gọi** `dp[x]` là tổng giá trị lớn nhất của túi khi tổng khối lượng là `x`. - **Trường hợp suy biến:** - `dp[0] = 0` - **Công thức truy hồi:** - Lặp biến i từ $1$ đến $n$. Trong mỗi lần lặp $i$, chạy biến $x$ từ $W$ đến $1$ (tránh trường hợp trùng lắp): `dp[x] = max(dp[x], dp[x - w[i]] + v[i])` (nếu $w[i] \le x$) :::spoiler **Tại sao lại duyệt x từ W đến 1?** Với giá trị i bất kì từ 1 -> n: - Giả sử đang xét đến x => đã cập nhật dp[0] -> dp[x - 1] (có thể có hoặc không chứa vật thứ i). - Nếu cập nhật dp[x] cho i dựa trên giá trị từ dp[0] -> dp[x - 1] thì có khả năng dùng đồ vật i 2 lần. => Nếu chạy ngược về thì khi xét đến dp[x] thì dp[0] -> dp[x - 1] chỉ mới được tính cho đồ vật thứ i - 1 => không chứa đồ vật thứ i. ::: - **Đáp số:** `dp[W]` :::spoiler **Code mẫu**: ```cpp= int n; long long W; long long w[102]; long long v[102]; long long dp[100002]; int main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> W; for(int i = 1; i <= n; ++i) cin >> w[i] >> v[i]; for(int x = 0; x <= W; ++x) dp[x] = 0; for(int i = 1; i <= n; i++){ for(int x = W; x >= 1; --x){ if(w[i] <= x) dp[x] = max(dp[x], dp[x - w[i]] + v[i]); } } cout << dp[W]; } ``` ::: ### 2.2. Cách 2 (tính theo (2)) - **Sử dụng:** Bài toán có giới hạn $n * W$ lớn, nếu dùng cách 1 sẽ bị quá thời gian, tuy nhiên, $n * V$ (với $V$ là tổng các giá trị $v[i]$) lại vừa đủ chạy trong giới hạn thời gian cho phép. - **Gọi** `dp[i][y]` là tổng khối lượng nhỏ nhất khi xét từ vật 1 đến vật `i` để đạt được giá trị `y`, $V$ là tổng các giá trị $v[i]$. - **Trường hợp suy biến:** - `dp[0][0] = 0` - `dp[i][0] = 0` - `dp[i][j] = INF` (với ô [i][j] chưa được gán ở 2 bước trên và INF là giá trị rất lớn (thường >= $10^{18}$ )) - **Công thức truy hồi:** - **Nếu** `v[i] > y` *Không* chọn vật thứ i => `dp[i][y] = dp[i - 1][y]` - **Nếu** `v[i] <= y`, ta có 2 lựa chọn: - Chọn vật thứ `i` => Cần lấy từ `i - 1` vật còn lại sao cho tổng giá trị của chúng không vượt quá `y - v[i]`: `dp[i - 1][y - v[i]] + w[i]` - *Không* chọn vật thứ `i` => Cần lấy từ `i - 1` vật còn lại sao cho tổng giá trị của chúng không vượt quá `y`: `dp[i - 1][y]` => **Phương án tối ưu:** Chọn cách cho ra tổng khối lượng nhỏ hơn trong 2 cách nêu trên (do tổng khối lượng nhỏ hơn đảm bảo khó vượt qua giới hạn túi hơn): `dp[i][y] = min(dp[i - 1][y - v[i]] + w[i], dp[i - 1][y])` - **Đáp số:** Lặp biến j từ 1 đến V. Với mỗi giá trị j, ta lặp tìm `min(dp[i][j])` (với i từ 1 -> n). Đáp số của bài toán là giá trị j lớn nhất mà `min(dp[i][j]) <= W`. - **Độ phức tạp thời gian:** $O(n * V)$ - **Luyện tập cách cài đặt:** [Atcoder Educational DP Contest E - Knapsack 2](https://oj.vnoi.info/problem/atcoder_dp_e) :::spoiler **Code mẫu**: ```cpp= #include<bits/stdc++.h> using namespace std; int n; long long W; long long V; const long long INF = 1000000000000000000; long long w[102]; long long v[102]; long long dp[102][100002]; int main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> W; for(int i = 1; i <= n; ++i) cin >> w[i] >> v[i]; for(int i = 1; i <= n; ++i) V += v[i]; for(int i = 0; i <= n; ++i) { for(int y = 1; y <= V; ++y) dp[i][y] = INF; } for(int y = 1; y <= V; ++y){ for(int i = 1; i <= n; ++i){ if(v[i] > y) dp[i][y] = dp[i - 1][y]; else dp[i][y] = min(dp[i - 1][y - v[i]] + w[i], dp[i - 1][y]); } } long long ans = 0; for(int j = 1; j <= V; ++j){ long long mn = INF; for(int i = 1; i <= n; ++i) mn = min(mn, dp[i][j]); if(mn <= W) ans = j; } cout << ans; } ``` ::: - Tối ưu 1 chiều: - **Gọi** `dp[y]` là tổng khối lượng bé nhất của túi khi tổng giá trị là `y`. - **Trường hợp suy biến:** - `dp[0] = 0` - `dp[i] = INF` - **Công thức truy hồi:** - Lặp biến i từ 1 đến n. Trong mỗi lần lặp i, chạy biến y từ V đến 1 (tránh trường hợp trùng lắp): `dp[y] = min(dp[y], dp[y - v[i]] + w[i])` (nếu v[i] <= y) - **Đáp số:** Lặp biến j từ 1 đến V. Đáp số của bài toán là giá trị j lớn nhất mà `dp[j] <= W`. :::spoiler **Code mẫu**: ```cpp= #include<bits/stdc++.h> using namespace std; int n; long long W; long long V; const long long INF = 1000000000000000000; long long w[102]; long long v[102]; long long dp[100002]; int main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> W; for(int i = 1; i <= n; ++i) cin >> w[i] >> v[i]; for(int i = 1; i <= n; ++i) V += v[i]; for(int i = 1; i <= V; i++) dp[i] = INF; dp[0] = 0; for(int i = 1; i <= n; ++i){ for(int y = V; y >= 1; --y){ if(v[i] <= y) dp[y] = min(dp[y], dp[y - v[i]] + w[i]); } } long long ans = INF; for(int j = 1; j <= V; ++j){ if(dp[j] <= W) ans = j; } cout << ans; } ``` ::: # Bài tập vận dụng Các bài tập dưới đây ứng dụng các ý tưởng và công thức truy hồi ở trên và chúng không nhất thiết phải liên quan đến "cái túi". ## [CSES - Minimizing Coins](https://cses.fi/problemset/task/1634) ### Đề bài Một hệ thống tiền xu có $n$ mệnh giá ($1 \le n \le 100$), mệnh giá thứ $i$ là $c_i$ ($1 \le i \le n$, $1 \le c_i \le 10^6$). Tìm cách để tạo ra số tiền $x$ ($1 \le x \le 10^6$) sao cho số đồng xu được sử dụng là ít nhất. #### Input - Dòng đầu tiên gồm $n$ và $x$ ($1 \le n \le 100$, $1 \le x \le 10^6$) - Dòng tiếp theo gồm $n$ giá trị $c_i$ ($1 \le i \le n$, $1 \le c_i \le 10^6$) #### Output - Một dòng duy nhất là số đồng xu ít nhất để tạo ra được số tiền $x$. Nếu không có cách nào để tạo ra được số tiền $x$ thì in ra `-1` #### Sample Input ``` 3 11 1 5 7 ``` #### Sample Output ``` 3 ``` #### Giải thích Cách để tạo ra số tiền là `11` mà sử dụng ít đồng xu nhất là: sử dụng `2` đồng xu mệnh giá `5` và `1` đồng xu mệnh giá `1`, tổng cộng `3` đồng xu. ## [CSES - Coin Combinations I](https://cses.fi/problemset/task/1635) ### Đề bài Một hệ thống tiền xu có $n$ mệnh giá ($1 \le n \le 100$), mệnh giá thứ $i$ là $c_i$ ($1 \le i \le n$, $1 \le c_i \le 10^6$). Tính số cách khác nhau *(hai cách **sử dụng những đồng tiền giống nhau** nhưng **thứ tự sử dụng khác nhau** được coi là **khác nhau**)* để tạo ra số tiền $x$ ($1 \le x \le 10^6$) modulo $10^9+7$. #### Input - Dòng đầu tiên gồm $n$ và $x$ ($1 \le n \le 100$, $1 \le x \le 10^6$) - Dòng tiếp theo gồm $n$ giá trị $c_i$ ($1 \le i \le n$, $1 \le c_i \le 10^6$) #### Output - Một dòng duy nhất là số cách khác nhau để tạo ra số tiền $x$ modulo $10^9+7$. #### Sample Input ``` 3 9 2 3 5 ``` #### Sample Output ``` 8 ``` #### Giải thích `8` cách để tạo ra số tiền là `9` như sau: 1. `2+2+5` 2. `2+5+2` 3. `5+2+2` 4. `3+3+3` 5. `2+2+2+3` 6. `2+2+3+2` 7. `2+3+2+2` 8. `3+2+2+2` ## [CSES - Coin Combinations II](https://cses.fi/problemset/task/1636) ### Đề bài Một hệ thống tiền xu có $n$ mệnh giá ($1 \le n \le 100$), mệnh giá thứ $i$ là $c_i$ ($1 \le i \le n$, $1 \le c_i \le 10^6$). Tính số cách khác nhau *(hai cách **sử dụng những đồng tiền giống nhau** được coi là **giống nhau**, cho dù **thứ tự sử dụng khác nhau**)* để tạo ra số tiền $x$ ($1 \le x \le 10^6$) modulo $10^9+7$. #### Input - Dòng đầu tiên gồm $n$ và $x$ ($1 \le n \le 100$, $1 \le x \le 10^6$) - Dòng tiếp theo gồm $n$ giá trị $c_i$ ($1 \le i \le n$, $1 \le c_i \le 10^6$) #### Output - Một dòng duy nhất là số cách khác nhau để tạo ra số tiền $x$ modulo $10^9+7$. #### Sample Input ``` 3 9 2 3 5 ``` #### Sample Output ``` 3 ``` #### Giải thích `3` cách để tạo ra số tiền là `9` như sau: 1. `2+2+5` 2. `3+3+3` 3. `2+2+2+3` ## Bài tập thêm - [AtCoder - abc321F](https://atcoder.jp/contests/abc321/tasks/abc321_f) - [Codeforces - 687C](https://codeforces.com/contest/687/problem/C) - [Codeforces - 1862F](https://codeforces.com/contest/1862/problem/F) # Hướng dẫn tự học | Bước | Nội dung chi tiết | |:----:|:--------------------------------------------------------------------------------------------------------------------------------------- | | 1 | - Đọc tài liệu thật kĩ để hiểu nội dung.<br>- Khi gặp bài toán: thử dành vài phút đọc đề, viết ý tưởng ra nháp, suy nghĩ trước khi xem lời giải. | | 2 | - Giải bài tập dựa trên lý thuyết, suy nghĩ đưa về hướng giải của các bài quen thuộc.<br>- Xem ý tưởng và cách cài đặt (nếu cần). # Nguồn tham khảo [2SG - LCS + Knapsack](https://hackmd.io/@hphuong/Bya-QW-H3) [Bài toán cái túi và những ứng dụng xung quanh nó](https://viblo.asia/p/bai-toan-cai-tui-va-nhung-ung-dung-xung-quanh-no-maGK7Nke5j2) [Quy hoạch động cơ bản](https://vnoi.info/wiki/algo/dp/basic-dynamic-programming-1.md)