背包问题

01背包问题

每件物品最多只能用一次。

int n, m;
int v[N], w[N];
int f[N];

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++){
        for(int j = m; j >= v[i]; j--){
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
}

图片说明

完全背包问题

每件物品有无数个。

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++){
        for(int j = v[i]; j <= m; j++){
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
}

图片说明
对比:第一个式子是01背包的状态转移,第二个是完全背包的。
图片说明

多重背包问题

每个物品有s[i]个。

朴素算法

const int N = 110;

int n, m;
int v[N], w[N], s[N];
int f[N][N];

int main(){
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];

    for(int i = 1; i <= n; i++){
        for(int j = m; j >= 0; j--){
            for(int k = 0; k <= s[i] && k * v[i] <= j; k++){
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
            }
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

优化:把si拆分为1,2,4,...,2^k,c,然后转化为01背包问题。

const int N = 11010, M = 2010;

int n, m;
int v[N], w[N];
int f[M];

int main(){
    cin >> n >> m;
    int cnt = 0;
    for (int i = 1; i <= n; i ++ ){
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;
        while(k <= s){
            cnt++;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if(s > 0){
            cnt++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }
    n = cnt;
    for(int i = 1; i <= n; i++){
        for(int j = m; j >= v[i]; j--){
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
}

分组背包问题

有N组,每组有若干种物品,每组只能选一个。
图片说明

int n, m;
int v[N][N], w[N][N], s[N];
int f[N];

int main(){
    cin >> n >> m;
    int cnt = 0;
    for (int i = 1; i <= n; i++){
        cin >> s[i];
        for(int j = 1; j <= s[i]; j++){
            cin >> v[i][j] >> w[i][j];
        }
    }

    for(int i = 1; i <= n; i++){
        for(int j = m; j >= 0; j--){
            for(int k = 1; k <= s[i]; k++){
                 if(v[i][k] <= j) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
            }

        }
    }

//朴素版:存的时候从1~s[i]存,然后比较的时候第三层循环从0~s[i]这样,当 k=0的时候,相当于就是f[i][j] = f[i - 1][j]了。
//     for(int i = 1; i <= n; i++)
//         for(int j = 0; j <= m; j++)
//             for(int k = 0; k <= s[i]; k++)
//                 if(j >= v[i][k]) f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
    cout << f[m] << endl;
    return 0;
}

线性DP

数字三角形
图片说明
最长上升子序列
O(n^2)
图片说明
最长上升子序列Ⅱ
优化时间复杂度到O(nlogn)
https://www.acwing.com/video/328/

int n;
int a[N], q[N];

int main(){
    cin >> n;
    for (int i = 0; i < n; i++) cin >> a[i];  

    int len = 0;
    for(int i = 0; i < n; i++){
        int l = 0, r = len;
        while(l < r){
            int mid = l + r + 1 >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1);
        q[r + 1] = a[i];
    }
    printf("%d\n", len);
    return 0;
}

最长公共子序列

int n, m;
char a[N], b[N];
int f[N][N];

int main(){
    cin >> n >> m;
    int cnt = 0;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= m; i++) cin >> b[i];

    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
        }
    }
    printf("%d\n", f[n][m]);
    return 0;
}

最短编辑距离
https://www.acwing.com/video/334/
图片说明

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    cin >> m;
    for (int i = 1; i <= m; i++) cin >> b[i];

    //初始化.当a、b有一个长度为0时,只能通过加或删达到匹配
    for (int i = 0; i <= m; i ++ ) f[0][i] = i;
    for (int i = 0; i <= n; i ++ ) f[i][0] = i;

    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
        }
    }
    printf("%d\n", f[n][m]);
    return 0;
}

编辑距离
https://www.acwing.com/video/331/
图片说明

区间DP

石子合并问题
图片说明
转移方程
图片说明

#include <bits/stdc++.h>

using namespace std;

const int N = 310;

int n;
int s[N];
int f[N][N];

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> s[i];
    for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1]; //前缀和 

    for(int len = 2; len <= n; len++){
        for(int i = 1; i + len - 1 <= n; i++){
            int l = i, r = i + len - 1;
            f[l][r] = 1e8;
            //k枚举的是左半边最后一个石子的位置
            for (int k = l; k < r; k ++ )
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }
    }
    printf("%d\n", f[1][n]);

    return 0;
}

计数类DP

整数划分 https://www.acwing.com/video/332/
方法一:
可以看作 容量为n,物品体积为1,2,3,...,n的完全背包问题,即每个数字可以选多次。
f(i,j)表示第1-i种物品的体积为j。第i种物品可以选任意个
图片说明
图片说明

int n;
int f[N]; //表示只从1~i中选,且总和等于j的方案数

int main(){
    cin >> n; 
    f[0] = 1; 
    for(int i = 1; i <= n; i++){
        for(int j = i; j <= n; j++){
            f[j] = (f[j] + f[j - i]) % mod;
        }
    }
    printf("%d\n", f[n]);

    return 0;
}

方法二:
图片说明

int n;
int f[N][N]; //f[i][j]表示总和为i,总个数为j的方案数

int main(){
    cin >> n; 
    f[1][1] = 1;   
    for(int i = 2; i <= n; i++){
        for(int j = 1; j <= i; j++){
            f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;
        }
    }
    int res = 0;
    for(int i = 1; i <= n; i++) res = (res + f[n][i]) % mod;
    printf("%d\n", res);

    return 0;
}

数位统计DP

图片说明
图片说明
https://www.acwing.com/solution/content/52109/

#include <bits/stdc++.h>

using namespace std;

const int N = 10;

/*

001~abc-1, 999

abc
    1. num[i] < x, 0
    2. num[i] == x, 0~efg
    3. num[i] > x, 0~999

*/

int get(vector<int> num, int l, int r){
    int res = 0;
    for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
    return res;
}

int power10(int x){
    int res = 1;
    while (x -- ) res *= 10;
    return res;
}

int count(int n, int x){
    if (!n) return 0;
    vector<int> num;
    while (n){
        num.push_back(n % 10);
        n /= 10;
    }
    n = num.size();

    int res = 0;
    for (int i = n - 1 - !x; i >= 0; i -- ){
        if (i < n - 1){
            res += get(num, n - 1, i + 1) * power10(i);
            if (!x) res -= power10(i);
        }
        if (num[i] == x) res += get(num, i - 1, 0) + 1;
        else if (num[i] > x) res += power10(i);
    }
    return res;
}


int main(){
    int a, b;
    while (cin >> a >> b){
        if(a == 0 && b == 0) break;
        if (a > b) swap(a, b);
        for (int i = 0; i <= 9; i ++ )
            cout << count(b, i) - count(a - 1, i) << ' ';
        cout << endl;
    }    
    return 0;
}

状态压缩DP

图片说明

const int N = 12, M = 1 << N;

int n, m;
long long f[N][M];
bool st[M];

int main(){
    while (cin >> n >> m){
        if(n == 0 && m == 0) break;
        for (int i = 0; i < 1 << n; i ++ ){
            int cnt = 0;
            st[i] = true;
            for (int j = 0; j < n; j ++ )
                if (i >> j & 1){
                    if (cnt & 1) st[i] = false;
                    cnt = 0;
                }
                else cnt ++ ;
            if (cnt & 1) st[i] = false;
        }

        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )
            for (int j = 0; j < 1 << n; j ++ )
                for (int k = 0; k < 1 << n; k ++ )
                    if ((j & k) == 0 && st[j | k])
                        f[i][j] += f[i - 1][k];

        cout << f[m][0] << endl;
    }
    return 0;
}

这里还有状态压缩的写法,之后再看看
https://www.acwing.com/activity/content/code/content/64200/
最短Hamilton路径
图片说明

    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;
    for(int i = 0; i < 1 << n; i++){
        for(int j = 0; j < n; j++){
            if(i >> j & 1){
                for(int k = 0; k < n; k++){
                    if(i >> k & 1){
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
                    }
                }
            }
        }
    }

树形DP

图片说明

图片说明

#include <bits/stdc++.h>

using namespace std;

const int N = 6010;

int n;
int h[N], e[N], ne[N], idx;
int happy[N];
int f[N][2];
bool has_fa[N];

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u){
    f[u][1] = happy[u];
    for (int i = h[u]; ~i; i = ne[i]){ // ~i ,等价于 i != -1
        int j = e[i];
        dfs(j);
        f[u][1] += f[j][0];
        f[u][0] += max(f[j][0], f[j][1]);
    }
}

int main(){
    cin >> n; 
    for (int i = 1; i <= n; i++) cin >> happy[i];
    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i++){
        int a, b;
        cin >> a >> b;
        add(b, a);
        has_fa[a] = true;
    }
    int root = 1;
    while (has_fa[root]) root++;
    dfs(root);
    printf("%d\n", max(f[root][0], f[root][1]));
    return 0;
}

记忆化搜索

图片说明

int n, m;
int g[N][N];
int f[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

int dp(int x, int y){
    int &v = f[x][y]; // 代表两者等价
    if(v != -1) return v;
    v = 1;
    for(int i = 0; i < 4; i++){
        int a = x + dx[i], b = y + dy[i];
        if(a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b])
            v = max(v, dp(a, b) + 1);
    } 
    return v;
}

int main(){
    cin >> n >> m; 
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            cin >> g[i][j];

    memset(f, -1, sizeof f);
    int res = 0;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            res = max(res, dp(i, j));

    printf("%d\n", res);
    return 0;
}