---
# System prepended metadata

title: Chữa bài DP 01/08/023
tags: [dp]

---

## 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;
}
```
:::