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.

20 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背包模板就有了用武之地,因为三维数组可能会爆内存。

AcWing 1020. 潜水员 二维费用背包问题,但是是一道 反题,与正常题目相左的一道题。 人家是装不下就不再装了,它的含义是相反的:我还缺少多少,如果你给的多,那么直接把我装满~

老套路,二维好想,一维好记:

二维写法

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

int main() {
    cin >> m1 >> m2 >> n;
    memset(f, 0x3f, sizeof f);
    f[0][0][0] = 0;

    for (int i = 1; i <= n; i++) {
        int v1, v2, w;
        cin >> v1 >> v2 >> w;
        for (int j = 0; j <= m1; j++)
            for (int k = 0; k <= m2; k++) {
                f[i][j][k] = f[i - 1][j][k];
                f[i][j][k] = min(f[i - 1][j][k], f[i - 1][max(0, j - v1)][max(0, k - v2)] + w);
            }
    }
    cout << f[n][m1][m2] << endl;
    return 0;
}

一维写法

#include <bits/stdc++.h>
using namespace std;

const int N = 22, M = 80;

int n, m, K;
int f[N][M];

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

    memset(f, 0x3f, sizeof f);
    f[0][0] = 0;

    while (K--) {
        int v1, v2, w;
        cin >> v1 >> v2 >> w;
        for (int i = n; i >= 0; i--)
            for (int j = m; j >= 0; j--)
                f[i][j] = min(f[i][j], f[max(0, i - v1)][max(0, j - v2)] + w);
    }

    cout << f[n][m] << endl;

    return 0;
}

三、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

AcWing 1019. 庆功会

数据范围 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;
}

AcWing 5. 多重背包问题 II

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

#include <bits/stdc++.h>
using namespace std;

const int N = 1010; // 个数上限
const int M = 2010; // 体积上限
int n, m, idx;
// 无法使用二维数组原因是因为分拆后N*31*M=31*1010*2010太大了MLE了
// 所以,需要使用滚动数组进行优化一下,思想还是二维的
int f[2][M];

struct Node {
    int w, v;
} c[N * 31];

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

    for (int i = 1; i <= n; i++) {
        int v, w, s;
        cin >> v >> w >> s;
        for (int j = 1; j <= s; j *= 2) { // 1,2,4,8,16,32,64,128,...打包
            c[++idx] = {j * w, j * v};
            s -= j;
        }
        // 不够下一个2^n时独立成包
        if (s) c[++idx] = {s * w, s * v};
    }

    // 按01背包跑就可以啦
    for (int i = 1; i <= idx; i++)
        for (int j = 1; j <= m; j++) {
            f[i & 1][j] = f[i - 1 & 1][j];
            if (j >= c[i].v)
                f[i & 1][j] = max(f[i & 1][j], f[i - 1 & 1][j - c[i].v] + c[i].w);
        }

    // 输出
    printf("%d\n", f[idx & 1][m]);
    return 0;
}

AcWing 6. 多重背包问题 III

数据范围 0<N≤1000

0<V≤20000

0<v_i,w_i,s_i≤20000

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;  // 物品种类上限
const int M = 20010; // 背包容量上限
int n, m;

int f[N][M]; // 前i个物品在容量为j的限定下最大的价值总和
int q[M];    // 单调优化的队列,M是背包容量上限说明q[]里面保存的是体积

// 二维+队列[k-s*v,k],队列长s+1
int main() {
    cin >> n >> m;

    for (int i = 1; i <= n; i++) { // 考虑前i种物品
        int v, w, s;               // 体积、价值、个数
        cin >> v >> w >> s;
        // 下面的j,k是一起用来描述剩余体积的,之所以划分成两层循环是因为依赖的前序是按v为间隔的依赖并且是有个数限制的依赖
        // j:按对体积取模分组0表示剩余空间除以当前物品的体积余数是0
        // k:分组内的每一个体积,注意:这里的体积不一定都是合法的,因为数量是有限制的
        // 单调队列的意义查找前面k-s*v范围内的价值的最大值是一个单调递减的队列队头保存的是获取到最大值的最近体积
        for (int j = 0; j < v; j++) {         // 按余数分组讨论
            int hh = 0, tt = -1;              // 全新的单调下降队列
            for (int k = j; k <= m; k += v) { // 与j一起构成了有效体积
                // 1、讨论到第i个物品时由于它最多只有s个所以有效的转移体积最小是k-s*v,更小的体积将被去除
                if (hh <= tt && q[hh] < k - s * v) hh++;
                // 2、处理队尾,下一个需要进入队列的是f[i-1][k],它是后来的,生命周期长,可以干死前面能力不如它的所有老头子,以保证一个单调递减的队列
                while (hh <= tt && f[i - 1][q[tt]] + (k - q[tt]) / v * w <= f[i - 1][k]) tt--;
                // 3、k入队列
                q[++tt] = k;
                // 4、队列维护完毕f[i-1][k]已经进入队列,f[i][k]可以直接从队头取出区间最大值更新自己
                f[i][k] = f[i - 1][q[hh]] + (k - q[hh]) / v * w;
            }
        }
    }
    printf("%d\n", f[n][m]);
    return 0;
}

七、混合背包

  • ① 将01背包看成是数量只有1个的多重背包问题
  • ② 完全背包也不是真正的无限个数,因为受背包容量的限制,它最多可以使用的个数是s_i=m/v_i个,也就转化为多重背包问题

使用多重背包问题的二进制优化统一处理即可

#include <bits/stdc++.h>

using namespace std;
const int N = 1e5 + 10;
int n;    // 物品种类
int m;    // 背包容量
int f[N]; // dp数组
int idx;

struct Node {
    int v, w;
} c[N * 31];

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

    // 二进制打包
    for (int i = 1; i <= n; i++) {
        // 体积,价值,个数
        int v, w, s;
        cin >> v >> w >> s;

        // 根据题意做一些小的变形
        if (s == -1)
            s = 1; // 题目中s=-1表示只有1个
        else if (s == 0)
            s = m / v; // 完全背包(其实本质上就是多重背包):最多总体积/该物品体积向下取整
        // 如果是其它大于0的数字那么是多重背包

        // 将完全背包和多重背包利用二进制优化转化为01背包
        for (int j = 1; j <= s; j *= 2) {
            c[++idx] = {j * v, j * w};
            s -= j;
        }
        // 不够下一个2^n时独立成包
        if (s) c[++idx] = {s * v, s * w};
    }
    // 01背包
    for (int i = 1; i <= idx; i++)
        for (int j = m; j >= c[i].v; j--)
            f[j] = max(f[j], f[j - c[i].v] + c[i].w);
    // 输出
    printf("%d\n", f[m]);
    return 0;
}

八、分组背包

AcWing 9. 分组背包问题

二维状态

#include <bits/stdc++.h>

using namespace std;

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

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> s[i]; // 第i个分组中物品个数
        for (int j = 1; j <= s[i]; j++)
            cin >> v[i][j] >> w[i][j]; // 第i个分组中物品的体积和价值
    }

    for (int i = 1; i <= n; i++)            // 每组
        for (int j = 0; j <= m; j++) {      // 每个合法体积
            f[i][j] = f[i - 1][j];          // 如果一个都不要那么这一组就相当于白费给你机会也不中用继承于i-1
            for (int k = 1; k <= s[i]; k++) // 选择第k个
                if (j >= v[i][k])
                    f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]); // 枚举每一个PK一下大小
        }
    // 输出打表结果
    printf("%d", f[n][m]);
    return 0;
}

一维状态

#include <bits/stdc++.h>

using namespace std;
const int N = 110;

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

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

    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 (j >= v[i][k])
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

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

AcWing 1013. 机器分配 分组背包求最优路径