# DYNAMIC PROGRAMING ## 1. Mua sắm > ###### Tags: DP, Greedy. > https://codeforces.com/gym/508163/problem/F ### Subtask 1 : $n = 1$ Với $n = 1$ thì đáp án của ta là : $min(\lfloor\frac{k}{c_1}\rfloor, l_1)\times e_1$. ### Subtask 2, 3 : $n \le 100, l_i \le 10$. Quy hoạch động Knapsack cơ bản : * Với $f[i][j]$ là giá trị dinh dưỡng lớn nhất của một cách chọn khi đã xét đến loại thực phẩm thứ $i$ và đã tốn số tiền là $j$. * Hoặc với công thức $f[j]$ là giá trị dinh dưỡng lớn nhất của một cách chọn khi đã tốn số tiền là $j$. ### Subtask 6 : $n \le 10^5, l_i \le 10^9$. Ta gọi $vec[i]$ $(1 \le i \le k)$ là tập hợp giá trị dinh dưỡng các thực phẩm có giá bán là $i$. **Ví dụ** : * Ta có các loại thực phẩm $(c_i, e_i, l_i) : (2, 5, 3), (2, 4, 4)$ * Thì $vec[2]$ của ta là : ${4, 4, 4, 4, 5, 5, 5}$. **Nhận xét** : trong tập $vec[i]$ các thực phẩm đều có giá bán là $i$ nên ta ưu tiên các thực phẩm có giá trị ưu tiên từ cao đến thấp. Cụ thể là với mỗi tập $vec[i]$ ta chỉ chọn được số lượng thực phẩm tối đa là : $min(\lfloor\frac{k}{i}\rfloor, vec.size())$. Sau đó ta quay lại bài toán Knapsack cơ bản. **Code** : https://ideone.com/Ak3aZj. ## 2. Nông trại. > ###### Tags: DP, Greedy. > https://codeforces.com/group/hlS1ctLR9t/contest/508163/problem/I ### Subtask 1 : $n, Q, P_i \le 100$ Gọi $f[i][j]$ là giá trị lớn nhất khi xét đến ngày thứ $i$ và đã gieo $j$ hạt giống. ```cpp template <class T> inline bool maximize(T &a, const T &b) { return (a < b ? (a = b, 1) : 0); } int f[1005][1005], n, k, q, a[1005], p[1005]; void solve() { cin >> n >> k >> q; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) cin >> p[i]; for (int i = 1; i <= n; i++) { for (int j = 1; j <= p[i]; j++) f[i][j] = j * a[i]; for (int j = 0; j <= q; j++) maximize(f[i][j], f[i - 1][j]); if (i >= k) { for (int j = 1; j <= p[i]; j++) for (int x = j; x <= q; x++) maximize(f[i][x], f[i - k][x - j] + j * a[i]); } } int res = 0; for (int i = 0; i <= q; i++) maximize(res, f[n][i]); cout << res; } ``` Độ phức tạp : $O (n \times P \times Q)$. ### Subtask 2 : $n, Q, P_i \le 1000$ * Nhận xét : Khi xét đến ngày trồng thứ i thì : > Trạng thái $f[i][x]$ $(1 \le x \le q)$ phải được cập nhật từ trạng thái $f[i - k][y]$ $(y \le x)$ trước đó mà $x - y \le P_i$ : Vì số hạt giống tối đa có thể trồng ở ngày $i$ là $P_i$. > Trạng thái $f[i][x + 1]$ sẽ được cập nhật từ trạng thái $f[i - k][y]$ mà $x + 1 - y \le P_i$. > Tương tự ..... Ta duy trì một sẽ $std::multiset$ ```cpp if (i >= k) { multiset<int> s; for (int x = 1; x <= q; x++) { s.insert(f[i - k][x - 1] - (x - 1) * a[i]); if (x - p[i] - 1 >= 0) { auto it = s.lower_bound(f[i - k][x - p[i] - 1] - (x - p[i] - 1) * a[i]); s.erase(it); } auto mx = s.end(); --mx; maximize(f[i][x], *(mx) + x * a[i]); } } ``` Độ phức tạp : $O (n \times Q \times log2(Q))$. ### Subtask 4 : $n, Q, P_i \le 10000$ Tương tự nhận xét từ subtask $2$ nhưng thay vì dùng $std::multiset$ thì ta sẽ dùng $deque$ (hàng đợi hai đầu) để làm công việc tương tự. ```cpp if (i >= k) { deque<int> pq; for (int x = 1; x <= q; x++) { while (pq.size() && f[i - k][x - 1] - (x - 1) * a[i] >= f[i - k][pq.back()] - (pq.back()) * a[i]) pq.pop_back(); pq.push_back(x - 1); while (pq.front() <= x - p[i] - 1) pq.pop_front(); maximize(f[i][x], f[i - k][pq.front()] - pq.front() * a[i] + x * a[i]); } } ``` Độ phức tạp : $O (n \times Q)$ ### Subtask 3 : $P_1 = P_2 =... = P_n$ * Nhận xét : Với việc $P_i$ bằng nhau, ta thấy rằng để tối đa giá trị cần tìm, thì tập các ngày cần trồng hạt giống thường cố gắng trồng đủ $P$ hạt, trừ khi sau khi trồng xong thì vẫn còn dư hạt. * Gọi $f[i][j]$ là giá trị lớn nhất khi xét đến ngày thứ $i$ và đã gieo $j$ hạt giống. * Nhưng bây giờ với mỗi ngày $i$ ta chỉ có những trạng thái như sau : > Không trồng hạt nào. > Gieo trồng đúng $P$ hạt. > Hoặc gieo trồng $Q % P$ hạt. **Code** : https://ideone.com/zC6Z0p. ## 3. Đóng gói. > #### Tags: DP, Greedy. > https://codeforces.com/group/hlS1ctLR9t/contest/508163/problem/H ### Subtask 1 : $L \le 10^9, N \le 20$ * Với $N \le 20$ ta có thể sinh dãy nhị phân độ dài $n$. Mỗi dãy nhị phân được sinh ra, ta thêm lần lượt các món hàng vào thùng, với phần tử $0$ được xếp vào thùng $1$, phần từ $1$ được xếp vào thùng $2$ hoặc có thể ngược lại. * Độ phức tạp : $O (2^N \times N)$. ### Subtask 2 : $1 \le a_i \le 2; L = 100; N \le 10^6$ * Gọi $sum = a_1 + a_2 + ... + a_n$ * Đáp án sẽ là : $\lfloor (\frac{sum}{L}) \rfloor$ + $sum$ mod $L$ $ > 0$ * Độ phức tạp : $O(N)$ ### Subtask 3 : $L \le 30; N \le 10^4$ * Đặt mảng quy hoạch động : $f[i][x][y]$ là số thùng ít nhất dùng để khi xét đến món hàng thứ $i$ và hiện tại lúc đó thúng $1$ đã chứa $x$ đơn vị cân nặng, thùng $2$ chứa $y$ đơn vị cân nặng. ```cpp void sub3() { memset(f, 0x3f, sizeof (f)); f[0][0][0] = 2; for (int i = 1; i <= n; i++) for (int l1 = 0; l1 <= lim; l1++) for (int l2 = 0; l2 <= lim; l2++) { if (a[i] + l1 <= lim) minimize(f[i][a[i] + l1][l2], f[i - 1][l1][l2]); if (a[i] + l2 <= lim) minimize(f[i][l1][a[i] + l2], f[i - 1][l1][l2]); minimize(f[i][a[i]][l2], f[i - 1][l1][l2] + 1); minimize(f[i][l1][a[i]], f[i - 1][l1][l2] + 1); } for (int l1 = 0; l1 <= lim; l1++) for (int l2 = 0; l2 <= lim; l2++) minimize(res, f[n][l1][l2]); cout << res << '\n'; } ``` ### Subtask 4 : $L \le 5000; N \le 100$ * Nhận xét : Với $N \le 100$ thì số thùng cần dùng của ta cũng chỉ tối đa $100$ mà thôi! * Nên ta có thể đặt mảng quy hoạch động là : $f[i][cnt][x]$ trả về $y$ nhỏ nhất sao cho tồn tại $1$ cách xếp $i$ món hàng đầu tiên vào đúng $cnt$ thùng, trong đó hiện tại thùng $1, 2$ đang chứa lần lượt là $x, y$. Giả sử đang ở trạng thái $f[i - 1][cnt][x]$ thì công thức quy hoạch động của ta của ta là : * Nếu có thể thêm món hàng $i$ vào thùng $1$ (nếu có thể) thì : $f[i][cnt][x + a[i]] = f[i - 1][cnt][x]$ * Nếu có thể thêm món hàng $i$ vào thùng $2$ (nếu có thể) thì : $f[i][cnt][x] = f[i - 1][cnt][x] + a[i]$ * Thay thùng 1 bằng một thùng mới : $f[i][cnt + 1][a[i]] = f[i - 1][cnt][x]$. * Thay thùng 2 bằng một thừng mơi : $f[i][cnt + 1][x] = a[i]$ ### Subtask 5 : $L \le 5000; N \le 10^4$ * Thay đổi từ subtask 4, ta đặt mảng quy hoạch động : **pair<int, int>** $f[i][x] = (cnt, y)$ khi xét tới món hàng thứ $i$ và thùng thứ $1$ chứa $x$ thì thùng thứ $y$ và số thùng ít nhất là $cnt$. ```cpp void sub45() { for (int i = 0; i <= n; i++) for (int j = 0; j <= lim; j++) f[i][j] = {INF, 0}; f[0][0] = {2, 0}; // Chu y so thung duoc su dung luon luon >= 2 for (int i = 1; i <= n; i++) { for (int j = 0; j <= lim; j++) { if (j + a[i] <= lim) f[i][j + a[i]] = min(f[i][j + a[i]], f[i - 1][j]); // Them mon hang thu i vao thung thu 1 neu co the f[i][a[i]] = min(f[i][a[i]], make_pair(f[i - 1][j].first + 1, f[i - 1][j].second)); // Dong thung 1 va mo mot thung moi thay the thung 1 if (f[i - 1][j].second + a[i] <= lim) f[i][j] = min(f[i][j], make_pair(f[i - 1][j].first, f[i - 1][j].second + a[i])); // Them mon hang thu i vao thung thu 2 neu co the f[i][j] = min(f[i][j], make_pair(f[i - 1][j].first + 1, a[i])); // Dong thung 2 va mo mot thung moi thay the thung 2 if (i == n) minimize(res, f[n][j].first); } } cout << res; } ``` **Code** : https://ideone.com/WHD3JT ## 4. Kho báu. > #### Tags: DP, bitset. > https://codeforces.com/group/hlS1ctLR9t/contest/508163/problem/G ### Subtask 1 : $n \le 10$ Ta gọi hàm đệ quy : $back(l, x)$ có nghĩa là đã xét đến rương thứ $i$ và đã mở được đến rương thứ $x$. ```cpp int back(int l, int x) { if (l > x || l > n) return 0; int res = 0; maximize(res, a[l] + back(l + 1, x)); maximize(res, back(l + 1, min(n, x + a[l]))); return res; } ``` ### Subtask 2 : $n \le 1000$. * Ta gọi hàm quy hoạch động $f[i][x]$ là số tiền lớn nhất thu được khi đang xét đến rương thứ $i$ và đã mở được đến rương thứ $x$. ```cpp int back(int l, int x) { if (l > x || l > n) return 0; int &res = f[l][x]; if (res != -1) return res; res = 0; maximize(res, a[l] + back(l + 1, x)); maximize(res, back(l + 1, min(n, x + a[l]))); return res; } void solve() { memset(f, -1, sizeof (f)); cout << back(1, 1); } ``` ### Subtask3 : $n \le 10 ^ 5$. * Nhận xét : Để mở được rương thứ $i$ ta cần chọn ra một tập hợp các rương có chỉ số $j < i$ sao cho tổng các rương này lớn hơn hoặc bằng $i - 1$. Để khi xét đến rương thứ $i$ được kết quả tối ưu, ta sẽ tìm ra một tập rương như đã nói mà có tổng $sum$ nhỏ nhất lớn hơn hoặc bằng $i - 1$, thì kết quả của ta thu được tại $i$ là $a_1 + a_2 + .. + a_i - sum$. * Ta có thể dùng Knapsack thông thường để tính các giá trị $sum$ có tồn tại hay không ?. Nhưng bằng cách này sẽ không thể AC khi $n \le 10^5$. Thay vào để ta có thể tính Knapsack bằng việc dùng $std::bitset$ * Với mỗi bit thứ $i$ trên bitset đại dương cho giá trị $sum = i$ đó có tồn tại hay không ? * Ví dụ trong ta đang tồn tại các $sum = (0, 1, 3, 5)$. Thì bitset của ta sẽ là $000101011$ (được đánh số từ phải sang trái, 0, 1, 2.... ). Nếu ta thêm một phần tử $x = 3$ thì rõ ràng tập $sum = (3, 4, 6, 8)$, bitset của ta là $00101011000$ * Cách thêm **bitset |= bitset << (x)** ```cpp bitset<MAXN * 2> g; void solve() { g[a[1]] = true; int res = a[1], j = 1; // biến j đến duy trì sum = j nhỏ nhất thỏa mãn. int sum = a[1]; g[0] = false; for (int i = 2; i <= n; i++) { sum[i] += a[i]; g |= (g << (a[i])); // update tập thỏa mãn maximize(j, i - 1); while (!g[j] && j < n * 2) ++j; // tìm sum = j nhỏ nhất thỏa mãn g[j] = 1. // để không bị tràn giá trị của sum khi ta dịch bit nên ta sẽ cho sum thỏa trong khoảng <= 2 * n. if (!g[j]) break; // nếu không sum = j không tồn tại, thì những sum > j sẽ không tồn tại maximize(res, sum[i] - j); // cập nhật kết quả. g[i - 1] = false;// vì như đã nói khi xét đến rương i thì cần phải mở i - 1 rương // nên khi xét đến rương i ta đều phải bỏ đi tất cả các sum < i - 1. } cout << res << '\n'; } ``` **Code :** https://ideone.com/JhQ24r.