## Bài 1: [Cái túi 3](https://marisaoj.com/problem/156) - Do vật thứ $i$ có thể được dùng tối đa $a_i$ lần, ta tạo ra $a_i$ vật thứ $i$ và làm dp knapsack như bình thường, mỗi vật chỉ được chọn tối đa một lần. Nhưng thế này thì TLE. - Vậy làm sao để vật $i$ thành các một số các vật khác $T_i$ sao cho nếu chọn $0 < x \le a_i$ lần vật $i$, luôn có cách để tạo ra nó bằng một tập con của $T_i$. Một vật mới bằng $p$ lần vật $i$ sẽ có khối lượng $w_i \times p$ và giá trị $v_i \times p$: + Với $a_i=2^k-1$, ta có thể tách thành $k$ vật $2^0,2^1,...,2^{k-1}$. Luôn có cách để phân tích $x$ thành tổng các lũy thừa cơ số $2$. + Với $a_i=2^k-1+t$, ta tách thành $k+1$ vật $2^0,2^1,...,2^{k-1},t$. Với $0 < x \le 2^k-1$, ta phân tích như bình thường. Với $2^k - 1 < x \le a_i$, ta phân tích $x-t$ như trước đó rồi thêm vật $t$ vào. - Phân tích mỗi vật $i$ thành $\log {a_i}$ vật như trên, làm dp knapsack như bình thường. Code: :::spoiler ```c++ #include <bits/stdc++.h> using namespace std; const int MAXW = 10005, MAXN = 1005; int n, k, dp[MAXW]; struct jew{ int w; int val; }; vector<jew> jewel; void split(int w, int v, int a){ int cur = 1; while(cur <= a){ jewel.push_back({w * cur, v * cur}); a -= cur; cur *= 2; } if(a) jewel.push_back({w * a, v * a}); } int main(){ cin >> n >> k; for(int i = 0; i < n; i++){ int a, b, c; cin >> a >> b >> c; split(a, b, c); } for(int i = 0; i < jewel.size(); i++){ for(int j = k; j >= jewel[i].w; j--){ dp[j] = max(dp[j], dp[j - jewel[i].w] + jewel[i].val); } } // for(int i = 0; i < jewel.size(); i++) // cout << jewel[i].val << ' ' << jewel[i].w << endl; int res = 0; for(int i = 0; i <= k; i++){ //cout << dp[i] << ' '; res = max(res, dp[i]); } cout << res; } ``` ::: ## Bài 2: [Đất](https://marisaoj.com/problem/367) ### **Cách dễ hơn (chậm hơn)** - Đặt $c_i = a_i-b_i$. - Nếu $c_i$ lớn hơn $0$, thêm $c_i$ phần tử $i$ vào mảng $A'$, ngược lại thêm $-c_i$ phần tử $i$ vào mảng $B'$. Nghĩa là mảng $A'$ là những vị trí cần thêm, còn $B'$ là cần bớt. - Trạng thái DP $f(i,j)$ là chi phí nhỏ nhất để xử lí xong $A'[1...i]$ và $B'[1...j]$. Chuyển nhãn $f(i,j) = min$ + $f(i-1,j-1) + |A'_i-B'_j| \times z$, tương ứng với việc chuyển $1$ đất giữa $A_{A'_i}$ và $A_{B'_j}$. + $f(i-1,j) + x$, thêm đất vào $A_{A'_i}$. + $f(i,j-1)+y$, bỏ đất khỏi $A_{B'_j}$. Code: :::spoiler ```c++ #include <bits/stdc++.h> using namespace std; #define oo 2e18 #define mod 1000000007 #define int long long #define inf32 0x3f3f3f3f #define inf64 0x3f3f3f3f3f3f3f3f #define get(x, i) (((x) >> (i)) & 1) #define pi pair<int, int> #define all(x) x.begin(), x.end() #define Log2(x) 63 - __builtin_clzll(x) #define set0(d) memset(d, 0, sizeof d) #define setneg(d) memset(d, -1, sizeof d) #define setinf(d) memset(d, inf32, sizeof d) #define Unique(v) v.erase(unique(all(v)), v.end()) #define FILENAME "f" const int maxn = 5005; int n, x, y, z; int dp[maxn][maxn]; vector<int> a, b; signed main(){ ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n >> x >> y >> z; a.push_back(0), b.push_back(0); for(int i = 1; i <= n; i++){ int u, v; cin >> u >> v; if(u > v) for(int j = 1; j <= u - v; j++) b.push_back(i); else if(u < v) for(int j = 1; j <= v - u; j++) a.push_back(i); } setinf(dp); dp[0][0] = 0 ; for(int i = 1; i < a.size(); i++) dp[i][0] = i * x; for(int i = 1; i < b.size(); i++) dp[0][i] = i * y; for(int i = 1; i < a.size(); i++){ for(int j = 1; j < b.size(); j++){ dp[i][j] = min({z * abs(a[i] - b[j]) + dp[i - 1][j - 1], dp[i - 1][j] + x, dp[i][j - 1] + y}); } } cout << dp[a.size() - 1][b.size() - 1]; } ``` ::: ### **Cách khó hơn** - Có các thao tác tăng $c_i$ lên $1$ hoặc $-1$, chuyển $1$ từ $c_i$ sang $c_{i-1}$ hoặc $c_{i+1}$. Làm sao để các phần tử $c_i$ thành $0$ hết. - Trạng thái DP: $f(i, k)$, chi phí tối thiểu khi $c_j=0$ với $j \le i$, và đang "cầm" $k$ đất. - Để đơn giản, hãy xét trường hợp chỉ được chuyển đất từ $i$ lên $j$ với $j>i$. Ở mỗi vị trí $i$, bạn được cầm lên một số đất, và có thể dùng nó để rải xuống các vị trí $>i$. Chuyển nhãn từ $f(i, k)$ lên $f(i+1,k+t)$ với $t$ là số đất cầm thêm. - Trường hợp còn lại là, "đặt trước" đất vào $a_i$, và ở chỗ nào đó từ $i+1$ trở đi, phải lấy đất bù vào đây. Chuyển nhãn từ $f(i,k)$ lên $f(i+1,k-t)$ với $t$ số đất đặt trước. - Trường hợp lấy thêm hoặc giảm đất thì tăng, giảm $k$. - Đáp án là $f(n,0)$. Code: :::spoiler ```c++ #include <bits/stdc++.h> using namespace std; #define ll long long #define fi first #define sc second const ll BASE = 1e4; ll x, y, z, n; ll a[1001] {}; ll dp[501][30001] {}; ll sum = 0; void inp() { cin >> n >> x >> y >> z; for (int i = 1; i <= n; i++) { ll u, v; cin >> u >> v; a[i] = u - v; } sum = 1e4; } void solve() { inp(); memset(dp, 0x3f, sizeof(dp)); dp[0][BASE] = 0; for (int i = 1; i <= n; i++) { for (int j = -sum; j <= sum; j++) { dp[i][j + BASE] = min(dp[i][j + BASE], dp[i - 1][j - a[i] + BASE] + abs(j - a[i]) * z); } for (int j = -sum; j <= sum; j++) { dp[i][j + BASE] = min(dp[i][j + BASE], dp[i][j - 1 + BASE] + x); } for (int j = sum; j >= -sum; j--) { dp[i][j + BASE] = min(dp[i][j + BASE], dp[i][j + 1 + BASE] + y); } } cout << dp[n][BASE]; } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); solve(); } ``` ::: ## Bài 3: [Xếp hộp](https://marisaoj.com/problem/368) - Đề bài bảo các hộp chồng lên nhau không bắt buộc thứ tự từ $1$ đến $n$. Nếu có thứ tự thì dễ quá, nên ta sẽ làm cách nào để tìm được thứ tự của các vật để DP siêu dễ. - Sort các thùng theo $w_i+c_i$, thùng nào có giá trị này lớn hơn được xếp ở dưới. - Chứng minh: Với hai vật $i,j$, nếu $w_i+c_i>w_j+c_j$, ta chứng minh nếu xếp $j$ dưới $i$ sẽ không tối ưu bằng $i$ trên $j$: + Nếu thùng $i$ nằm dưới $j$, tải trọng tối đa của các thùng ngoài $j$ ra là $c_i-w_j$. + Nếu thúng $j$ nằm dưới $i$, tải trọng tối đa của các thùng ngoài $i$ ra là với $j$ nằm dưới $i$ là $c_j-w_i$. + $w_i+c_i>w_j+c_j \Leftrightarrow c_j-w_i<c_i-w_j$ nên để $i$ nằm dưới $j$ sẽ tối ưu hơn. Code :::spoiler ```c++ #include <bits/stdc++.h> using namespace std; #define pi pair<int, int> #define inf32 0x3f3f3f3f #define inf64 0x3f3f3f3f3f3f3f3f #define all(x) x.begin(), x.end() #define Unique(v) v.erase(unique(all(v)), v.end()); #define int long long #define setinf(d) memset(d, inf32, sizeof d) #define setneg(d) memset(d, -1, sizeof d) #define set0(d) memset(d, 0, sizeof d) #define Log2(x) 63 - __builtin_clzll(x) #define oo 2e18 #define mod 1000000007 #define FILENAME "f" const int maxn = 5005; int n; pi a[maxn]; int dp[maxn][maxn], ans = 0; #define w first #define limit second inline void mini(int &a, int b){ a = min(a, b); } signed main(){ ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0); setinf(dp); cin >> n; for(int i = 1; i <= n; i++) cin >> a[i].w; for(int i = 1; i <= n; i++) cin >> a[i].limit; sort(a + 1, a + n + 1, [&](const pi &a, const pi &b){ if(a.w + a.limit == b.w + b.limit) return a.w < b.w; return a.w + a.limit > b.w + b.limit; }); setinf(dp); for(int i = n; i >= 1; i--){ dp[i][1] = min(dp[i][1], a[i].w); for(int j = 1; j <= n; j++){ mini(dp[i - 1][j], dp[i][j]); } for(int j = 1; j <= n - i + 1; j++){ if(dp[i][j] == inf64) continue; if(dp[i][j] <= a[i - 1].limit) mini(dp[i - 1][j + 1], dp[i][j] + a[i - 1].w); } } for(int i = n; i >= 1; i--){ if(dp[1][i] != inf64) return cout << i , 0; } cout << 0; } ``` :::