背包问题是动态规划问题的一大类型,下面我们对这个进行总结。
以 Acwing y中总结的 几个类型,我写了几个题解
应用知识点
- 01背包、完全背包 空间压缩的写法
- 多维费用的背包问题,以及状态的不同表示对复杂度的 影响
- 完全背包问题的三种求解方法O(NMS),O(NMlogS),O(NM)O(NMS), O(NMlogS),O(NM)O(NMS),O(NMlogS),O(NM)
- dp维度关系等于,小于等于,大于等于对初始化的影响(潜水员)
- dp关键信息最小值,最大值,方案数对初始化的影响
一、采药
Acwing 题目链接
非常裸的 01 背包
#include <bits/stdc++.h>
using namespace std;const int N = 1010;
int f[N][N];
int n, m;int main()
{cin >> m >> n;memset(f, 0, sizeof f);for (int i = 1; i <= n; i ++ ) {static int t, v;scanf("%d%d", &t, &v);for (int j = 0; j <= m; j ++ ) {f[i][j] = f[i - 1][j];if (j >= t) {f[i][j] = max(f[i][j], f[i - 1][j - t] + v);}}}cout << f[n][m] << endl;return 0;
}
二、装箱问题
ACwing 题目链接
对于该问题,我给出两个解法(其实都类似 )
第一种
f[i][j] 表示利用前 i 个箱子,体积恰好为 j 的方案是否可行,是一个 bool 数组
那么 f[i][j]=f[i−1][j]∣f[i−1][j−v[i]]f[i][j] = f[i-1][j] | f[i-1][j - v[i]]f[i][j]=f[i−1][j]∣f[i−1][j−v[i]]
初始化时, f[0][0]f[0][0]f[0][0] 置为 true,其余置为 false
下面是进行空间优化的写法
#include <bits/stdc++.h>
using namespace std;const int N = 40, M = 40010;
int f[M];
int n, m;
int cost[N];
// f[i][j] 表示前 i个物品,体积 恰好等于 j 是否可以
// 然后进行状态压缩
int main()
{// inputcin >> m >> n;for (int i = 1; i <= n; i ++ )scanf("%d", &cost[i]);f[0] = true;for (int i = 1; i <= n; i ++ ) {for (int j = m; j >= cost[i]; j -- ) {f[j] |= f[j - cost[i]];}}for (int i = m; ; i -- ) {if (f[i]) {cout << m - i << endl;break;}}return 0;
}
第二种写法
f[i][j]f[i][j]f[i][j]表示的是对前 i 个箱子,体积小于等于 j 时候的最大体积
f[i][j]=max(f[i−1][j],f[i−1][j−v[i]]+v[i]f[i][j] =max(f[i-1][j], f[i-1][j-v[i]] + v[i]f[i][j]=max(f[i−1][j],f[i−1][j−v[i]]+v[i]
这样的话,初始化全部为 0
而且最后输出结果也比较简单
m−f[n][m]m - f[n][m]m−f[n][m]
下面给出空间优化之后的代码
#include <bits/stdc++.h>
using namespace std;const int N = 40010;
int f[N];
int n, m;
// f[i][j] 表示前 i 个物品,体积小于等于 j 时候可以容纳的最大体积
// 然后进行状态压缩int main()
{cin>>m>>n;int v;memset(f, 0, sizeof f);for(int i = 1; i <= n; i++){cin>>v;for(int j = m; j >= v; j--){f[j] = max(f[j], f[j-v]+v);}}cout<<m-f[m]<<endl;return 0;
}
三、宠物小精灵值收服
Acwing 题目链接
本题是一个二维费用的 01 背包,理解起来不拿,不过题目属实有点长,而且 皮卡丘 的血量为 0 也会抓取失败,这是一个比较坑的点
下面,我给出两个dp的解决方案
方案一
f[i][j][k]f[i][j][k]f[i][j][k] 表示对前 i 个怪兽进行遍历,消耗精灵球数量小于等于j,消耗血量小于等于 k,的最大抓捕量
f[i][j][k]=max(f[i−1][k][k],f[i−1][j−cost1i][k−cost2i])f[i][j][k] = max(f[i-1][k][k], f[i-1][j-cost1_i][k-cost2_i])f[i][j][k]=max(f[i−1][k][k],f[i−1][j−cost1i][k−cost2i])
对应的代码如下
#include <bits/stdc++.h>
using namespace std;const int N = 1010, M = 510, K = 110, INF = 0x3f3f3f3f;
int cost1[K], cost2[K], n, m, k;
int f[N][M];int main()
{// inputcin >> n >> m >> k;m --;for (int i = 1; i <= k; i ++ ) {scanf("%d%d", &cost1[i], &cost2[i]);}for (int i = 1; i <= k; i ++ ) {for (int j = n; j >= cost1[i]; j -- ) {for (int k = m; k >= cost2[i]; k -- ) {f[j][k] = max(f[j][k], f[j - cost1[i]][k - cost2[i]] + 1);}}}int ans1, ans2;ans1 = f[n][m];for (int j = m; ; j -- ) {if (ans1 == 0) {ans2 = 0;break;}if (f[n][j] == ans1) {ans2 = j;} else {break;}}printf("%d %d\n", ans1, m - ans2 + 1);return 0;
}
但是我们将复杂度考虑进去的话,O(KNM)O(KNM)O(KNM)有时候会过大,万一被卡怎么办? 给出方案二
方案二
f[i][M][K]f[i][M][K]f[i][M][K] 遍历前面i个精灵,统计的是在 体力恰好为 m, 捕捉恰好为 k 时候的最小消耗求的数量
复杂度O(KKM)O(KKM)O(KKM)快了那么一点点
f[i][[j][k]=min(f[i−1][j][k],f[i−1][m−cost2i][k−1]+cost1i)f[i][[j][k]=min(f[i-1][j][k], f[i-1][m-cost2_i][k-1]+cost1_i)f[i][[j][k]=min(f[i−1][j][k],f[i−1][m−cost2i][k−1]+cost1i)
代码如下
#include <bits/stdc++.h>
using namespace std;const int N = 1010, M = 510, K = 110, INF = 0x3f3f3f3f;
int n, m, k1;
int cost1[K], cost2[N];
int f[M][K]; // 统计的是在 体力恰好为 m, 捕捉恰好为 k 时候的最小消耗求的数量int main()
{// inputcin >> n >> m >> k1;for (int i = 1; i <= k1; i ++ ) {scanf("%d%d", &cost1[i], &cost2[i]);}memset(f, 0x3f, sizeof f); f[0][0] = 0;for (int i = 1; i <= k1; i ++ ) {for (int j = m - 1; j >= cost2[i]; j -- ) {for (int k = k1; k >= 1; k -- ) {f[j][k] = min(f[j][k], f[j - cost2[i]][k - 1] + cost1[i]);}}}int ans1 = 0, ans2 = 0;for (int j = 0; j <= m - 1; j ++ ) {for (int k = 0; k < k1; k ++) {if (f[j][k] <= n) {if (k > ans1) {ans1 = k, ans2 = j;} else if (f[j][k] == n && j < ans2) {ans2 = j;}}}}printf("%d %d\n", ans1, m - ans2);return 0;
}
DP很灵活,有时候换一个dp思路,就可以优化一下复杂度
四、数字组合
Acwing 题目链接
挺简单一个 dp问题,和 01 背包很像
下面给出空间优化的代码
#include <bits/stdc++.h>
using namespace std;const int N = 10010;
int f[N], a[N];
int n, m;int main()
{cin >> n >> m;for (int i = 1; i <= n; i ++ ) {scanf("%d", &a[i]);}memset(f, 0, sizeof f);f[0] = 1;for (int i = 1; i <= n; i ++ ) {for (int j = m; j >= a[i]; j -- ) {f[j] += f[j - a[i]];}}cout << f[m] << endl;return 0;
}
五、买书
题目链接
挺简单的一个 完全背包问题
将书的个数看成 value,花费就是费用,然后跑一个完全背包
#include <bits/stdc++.h>
using namespace std;typedef long long LL;
typedef pair<int, int> PII;
const int M = 1010;
LL f[M];
int n = 4, m;
int a[6] = {0, 10, 20, 50, 100};int main()
{cin >> m;memset(f, 0, sizeof f);f[0] = 1;for (int i = 1; i <= 4; i ++ ) {for (int j = a[i]; j <= m; j ++ ) {f[j] += f[j - a[i]];}}cout << f[m] << endl;return 0;
}
六、货币系统
Problem Link
题目大意就是 在 n 个数 a1,a2,…,ana_1,a_2,\dots,a_na1,a2,…,an中,尽可能的选取少的数字将原数组给表示出来
解题思路如下
- 首先,我们先将 原数组 a 从小到大进行排序
- 最小的数字是不可以被表示的,因此最小的数字 a1a_1a1需要被选出,放在bbb数组中
- 然后我们查看 b 数组可以组合出哪些数字
- 不断对 a 数组的元素进行遍历,倘若他无法被 b 表示,那么他需要加入到 b 中,这是因为 如果 a[i]a[i]a[i] 无法被 a[0..i]a[0..i]a[0..i]所表示,那么他就无法被表示,需要放入支撑集 b 中
这就是一个完全背包的模型
/*这次的dp需要考虑到最小依赖集这个东西:对于 (n, a) ==> (m, b):那么:1. a1、、an一定是可以被b表示出来的2. b 一定是属于 a, 主要是因为倘若 b 不属于 a, 那么 b = sum(a[i] * t[i]) = sum(b[i] * t[i])自己被自己表示3. b不能被自己表示因此这个问题转换为了 求 不能被自己组成的a,然后作为b的一部分,也就是一个完全背包题目;代码简单,但是思路有点意思
*/
#include <bits/stdc++.h>
using namespace std;const int M = 25010, N = 110;
int a[N], n, m;
bool f[M];void sol() {sort(a + 1, a + 1 + n);m = a[n];memset(f, 0, sizeof f);f[0] = true;int res = 0;for (int i = 1; i <= n; i ++ ) {if (f[a[i]]) {continue;}// 无法被表示,需要被加入res ++;for (int j = a[i]; j <= m; j ++ ) {f[j] |= f[j - a[i]];}}printf("%d\n", res);}int main()
{int T; cin >> T;while (T -- ) {scanf("%d", &n);for (int i = 1; i <= n; i ++ ) {scanf("%d", &a[i]);}sol();}
}
七、多重背包问题III
多重背包问题链接
普通多重背包问题朴素写法O(NMS)O(NMS)O(NMS)
f(i,j)f(i, j)f(i,j)表示前i个物品,容量小于等于jjj的最大价值
#include <bits/stdc++.h>
using namespace std;const int N = 1010;
int f[N][N];
int n, m, v[N], w[N], s[N];int main()
{memset(f, 0, sizeof f);cin >> n >> m;for (int i = 1; i <= n; i ++ ) scanf("%d%d%d", &v[i], &w[i], &s[i]);for (int i = 1; i <= n; i ++ ) {for (int j = 0; j <= m; 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;
}
多重背包问题二进制优化方法O(NMlogS)O(NMlogS)O(NMlogS)
首先,对于容量,价值,和数量分别为 vi,wi,siv_i,w_i,s_ivi,wi,si的物品iii,我们可以将它分解为
多个 01 背包的物品
倘若 si=10s_i = 10si=10,我们可以将其分为
(vi,wi),(2vi,2wi),(4vi,4wi),(3vi,3wi)(v_i,w_i),(2v_i,2w_i),(4v_i,4w_i),(3v_i,3w_i)(vi,wi),(2vi,2wi),(4vi,4wi),(3vi,3wi),这几个 01背包,可以将
(vi,wi),(2vi,2wi),(3vi,3wi),…(siwi,sivi)(v_i,w_i),(2v_i,2w_i),(3v_i,3w_i), \dots (s_iw_i,s_i v_i)(vi,wi),(2vi,2wi),(3vi,3wi),…(siwi,sivi)全部给枚举一遍
这个是可以证明的
(w,v),(2w,2v),(4w,4v),(2kw,2kv),(surplus∗w,surplus∗v),其中surplus≥2k,1+2+4+⋯+2k+surplus=s(w,v),(2w,2v),(4w,4v),(2^kw,2^kv),(surplus*w,surplus*v), 其中surplus\geq 2^k,1+2+4+\dots+2^k+surplus=s(w,v),(2w,2v),(4w,4v),(2kw,2kv),(surplus∗w,surplus∗v),其中surplus≥2k,1+2+4+⋯+2k+surplus=s
首先1,2,4,⋯,2k1,2,4,\cdots,2^k1,2,4,⋯,2k可以将[0,2k+1][0,2^k+1][0,2k+1]完全包含,通过surplussurplussurplus区间再次移动,将[0,s][0,s][0,s]方案完全覆盖掉。
对用代码如下
#include <bits/stdc++.h>
using namespace std;const int N = 2010;
int f[N];
int n, m, v[N * 20], w[N * 20];int main()
{memset(f, 0, sizeof f);cin >> n >> m;int n2 = 0;// 多重背包拆成 01 背包for (int i = 1; i <= n; i ++ ) {static int a, b, s;scanf("%d%d%d", &a, &b, &s);int base = 1;while (true) {if (s <= base) {v[++ n2] = s * a;w[n2] = s * b;break;} else {s -= base;v[++ n2] = base * a;w[n2] = base * b;base *= 2;}}}// 跑一遍 01 背包n = n2;for (int i = 1; i <= n; i ++ ) {for (int j = m; j >= v[i]; j -- ) {// f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);f[j] = max(f[j], f[j - v[i]] + w[i]);}}cout << f[m] << endl;return 0;
}
完全背包的优化,类似于前缀最大值,
然后我们多重背包的优化,利用单调队列的优化
下面我来介绍优化的方法
首先,我们还是先对 i 个物品进行遍历
for(int i=1;i <= n; i ++ )
此时,我们第 i 个物品的体积为 v,价值为w,可用数量为 j
考虑一下我们的状态转移方程f[i][j]=max(f[i−1][j],f[i−1][j−v]+w,f[i−1][j−2v]+2w…,f[i−1][j−sv]+sw)f[i][j] = max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w\dots ,f[i-1][j-sv]+sw)f[i][j]=max(f[i−1][j],f[i−1][j−v]+w,f[i−1][j−2v]+2w…,f[i−1][j−sv]+sw),发现,倘若我们将原本的 f[i−1][j]f[i-1][j]f[i−1][j] 分组,组别如下
组号 | 内容 |
---|---|
0 | f[i][0], f[i][v], f[i][2v],…,f[i][kb+1] |
1 | f[i][1], f[i][v+1], f[i][2v+1],…,f[i][kv+1] |
…\dots… | ⋯⋯\cdots \cdots⋯⋯ |
v-1 | f[i][v-1], f[i][v+v-1], f[i][2v+v-1],…,f[i][kv+v-1] |
不难发现,只有组内之间还是有递归关系的,组间不存在联系,下面我们讨论组内是如何优化的的
首先,我们假设当前更新的 f[i][0…m] 为数组 dp[M], 上一层的 f[i-1][0…m]为数组pre[M]
有以下公式
dp[j] = pre[j]
dp[j+v] = max(pre[j] + w, pre[j+v])
dp[j+2v] = max(pre[j] + 2w, pre[j+v] + w, pre[j+2v])
dp[j+3v] = max(pre[j] + 3w, pre[j+v] + 2w, pre[j+2v] + w, pre[j+3v])
…
进一步整理一下
dp[j] = pre[j]
dp[j+v] = max(pre[j], pre[j+v] - w) + w
dp[j+2v] = max(pre[j], pre[j+v] - w, pre[j+2v] - 2w) + 2w
dp[j+3v] = max(pre[j], pre[j+v] - w, pre[j+2v] - 2w, pre[j+3v] - 3w) + 3w
不难发现,经过这样的整理,我们的 dp 数组就是在原数组基础之上增加了一个 滑动窗口求最大值的算法(单调队列,应该是递减的)
所以说,对应的代码如下所示
#include <bits/stdc++.h>
using namespace std;const int N = 1010, M = 20010;
int f[M], pre[M], n, m;
int v, w, s;
int que[M];int main()
{cin >> n >> m;for (int i = 1; i <= n; i ++ ) { // 对每个物品进行遍历scanf("%d%d%d", &v, &w, &s);memcpy(pre, f, sizeof f); // 将 i - 1 层的状态保存起来,后面dp需要使用for (int j = 0; j < v; j ++ ) { // 对每一类进行遍历,j, j+v, j+2v,..static int head, tail;head = 0, tail = -1; // 循环队列for (int k = j; k <= m; k += v) { // 对该类进行 动态规划// 首先更新我们的滑动窗口,去掉队列前划过去的,往后面加入if (head <= tail && k - que[head] > s * v) { // 队列头超过了窗口的最左侧head ++;}while (head <= tail && (pre[que[tail]] - (que[tail] - j) / v * w) <= (pre[k] - (k - j) / v * w)) { // 用于保证窗口的单调性-- tail;}que[++ tail] = k; // 增加窗口的尾部f[k] = pre[que[head]] + (k - que[head]) / v * w; // 用窗口的维护数值来更新结果}}}cout << f[m] << endl;return 0;
}
八、庆功会
Acwing 链接
本题就是一个很裸的 多重背包问题
通过观察复杂度,不难发现, NMS 复杂度就可以过去
倘若空间不够存储的话,可以采取滚动数组进行优化
#include <bits/stdc++.h>
using namespace std;const int N = 510, M = 6010;
int f[N][M];
int v, w, s, n, m;int main()
{cin >> n >> m;for (int i = 1; i <= n; i ++ ) {scanf("%d%d%d", &v, &w, &s);for (int j = 0; j <= m; j ++ ) { // f[i][j] = max(f[i-1][j], f[i-1][j-kv] + kw)f[i][j] = f[i - 1][j];for (int k = 1; k <= s; k ++ ) {if (j < k * v) break;f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w);}}}cout << f[n][m] << endl;return 0;
}
空间优化之后的代码
#include <bits/stdc++.h>
using namespace std;const int N = 510, M = 6010;
int f[M];
int v, w, s, n, m;int main()
{cin >> n >> m;for (int i = 1; i <= n; i ++ ) {scanf("%d%d%d", &v, &w, &s);for (int j = m; j >= 0; j -- ) { // 注意这个顺序// f[j] = f[j];// f[i][j] = max(f[i-1][j], f[i-1][j-kv] + kw)for (int k = 1; k <= s; k ++ ) {if (j < k * v) break;f[j] = max(f[j], f[j - k * v] + k * w);}}}cout << f[m] << endl;return 0;
}
九、混合背包问题
Acwing 混合背包
这就是一个 01 背包,完全背包,多重背包的综合
这里多重背包使用 二进制优化 即可跑完,复杂度O(NMlogS)O(NMlogS)O(NMlogS),时间复杂度没有超时
下面给出 空间优化的解题方法
#include <bits/stdc++.h>
using namespace std;const int M = 1010;
int f[M], n, m, v, w, s;int main()
{cin >> n >> m;memset(f, 0, sizeof f);for (int i = 1; i <= n; i ++ ) { scanf("%d%d%d", &v, &w, &s);if (s == 0) {// 完全背包for (int j = v; j <= m; j ++ ) {f[j] = max(f[j], f[j - v] + w);}} else { // 01 背包和多重背包if (s == -1) {s = 1;}for (int k = 0; (1 << k) <= s; k ++ ) {s -= (1 << k);for (int j = m; j >= v * (1 << k); j -- ) { // v * (1 << k)f[j] = max(f[j], f[j - v * (1 << k)] + w * (1 << k));}}if (s) {for (int j = m; j >= s * v; j -- ) { // 注意这个下界的判断f[j] = max(f[j], f[j - s * v] + s * w);}}}}cout << f[m] << endl;return 0;
}
十、二维费用的背包问题
Acwing 题目链接
一个非常裸的 二维费用 背包问题
#include <bits/stdc++.h>
using namespace std;const int N = 110;
int f[N][N];
int n, v, m;int main()
{memset(f, 0, sizeof f); // 01 bagcin >> n >> v >> m;for (int i = 1; i <= n; i ++ ) {static int a, b, c;scanf("%d%d%d", &a, &b, &c);for (int j = v; j >= a; j -- ) {for (int k = m; k >= b; k -- ) {f[j][k] = max(f[j][k], f[j - a][k - b] + c);}}}cout << f[v][m] << endl;return 0;
}
十一、潜水员
Acwing 题目链接
首先,我先给出一个错误的方法,然后说为什么是不可以的
该种方法 f[i][j][k]f[i][j][k]f[i][j][k] 表示对前 iii 个物品,选取氧气量恰好为 jjj,氮气量恰好为kkk,他所对应的最小重量
初始化,显然 memset(f, 0, sizeof f), f[0][0][0] = 1
对应的代码如下所示
#include <bits/stdc++.h>
using namespace std;const int M = 30, N = 110, INF = 0x3f3f3f3f;
int f[M * 2 + 5][N * 2 + 5];
int m, n, k1;int main()
{cin >> m >> n;cin >> k1;memset(f, 0x3f, sizeof f);f[0][0] = 0;for (int i = 1; i <= k1; i ++ ) { static int a, b, c;scanf("%d%d%d", &a, &b, &c);// 一定要注意他这个超限制的情况!// 这样做也错是因为,虽然 m 已经够了,但是因为 n还不够,因此 m 的增加量可以使很大的for (int j = m + M; j >= a; j -- ) {for (int k = n + N; k >= b; k -- ) {f[j][k] = min(f[j][k], f[j - a][k - b] + c);} }}int ans = INF;for (int j = m; j <= m + M; j ++ ) {for (int k = n; k <= n + N; k ++ ) {ans = min(ans, f[j][k]);}}cout << ans << endl;return 0;
}
但是该种方案是错误的!
原因在于,我们寻找的方案是 氧气容量 大于等于 m, 氮气容量大于等于 n 的最消息重量的解决方案,**但是**上限是多少,我们是没有办法确定了 你可以理所当然的以为 上限是 m * 2, n * 2, 或者是 m + M, n + N,但是都不对 上限其实是 N * M 级别的,因为倘若氧气很充足,但是氮气就是很少,因为氮气不足需要不断加入气缸使得氮气达到标准,与此同时氧气也在增加,最坏情况下是 M * N 因此,数组大小很爆炸,消耗时间也很爆炸,虽然递归式子没问题,但是从时间和空间的角度来看,并不可行。
下面我们考虑另一种dp思路
f[i][j][k] 表示的是 前 i 个物品,氧气量大于等于 j,氮气量大于等于 k 的最小重量,同样是从初始化和状态转移方程两个方面进行分析
初始化方面
f[0][x][y]=INF, 如果x>0∣∣y>0x>0 || y>0x>0∣∣y>0
f[0][x][y]=0,如果x<=0&&y<=0x<=0\&\&y<=0x<=0&&y<=0
其他都初始化为 INF
但是,因为我们的数组下表数值都为正数,也就是说取不到这个负数的情况,因此后面的运算对于负值的下标我们是通过特判来选取数值的。
状态转移方程
对于遍历第 i 个物品的属性 a, b, c
f[i][j][k]=max(f[i-1][j][k], f[i-1][j-a][k-b]+v)
这个方程是很好理解的,但是关键是我们有的合法状态是小于等于 0 的情况,使用数组是没有办法表示的(即使使用偏移量,也因为空间过大,而不可行,类似于我们讨论的第一种方法)
那么,我们是如何解决这个问题的呢?
对应代码如下所示
/*f[i][j]:表示恰到达到 i, j这个的时候, 最小重量注意是最小,初始化应该为INF它需要的是M, 但是我们不能遍历只是到M, N注意,如果是恰好的话,那么,范围太大, 数组开不了,而且会超时但是这个数据开的 应该要很大,很烦, 应该是 [N*M][N*M]....因此我们应该把这个状态给改了,不适用这个f[i][j]:表示大于等于 i, j这个的时候, 最小重量f[-x][-y] = 0;others INF;
*/
#include <bits/stdc++.h>
using namespace std;const int M = 30, N = 110, INF = 0x3f3f3f3f;
int f[M * 2 + 5][N * 2 + 5];
int m, n, k1;int main()
{cin >> m >> n;cin >> k1;memset(f, 0x3f, sizeof f);f[0][0] = 0;for (int i = 1; i <= k1; i ++ ) { static int a, b, c;scanf("%d%d%d", &a, &b, &c);for (int j = m; j >= 0; j -- ) {for (int k = n; k >= 0; k -- ) {if (j - a >= 0 && k - b >= 0) {f[j][k] = min(f[j][k], f[j - a][k - b] + c);} else if (j - a < 0 && k - b < 0) {f[j][k] = min(0 + c, f[j][k]);} else {f[j][k] = min(f[j][k], f[max(0, j - a)][max(0, k - b)] + c);}} }}cout << f[m][n] << endl;return 0;
}
十二、机器分配
ACWing Problem Link
挺简单个一个题目,该问题非 01 背包,也非完全背包、多重背包,因为虽然是设备分配给同一个公司、或者是其他公司,增加的价值是不一样的!
它更像是一个分组背包,f[i][j] i 表示的第 i 组,选取组内的物品,组内的物品可以使一个设备、两个设备、⋯\cdots⋯ 多个设备
f[i][j]f[i][j]f[i][j] 可以表示为前 i 个公司分配小于等于 j 个设备时候的最大价值
f[i][j]=maxf[i−1][j],f[i−1][j−1]+w[i][1],f[i−1][j−2]+w[i][2],⋅f[i−1][j−j]+w[i][j]f[i][j] = max{f[i-1][j], f[i-1][j-1]+w[i][1], f[i-1][j-2]+w[i][2], \cdot f[i-1][j-j]+w[i][j]}f[i][j]=maxf[i−1][j],f[i−1][j−1]+w[i][1],f[i−1][j−2]+w[i][2],⋅f[i−1][j−j]+w[i][j]
但是,因为最后需要输出分配的方案,我们需要数组 g[i][j] 记录一下到达 f[i][j] 这个状态时候,公司 i 分配了多少台机器,之后就可以知道合法方案了
代码如下
#include <bits/stdc++.h>
using namespace std;const int N = 15, M = 20;
int f[N][M], w[N][M];
int g[N][M], n, m;int main()
{cin >> n >> m;for (int i = 1; i <= n; i ++ ) {for (int j = 1; j <= m; j ++ ) {scanf("%d", &w[i][j]);}}memset(f, 0, sizeof f);memset(g, 0, sizeof g);for (int i = 1; i <= n; i ++ ) {for (int j = 1; j <= m; j ++ ) {for (int k = 0; k <= j; k ++ ) {if (f[i][j] < f[i - 1][j - k] + w[i][k]) {f[i][j] = f[i - 1][j - k] + w[i][k];g[i][j] = k;}}}}// find the maxint ri, rj, res = -1;for (int j = 0; j <= m; j ++ ) {if (f[n][j] > res) {res = f[n][j];ri = n, rj = j;}}// find the pathvector<int> path;while (ri != 0) {path.push_back(g[ri][rj]);rj -= g[ri][rj];ri --;}reverse(path.begin(), path.end());cout << res << endl;for (int i = 0; i < path.size(); i ++ ) {printf("%d %d\n", i + 1, path[i]);}return 0;
}
十三、开心的金明
ACWing 题目链接
写一个空间优化的 01 背包即可
#include <bits/stdc++.h>
using namespace std;const int N = 30, M = 30010;
int f[M], n, m;int main()
{cin >> m >> n;memset(f, 0, sizeof f);for (int i = 1; i <= n; i ++ ) {static int level, v;scanf("%d%d", &v, &level);for (int j = m; j >= v; j -- ) {f[j] = max(f[j], f[j - v] + v * level);}}cout << f[m] << endl;return 0;
}
十四、有依赖的背包问题
ACWing Link
一个树状的有依赖背包问题,
f[u][j] 表示的是 u 作为根的子树,当需要的体积小于等于 j 的最大价值
显然f[u][j] 的更新是依赖于他的儿子的,(因为是树,我们必须是dfs来确定便利的先后顺序,而不是之间 for i = 1;i <= n; i ++了)
首先,我们先让 父节点为 u 的点集合 set 跑一个 分组背包,得到 没有加入 u 的 f[u][j],之后我们在人为的加入 u 这个点就可以了,具体还可以优化,请看下面的代码.
#include <bits/stdc++.h>
using namespace std;const int N = 110, INF = 0x3f3f3f3f;int f[N][N], n, m, v[N], w[N];
bool st[N];
int h[N], e[N], ne[N], idx;void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}void dfs(int u, int m) {int v2;for (int i = h[u]; ~i; i = ne[i]) {v2 = e[i];if (!st[v2]) {dfs(v2, m - v[u]); // 减小一下子复杂度// dfs(v2, m);st[v2] = true;}// 多重背包for (int j = m; j >= 0; j -- ) { // 注意,这里是大于等于 0,和 Vi 没有关系,而且注意这个优化空间的顺序for (int k = 0; k <= j; k ++ ) {f[u][j] = max(f[u][j], f[v2][k] + f[u][j - k]);} }}// 最后处理一下,这个一定不能提前处理,因为自己只能买一次// 这个处理是写在外面的,千万别给我写里面去,里面跑的是多重背包for (int t = m; t >= v[u]; t -- ) { // 而且需要注意这个顺序 f[u][t] = w[u] + f[u][t - v[u]];}for (int t = 0; t < v[u]; t ++ )f[u][t] = 0;
}int main()
{// input and build the treecin >> n >> m;int root = -1;memset(h, -1, sizeof h), idx = 0;for (int i = 1; i <= n; i ++ ) {static int p;scanf("%d%d%d", &v[i], &w[i], &p);if (p == -1) {root = i;} else {add(p, i);}}// dp process memset(st, false, sizeof st);memset(f, 0, sizeof f);dfs(root, m);cout << f[root][m] << endl;return 0;
}
十五、背包问题求方案数
Acwing Link
这个题目和 原本的背包问题类似,只不过是在维护 原本dp的数组过程中,多维护了一个数组, g[i][j] 用于表示到达当前状态的方案数
#include <bits/stdc++.h>
using namespace std;const int N = 1010;
const int MOD = 1e9 + 7;
int v[N], w[N], n, m;
int g[N][N], f[N][N];int main()
{// inputcin >> n >> m;for (int i = 1; i <= n; i ++ ) {scanf("%d%d", &v[i], &w[i]);}// initialize, 注意这里的初始化可能和我们之前的初始化有些许的不同memset(f, 0, sizeof f);memset(g, 0, sizeof f);for (int i = 0; i <= m; i ++ ) {g[0][i] = 1;}// dpfor (int i = 1; i <= n; i ++ ) {for (int j = 0; j <= m; j ++ ) {if (j >= v[i]) {f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);g[i][j] = 0;// 更新方案数if (f[i][j] == f[i - 1][j]) {g[i][j] += g[i - 1][j];} if (f[i][j] == f[i - 1][j - v[i]] + w[i]) {g[i][j] += g[i - 1][j - v[i]];}} else {// 更新方案数f[i][j] = f[i - 1][j];g[i][j] = g[i - 1][j];}g[i][j] %= MOD;}}// outputcout << g[n][m] << endl;return 0;
}
十六、背包问题求具体方案
Problem Link
这个题目很有意思,我在这里进行一下详细的讲解
对于原本的递归公式
f[i][j]=max(f[i−1][j],f[i−1][j−v[i]]+w[i])f[i][j] = max(f[i-1][j], f[i - 1][j - v[i]] + w[i])f[i][j]=max(f[i−1][j],f[i−1][j−v[i]]+w[i]),f[i][j] 表示的是从 1…n 个物品选择部分,使得体积为 j 的最大价值,使用 g[i][j] 可以表示当前状态 f[i][j] 是从哪个状态转移来的。
为了寻找方案的最小字典序,那我们必须要对 g[i][j] 进行处理,使其满足这个最小字典序的要求,那么我们是让 当前物品 i 被选,还是不被选好呢?
从直觉上来看,尽可能的让当前物品被选,可能 会好一些,这是因为我们 i 是从 1 -> n 开始枚举的,当然是需要被选中比较好。
但是,不妨考虑一种情况,f[i][j] (假设 i 为 7)倘若不选自己的话,为 1, 3, 5物品组合,倘若选了自己的话 为 2, 3, 7,显然来看,这种贪心思路是有问题的。
回到看看,问题到底是出在哪里了呢?因为 f[i][j] 指的是 1…n 个物品中选,对当前物品 i 选不选进行判断,我们对 最后一个物品进行优先的选择,然而最后一个物品,就是方案数中的最后一个数,他是最不重要的,换而言之,我们应当将第一个物品,作为最重要的点,最后判断(因为最后决策的,才是最重要的)
所以,不难得知,求字典序最大的,也是第一个数决策粒度对大,也应该 放在最后一个进行判断。
为了适应这种变化,我将 f[i][j] 数组的递归方程个真实含义做了改变
f[i][j] 表示从前 i … n 个物品中 选取部分,使得物品体积(代价) 小于等于 j 的最大价值
f[i][j] = max(f[i + 1][j], f[ i + 1][j - v[i]] + w[i]) 而且尽可能的使用后者,即保证能使用 i 物品就使用 i 物品(字典序最小)
#include <bits/stdc++.h>
using namespace std;const int N = 1010;
int f[N][N], g[N][N];
int v[N], w[N];
int n, m;int main()
{// inputcin >> n >> m;for (int i = 1; i <= n; i ++ ) {scanf("%d%d", &v[i], &w[i]);}// initialize memset(f, 0, sizeof f);memset(g, -1, sizeof f);// dpfor (int i = n; i >= 1; i -- ) {for (int j = 0; j <= m; j ++ ) {if (j >= v[i]) {f[i][j] = max(f[i + 1][j], f[i + 1][j - v[i]] + w[i]);if (f[i][j] == f[i + 1][j - v[i]] + w[i]) {g[i][j] = j - v[i];} else {g[i][j] = j;}} else {f[i][j] = f[i + 1][j];g[i][j] = j;}}}// outputint res = m;for (int i = 1; i <= n; i ++ ) {if (g[i][res] == res) {continue;} else {cout << i << ' ';res = g[i][res];}}puts("");return 0;
}
十七、能量石
Problem Link