You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

背包问题

一、01背包基础题

AcWing 2. 01背包问题

AcWing 423. 采药

AcWing 1024. 装箱问题

二维状态表示

#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = 1010;

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

int main() {
    cin >> m >> n;

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

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            f[i][j] = f[i - 1][j]; // 不选
            if (j >= v[i])
                f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); // 选
        }
    printf("%d\n", f[n][m]);
    return 0;
}

一维状态表示

#include <bits/stdc++.h>

using namespace std;
const int N = 1010;

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

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

    // 01背包模板
    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]);

    printf("%d\n", f[m]);
    return 0;
}

二、二维费用01背包问题

AcWing 1022. 宠物小精灵之收服

AcWing 8. 二维费用的背包问题

#include <bits/stdc++.h>

using namespace std;

const int N = 110;   // 野生小精灵的数量
const int M1 = 1010; // 小智的精灵球数量
const int M2 = 510;  // 皮卡丘的体力值

int n, m1, m2;
int f[M1][M2]; // 一维:精灵球数量,二维:皮卡丘的体力值,值:抓到的小精灵数量最大值

int main() {
    cin >> m1 >> m2 >> n;
    m2--; // 留一滴血

    // 二维费用01背包
    // 降维需要将体积1、体积2倒序枚举
    for (int i = 1; i <= n; i++) {
        int v1, v2;
        cin >> v1 >> v2;
        for (int j = m1; j >= v1; j--)
            for (int k = m2; k >= v2; k--)
                f[j][k] = max(f[j][k], f[j - v1][k - v2] + 1); // 获利就是多了一个小精灵
    }
    // 最多收服多少个小精灵[在消耗精灵球、血极限的情况下,肯定抓的是最多的,这不废话吗]
    printf("%d ", f[m1][m2]);

    // 找到满足最大价值的所有状态里,第二维费用消耗最少的
    int cost = m2;
    for (int i = 0; i <= m2; i++) // 如果一个都不收服则体力消耗最少消耗值为0
        if (f[m1][i] == f[m1][m2])
            cost = min(cost, i);

    // 收服最多个小精灵时皮卡丘的剩余体力值最大是多少
    printf("%d\n", m2 + 1 - cost);
    return 0;
}

总结

  • 01背包,还是背一维的形式比较好,一来代码更短,二来空间更省,倒序就完了。
  • 二维费用的01背包,简化版本的01背包模板就有了用武之地,因为三维数组可能会爆内存。

三、01背包之恰好装满

AcWing 278. 数字组合

二维代码

#include <bits/stdc++.h>

using namespace std;
const int N = 110;
const int M = 10010;
int n, m;
int v;
int f[N][M];

int main() {
    cin >> n >> m;

    for (int i = 0; i <= n; i++) f[i][0] = 1; // base case

    for (int i = 1; i <= n; i++) {
        cin >> v;
        for (int j = 1; j <= m; j++) {
            // 从前i-1个物品中选择装满j这么大的空间假设方案数是5个
            // 那么在前i个物品中选择装满j这么大的空间方案数最少也是5个
            // 如果第i个物品可以选择那么可能使得最终的选择方案数增加
            f[i][j] = f[i - 1][j];
            // 增加多少呢前序依赖是f[i - 1][j - v]
            if (j >= v) f[i][j] += f[i - 1][j - v];
        }
    }
    // 输出结果
    printf("%d\n", f[n][m]);
    return 0;
}

一维代码

#include <bits/stdc++.h>

using namespace std;
const int N = 10010;

int n, m;
int v;
int f[N]; // 在前i个物品体积是j的情况下恰好装满的方案数

int main() {
    cin >> n >> m;

    // 体积恰好j, f[0]=1, 其余是0
    f[0] = 1;
    for (int i = 1; i <= n; i++) {
        cin >> v;
        for (int j = m; j >= v; j--)
            f[j] += f[j - v];
    }
    printf("%d\n", f[m]);
    return 0;
}

四、完全背包

AcWing 3. 完全背包问题

二维写法

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int f[N][N];

int main() {
    cin >> n >> m;

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

一维解法

#include <bits/stdc++.h>

using namespace std;
const int N = 1010;
int n, m;
int f[N];

// 完全背包问题
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int v, w;
        cin >> v >> w;
        for (int j = v; j <= m; j++)
            f[j] = max(f[j], f[j - v] + w);
    }
    printf("%d\n", f[m]);
    return 0;
}

五、完全背包之恰好装满

AcWing 1023. 买书

二维数组

#include <bits/stdc++.h>

using namespace std;
const int N = 1010;
int v[5] = {0, 10, 20, 50, 100};
int f[5][N];

int main() {
    int m;
    cin >> m;
    // 前0种物品体积是0的情况下只有一种方案
    f[0][0] = 1;
    for (int i = 1; i <= 4; i++)
        for (int j = 0; j <= m; j++) {
            f[i][j] = f[i - 1][j];
            if (v[i] <= j) f[i][j] += f[i][j - v[i]];
        }
    printf("%d\n", f[4][m]);
    return 0;
}

一维数组

#include <bits/stdc++.h>

using namespace std;
const int N = 1010;
int v[5] = {0, 10, 20, 50, 100};
int f[N];

// 体积限制是恰好是因此需要初始化f[0][0]为合法解1其他位置为非法解0。
int main() {
    int m;
    cin >> m;
    // 前0种物品体积是0的情况下只有一种方案
    f[0] = 1;
    for (int i = 1; i <= 4; i++)
        for (int j = v[i]; j <= m; j++)
            f[j] += f[j - v[i]];
    // 输出
    printf("%d\n", f[m]);
    return 0;
}

总结 ① 完全背包的经典优化是哪个混蛋想出来的,真它娘的是个人才。 ② 对比01背包与完全背包的代码,发现是一正一反。 ③ 完全背包求最大值与恰好装满的方案数,除了初始化不同,其它的一样。f[i][0]=1这样的初始化,我也服了~ ④ 背包问题这样的经典代码,除了理解算法原理,会推导外,重点还是模板背诵。用模板知识解决实际问题才是考试的本质,虽然考试不一定能选拔出能力强的人才,但能选拔出做过这方面训练的人员。

AcWing 1021. 货币系统

完全背包之恰好装满,只不过int装不下,需要开long long,捎带着学习一下隔板法,应对一下高中数学组合数学。

十年OI一场空,不开long long见祖宗。

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;

const int N = 20;
const int M = 3010;
int n, m;
LL v[N];
LL f[M];

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

AcWing 532. 货币系统

总结 非常经典的NOIP题目!一般都是需要自己挖掘一些性质,然后再用代码模板去解题,挖掘的性质一般靠经验去猜。

常见的套路包括:排序,按数对左端点排序啥的。

排序完成后,逐个遍历一下,看看这个数字能不能用它前面的数字表示出来(完全背包+恰好装满),如果能的话,说明这个数字可以扔掉,因为扔掉后依然可以靠比它小的构造出来,如果不能就必须保留下来,最终统计一下数字个数就行了。

本题最后一个细节:不能跑多次DP,性能差,需要只跑一次DP,可以使用求组成方案数,如果只有一种方案,即f[i]=1表示i只能用自己表示自己,就是需要保留的。

#include <bits/stdc++.h>

using namespace std;
const int N = 110;   // N个正整数
const int M = 25010; // 表示的最大金额上限
int n;               // 实际输入的正整数个数
int v[N];            // 每个输入的数字,也相当于占用的体积是多大
int f[M];            // 二维优化为一维的DP数组f[i]面额为i时的前序货币组成方案数

int main() {
    int T;
    cin >> T;
    while (T--) {
        // 每轮初始化一次dp数组因为有多轮
        memset(f, 0, sizeof f);

        cin >> n;
        for (int i = 0; i < n; i++) cin >> v[i];
        // 每个货币的金额,都只能由比它小的货币组装而成,需要排一下序
        sort(v, v + n);

        // 背包容量
        int m = v[n - 1];

        // 在总金额是0的情况下只有一种方案
        f[0] = 1;
        
        // 恰好装满:计算每个体积(面额)的组成方案
        for (int i = 0; i < n; i++)
            for (int j = v[i]; j <= m; j++)
                f[j] += f[j - v[i]];

        // 统计结果数
        int res = 0;
        for (int i = 0; i < n; i++)
            // 如果当前面额的组成方案只有一种,那么它只能被用自己描述自己,不能让其它人描述自己
            // 这个面额就必须保留
            if (f[v[i]] == 1) res++;
        // 输出结果
        printf("%d\n", res);
    }
    return 0;
}

六、多重背包

AcWing 4. 多重背包问题 I

数据范围 0<N,V≤100 0<v_i,w_i,s_i≤100

二维数组

#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;

int f[N][N];
int main() {
    cin >> n >> m;

    for (int i = 1; i <= n; i++) { // 讨论每个物品
        int w, v, s;
        cin >> v >> w >> s;
        for (int j = 0; j <= m; j++)                   // 讨论每个剩余的体积
            for (int k = 0; k <= s && v * k <= j; k++) // 讨论加入的个数
                f[i][j] = max(f[i][j], f[i - 1][j - k * v] + w * k);
    }
    printf("%d\n", f[n][m]);
    return 0;
}

一维数组

#include <bits/stdc++.h>

using namespace std;

const int N = 110;

int n, m;
int f[N];

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int v, w, s;
        cin >> v >> w >> s;
        for (int j = m; j >= v; j--)
            for (int k = 0; k <= s && k * v <= j; k++)
                f[j] = max(f[j], f[j - v * k] + w * k);
    }
    printf("%d\n", f[m]);
    return 0;
}