背包问题(背包九讲)
背包九讲
概述
本文参考自bilibili上up主大雪菜的背包九讲专题:背包九讲专题1、背包九讲专题2 以及AcWing上的相关讲解。
背包问题代表了一类问题,即组合类的最优化问题,就是如果给我们一堆物品(元素),我们要按照某种限从中选出若干个物品(元素),求最大/最小值。
本文将将对如下的九种背包问题给出分析过程以及实现代码(提供C++和Java代码,代码链接:github),最后还给出了Leetcode上部分相关的背包问题以及解答。
/* 1. 01背包问题 2. 完全背包问题 3. 多重背包问题 4. 混合背包问题 5. 二维费用背包问题 6. 分组背包问题 7. 背包问题求方案数 8. 求背包问题的方案 9. 有依赖的背包问题 */
背包问题是一类典型的动态规划问题,一般对于动态规划问题,常规分析方式是给出
状态定义
和状态转移
,这里为了更加容易理解,采用yxc提出的闫式dp分析法
。
一. 01背包问题
问题描述
问题链接:01背包问题
分析
代码
- C++:
// Created by WXX on 2021/2/24 13:46
#include <iostream>using namespace std;const int N = 1010; // 多开几个数据,防止数组下标越界int n, m; // 物品种类数,背包容积
int v[N], w[N]; // 体积,价值。注意:v[1]存储第一件物品,索引0未使用
int f[N][N]; // dp数组int main() {// 读入数据cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];// 算法过程,因为f[0][0~m]初始就为0,因此初始化可以省略for (int i = 1; i <= n; i++) { // 先循环物品for (int j = 0; j <= m; j++) { // 再循环容量// 最后循环决策f[i][j] = f[i - 1][j]; // 不选第i件物品if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); // 考虑选第i件物品}}cout << f[n][m] << endl;return 0;
}
- Java
import java.util.Scanner;// Created by WXX on 2021/2/24 14:15
public class Main {public static final int N = 1010; // 多开几个数据,防止数组下标越界static int n, m; // 物品种类数,背包容积static int[] v = new int[N], w = new int[N]; // 体积,价值。注意:v[1]存储第一件物品,索引0未使用static int[][] f = new int[N][N]; // dp数组public static void main(String[] args) {// 读入数据Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt();}// 算法过程,因为f[0][0~m]初始就为0,因此初始化可以省略for (int i = 1; i <= n; i++) { // 先循环物品for (int j = 0; j <= m; j++) { // 再循环容量// 最后循环决策f[i][j] = f[i - 1][j]; // 不选第i件物品if (j >= v[i]) f[i][j] = Math.max(f[i][j], f[i - 1][j - v[i]] + w[i]); // 考虑选第i件物品}}System.out.println(f[n][m]);}
}
代码优化(只用C++演示)
- 考虑到在计算 f 的时候,当计算第 i 行时,只用到了第 i - 1 行的数据,因此可以用滚动数组优化
// Created by WXX on 2021/2/24 14:29
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[2][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 = 0; j <= m; j++) {f[i & 1][j] = f[(i - 1) & 1][j];if (j >= v[i]) f[i & 1][j] = max(f[i & 1][j], f[(i - 1) & 1][j - v[i]] + w[i]);}}cout << f[n & 1][m] << endl;return 0;
}
- 其实这里还可以将 f 数组优化为一维数组,但是考虑到要用到上一行数据,因此第二层循环应该从大到小进行遍历,这样每次更新用到的就是未被更新的数据
// Created by WXX on 2021/2/24 14:36
#include <iostream>using namespace std;const int N = 1010;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;
}
二. 完全背包问题
问题描述
问题链接:完全背包问题
分析
代码
- C++:
// Created by WXX on 2021/2/24 14:52
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[N][N];// TLE
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 = 0; j <= m; j++) // 再循环容量for (int k = 0; 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;
}
- Java:
import java.util.Scanner;// Created by WXX on 2021/2/24 14:58
public class Main {public static final int N = 1010;static int n, m;static int[] v = new int[N], w = new int[N];static int[][] f = new int[N][N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt();}for (int i = 1; i <= n; i++) // 先循环物品for (int j = 0; j <= m; j++) // 再循环容量for (int k = 0; k * v[i] <= j; k++) // 最后循环决策f[i][j] = Math.max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);System.out.println(f[n][m]);}
}
代码优化(只用C++演示)
我们可以将 f(i,j)=f(i−1,j−k∗v[i])+k∗w[i]f(i, j) = f(i-1, j-k*v[i]) + k*w[i]f(i,j)=f(i−1,j−k∗v[i])+k∗w[i] 展开,就可以发现如下规律:
因此代码可以根据 f(i,j)=max(f(i−1,j),f(i,j−v)+w)f(i, j) = max( f(i-1, j), f(i, j-v) + w)f(i,j)=max(f(i−1,j),f(i,j−v)+w) 进行优化:
// Created by WXX on 2021/2/24 15:21
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[N][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 = 0; j <= m; j++) {f[i][j] = f[i - 1][j];if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);}cout << f[n][m] << endl;return 0;
}
- 这里还可以将 f 数组优化为一维数组,因为不需要用到上一行数据,要用到本行之前计算出来的户籍,因此第二层循环应该从小到大进行遍历
// Created by WXX on 2021/2/24 15:21
#include <iostream>using namespace std;const int N = 1010;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 = v[i]; j <= m; j++)f[j] = max(f[j], f[j - v[i]] + w[i]);cout << f[m] << endl;return 0;
}
三. 多重背包问题
3.1 多重背包问题 I
问题描述
问题链接:多重背包问题 I
分析
代码
- C++
// Created by WXX on 2021/2/24 16:07
#include <iostream>using namespace std;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 = 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;
}
- Java
import java.util.Scanner;// Created by WXX on 2021/2/24 16:13
public class Main {public static final int N = 110;static int n, m;static int[] v = new int[N], w = new int[N], s = new int[N];static int[][] f = new int[N][N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt(); s[i] = sn.nextInt();}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] = Math.max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);System.out.println(f[n][m]);}
}
3.2 多重背包问题 II
问题描述
问题链接:多重背包问题 II
分析
这一题不能使用类似于完全背包问题的方式进行优化,可以将 f(i,j)=f(i−1,j−k∗v)+k∗w,k=0,1,...,sf(i, j) = f(i-1, j-k*v) + k*w, k=0,1,...,sf(i,j)=f(i−1,j−k∗v)+k∗w,k=0,1,...,s 展开,如下:
我们发现 f[i,j−v]f[i, j-v]f[i,j−v] 中比 f[i,j]f[i, j]f[i,j] 最后多了一项,不能直接得到这两者的关系,所以不能使用类似于完全背包问题的方式进行优化。
本次的优化方式为二进制优化,将s件物品进行拆分,然后可以转化成01背包问题
/* 例如 s=10 10可以拆分为0、1、2、4、3; 前四个数可以凑出0~7之间的任何数据,加上3可以凑出3~10之间的任何数据,因此这5个数可以凑出0~10内的任何数据; 相当于将10个物品打包成4个物品,打包后的物品的体积和价值分别为单个物品的1、2、4、3倍; 这四个物品都是可选可不选,因此就转化成了01背包问题。例如 s=200 200可以拆分为0、1、2、4、8、16、32、64、73; 一共8个物品总结:对于s,可以划分为 log(s) 上取整个单一物品,然后用01背包问题的思路解决即可,时间复杂度:O(N*V*log(s)) */
代码
- C++
// Created by WXX on 2021/2/24 16:42
#include <iostream>
#include <vector>using namespace std;const int N = 2010;int n, m;
int f[N];struct Good {int v, w;
};int main() {vector<Good> goods;cin >> n >> m;for (int i = 0; i < n; i++) {int v, w, s;cin >> v >> w >> s;for (int k = 1; k <= s; k *= 2) {s -= k;goods.push_back({k * v, k * w});}if (s > 0) goods.push_back({s * v, s * w});}for (auto good : goods)for (int j = m; j >= good.v; j--)f[j] = max(f[j], f[j - good.v] + good.w);cout << f[m] << endl;return 0;
}
- Java
import java.util.ArrayList;
import java.util.Scanner;// Created by WXX on 2021/2/24 16:50
public class Main {public static final int N = 2010;static int n, m;static int[] f = new int[N];static class Good {int v, w;public Good(int v, int w) {this.v = v;this.w = w;}}public static void main(String[] args) {ArrayList<Good> goods = new ArrayList<>();Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 0; i < n; i++) {int v = sn.nextInt(), w = sn.nextInt(), s = sn.nextInt();for (int k = 1; k <= s; k *= 2) {s -= k;goods.add(new Good(k * v, k * w));}if (s > 0) goods.add(new Good(s * v, s * w));}for (Good good : goods)for (int j = m; j >= good.v; j--)f[j] = Math.max(f[j], f[j - good.v] + good.w);System.out.println(f[m]);}
}
3.2 多重背包问题 III
问题描述
问题链接:多重背包问题 III
分析
DP分析和 3.1 多重背包问题 I 完全一致,但是本题的数据量更大,需要进一步优化,这里采用滑动窗口求最值的方式优化
还有一个问题需要解决,就是窗口每次向右滑动一格,除了滑出窗口外的元素,其余的元素在下一次比较中都需要加上一个偏移量w,这个问题可以通过如下方式解决(参考网址):
/* 因为每次循环都只需要用到第i-1层的结果,所以在这里: g:第i-1层结果 f:第i层结果如果一共有3个物品,即s=3的话: 取r=1,那么f[3*v+1]的最大值为 f[3*v+1] = max(g[3*v+1], g[2*v+1]+w, g[v+1]+2w, g[1]+3w);所以我们可以得到下列算式:(其中r表示余数) f[r]= g[r]; f[r+v]= max(g[r] + w, g[r+v]); f[r+2v]=max(g[r] + 2w, g[r+v] + w, g[r+2v],); f[r+3v]=max(g[r] + 3w, g[r+v] + 2w, g[r+2v] + w, g[r+3v]); …… f[r+sv]=max(g[r] + sw,……, g[r+(s-1)v] + w, g[r+sv]);可以发现上面的等式上下存在偏移量w,所以可以减去kw再加上kw进行转换 f[r]= g[r]; f[r+v]= max(g[r], g[r+v] - w) + w; f[r+2v]=max(g[r], g[r+v] - w, g[r+2v] - 2w) + 2w; f[r+3v]=max(g[r], g[r+v] - w, g[r+2v] - 2w, g[r+3v] - 3w) + 3w; …… f[r+sv]=max(g[r], ……, g[r+(s-1)v] - (s-1)w, g[r+sv] - sw) + sw; 每次单调队列q (q中存储的是体积,如r,r+v,...) 用队尾数据和要插入的数据进行比较时,需要减去一个数 这里比较的其实是上面max小括号中的数据 队尾数据:g[a] - (a-r)/v * w,其中a是队尾体积q[tt] ==> g[q[tt]] - (q[tt]-r)/v * w 即将滑入滑动窗口的数据:g[k] - (k-r)/v * w,其中k是当前考察的同余的体积,例如k=r+3v等另外还要注意,f中真实存储的数据为:g[q[hh]] + (q[hh]-r)/v * w,其中(q[hh]-r)/v * w为当前考察物品的收益 */
关于滑动窗口求最值,这是一个经典的问题,在LeetCode上有对应的问题:Leetcode 0239 滑动窗口最大值;在AcWing上也有对应的练习:AcWing 0154. 滑动窗口
这里给出 Leetcode 0239 滑动窗口最大值 的C++解法(注意:这里的滑动窗口大小必须是k,不能小于k;多重背包中的滑动窗口为1,然后变为2,直到到达v后不再改变):
// 考点:单调队列 /*** 执行用时:240 ms, 在所有 C++ 提交中击败了97.90%的用户* 内存消耗:114.1 MB, 在所有 C++ 提交中击败了88.81%的用户*/ class Solution {public:vector<int> maxSlidingWindow(vector<int> &nums, int k) {int n = nums.size();int q[n];int hh = 0, tt = -1;vector<int> res;for (int i = 0; i < nums.size(); i++) {if (hh <= tt && i - k + 1 > q[hh]) hh++;while (hh <= tt && nums[q[tt]] <= nums[i]) tt--;q[++tt] = i;if (i >= k - 1) res.push_back(nums[q[hh]]);}return res;} };
代码
// Created by WXX on 2021/2/24 18:43
#include <iostream>
#include <cstring>using namespace std;const int N = 20010;int n, m;
int f[N], g[N], q[N]; // g存储上一行的值,f存储当前行的值,q是单调队列int main() {cin >> n >> m;for (int i = 0; i < n; i++) { // 这里采取边读入物品,边处理;每个物品都要进行滑窗处理int v, w, s;cin >> v >> w >> s;memcpy(g, f, sizeof f); // 将f中的数据拷贝到g中for (int r = 0; r < v; r++) {int hh = 0, tt = -1;for (int k = r; k <= m; k += v) {if (hh <= tt && q[hh] < k - s * v) hh++; // 说明队头元素应该 滑出滑窗while (hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[k] - (k - r) / v * w) tt--;q[++tt] = k; // 将当前考察体积存入滑窗if (hh <= tt) f[k] = max(f[k], g[q[hh]] + (k - q[hh]) / v * w);}}}cout << f[m] << endl;return 0;
}
- Java
import java.util.Scanner;// Created by WXX on 2021/2/24 19:37
public class Main {public static final int N = 20010;static int n, m;static int[] f = new int[N], g = new int[N], q = new int[N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 0; i < n; i++) { // 这里采取边读入物品,边处理;每个物品都要进行滑窗处理int v = sn.nextInt(), w = sn.nextInt(), s = sn.nextInt();System.arraycopy(f, 0, g, 0, N); // 将f中的数据拷贝到g中for (int r = 0; r < v; r++) {int hh = 0, tt = -1;for (int k = r; k <= m; k += v) {if (hh <= tt && q[hh] < k - s * v) hh++; // 说明队头元素应该 滑出滑窗while (hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[k] - (k - r) / v * w) tt--;q[++tt] = k; // 将当前考察体积存入滑窗if (hh <= tt) f[k] = Math.max(f[k], g[q[hh]] + (k - q[hh]) / v * w);}}}System.out.println(f[m]);}
}
四. 混合背包问题
问题描述
问题链接:混合背包问题
分析
- 分不同情况下进行状态转移即可,如下图:
代码
- C++
// Created by WXX on 2021/2/24 19:59
#include <iostream>using namespace std;const int N = 1010;int n, m;
int f[N];int main() {cin >> n >> m;for (int i = 0; i < n; i++) {int v, w, s;cin >> v >> w >> s;if (s == 0) { // 完全背包for (int j = v; j <= m; j++) f[j] = max(f[j], f[j - v] + w);} else {if (s == -1) s = 1; // 全部转为多重背包,然后使用二进制优化for (int k = 1; k <= s; k *= 2) {for (int j = m; j >= k * v; j--)f[j] = max(f[j], f[j - k * v] + k * w);s -= 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;
}
- Java
import java.util.Scanner;// Created by WXX on 2021/2/24 20:06
public class Main {public static final int N = 1010;static int n, m;static int[] f = new int[N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 0; i < n; i++) {int v = sn.nextInt(), w = sn.nextInt(), s = sn.nextInt();if (s == 0) { // 完全背包for (int j = v; j <= m; j++) f[j] = Math.max(f[j], f[j - v] + w);} else {if (s == -1) s = 1; // 全部转为多重背包,然后使用二进制优化for (int k = 1; k <= s; k *= 2) {for (int j = m; j >= k * v; j--)f[j] = Math.max(f[j], f[j - k * v] + k * w);s -= k;}if (s != 0) {for (int j = m; j >= s * v; j--)f[j] = Math.max(f[j], f[j - s * v] + s * w);}}}System.out.println(f[m]);}
}
五. 二维费用背包问题
问题描述
问题链接:二维费用背包问题
分析
- 需要说明一点,二维费用背包为可以和 01背包、完全背包、多重背包、分组背包 这四种类型背包中的任何一种组合起来。
- 当前题目是和 01背包 绑定起来的题目
代码
- C++
// Created by WXX on 2021/2/24 20:25
#include <iostream>using namespace std;const int N = 110;int n, V, M;
int f[N][N]; // 第一维代表体积,第二维代表重量int main() {cin >> n >> V >> M;for (int i = 0; i < n; i++) {int v, m, w;cin >> v >> m >> w;for (int j = V; j >= v; j--)for (int k = M; k >= m; k--)f[j][k] = max(f[j][k], f[j - v][k - m] + w);}cout << f[V][M] << endl;return 0;
}
- Java
import java.util.Scanner;// Created by WXX on 2021/2/24 20:34
public class Main {public static final int N = 110;static int n, V, M;static int[][] f = new int[N][N]; // 第一维代表体积,第二维代表重量public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); V = sn.nextInt(); M = sn.nextInt();for (int i = 0; i < n; i++) {int v = sn.nextInt(), m = sn.nextInt(), w = sn.nextInt();for (int j = V; j >= v; j--)for (int k = M; k >= m; k--)f[j][k] = Math.max(f[j][k], f[j - v][k - m] + w);}System.out.println(f[V][M]);}
}
六. 分组背包问题
问题描述
问题链接:分组背包问题
分析
代码
- C++
// Created by WXX on 2021/2/24 20:52
#include <iostream>using namespace std;const int N = 110;int n, m;
// s[i]: 代表第i组物品的数量;v[i][k],w[i][k]: 第i组中第k个物品的体积和价值
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 = 0; 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 = 0; k < s[i]; k++) // 最后循环决策if (j >= v[i][k])f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);cout << f[m] << endl;return 0;
}
- Java
import java.util.Scanner;// Created by WXX on 2021/2/24 21:01
public class Main {public static final int N = 110;static int n, m;// s[i]: 代表第i组物品的数量;v[i][k],w[i][k]: 第i组中第k个物品的体积和价值static int[] s = new int[N];static int[][] v = new int[N][N], w = new int[N][N];static int[] f = new int[N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {s[i] = sn.nextInt();for (int j = 0; j < s[i]; j++) {v[i][j] = sn.nextInt(); w[i][j] = sn.nextInt();}}for (int i = 1; i <= n; i++) // 先循环物品for (int j = m; j >= 0; j--) // 再循环容量。因为某件物品要么选要么不选,所以递减遍历for (int k = 0; k < s[i]; k++) // 最后循环决策if (j >= v[i][k])f[j] = Math.max(f[j], f[j - v[i][k]] + w[i][k]);System.out.println(f[m]);}
}
注意点
其实 多重背包问题 是 分组背包问题 的一个特例。
对于多重背包问题:对于某个物品来说,如果出现 s 次,则可以选择0次,(1次…s次),我们将(1次…s次)这些情况打包起来形成一组,看成不同的物品(s个),我们最多从中选1个,因此有 s+1 种可能的情况。
七. 背包问题求方案数
问题描述
问题链接:背包问题求方案数
分析
需要说明一点,和二维费用背包一样。背包问题求方案数可以和 01背包、完全背包、多重背包、分组背包 这四种类型背包中的任何一种组合起来。
本题的本质是求最短路径的条数。其实动态规划的本质是图论问题。
我们可以根据背包问题的状态转移方程来分析方案数,这里状态转移方程是:f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi)f(i,j)=max(f(i-1,j), f(i-1,j-vi)+wi)f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi)。为了计算方案数,这里的 f(i,j)f(i,j)f(i,j) 定义需要做一下改变:f(i,j)f(i,j)f(i,j) 代表只从前 i 个物品中选,体积恰好是 j 的最大价值。
为了实现 f(i,j)f(i,j)f(i,j) 所定义的含义,我们需要在初始化是将 f(0, k),即第一行初始化为负无穷,且只有 f(0, 0) = 0。这样的话,负无穷所更新的状态仍然是负无穷,只有被 f(0, 0)更新的状态是有效的状态,因此体积恰好是 j。
为了记录方案数,我们还需要另外开辟一个数组 g(i,j)g(i,j)g(i,j) ,代表的含义是: f(i,j)f(i,j)f(i,j) 取最大值时的方案数。 g(i,j)g(i,j)g(i,j) 的求解存在三种情况:
(1)如果 f(i−1,j)f(i - 1,j)f(i−1,j) > f(i−1,j−v)+wf(i - 1,j-v) + wf(i−1,j−v)+w ,则 g(i,j)g(i,j)g(i,j) = g(i−1,j)g(i - 1,j)g(i−1,j) ;
(2)如果 f(i−1,j)f(i - 1,j)f(i−1,j) < f(i−1,j−v)+wf(i - 1,j-v) + wf(i−1,j−v)+w ,则 g(i,j)g(i,j)g(i,j) = g(i−1,j−v)g(i - 1,j-v)g(i−1,j−v) ;
(3)如果 f(i−1,j)f(i - 1,j)f(i−1,j) = f(i−1,j−v)+wf(i - 1,j-v) + wf(i−1,j−v)+w ,则 g(i,j)g(i,j)g(i,j) = g(i−1,j)g(i - 1,j)g(i−1,j) + g(i−1,j−v)g(i - 1,j-v)g(i−1,j−v) ;
因为本题对应的是 01背包问题,所以上面的二维数组都可以被优化为一维数组,只要在遍历体积的时候从大到小进行遍历即可。
代码
- C++
// Created by WXX on 2021/2/24 21:56
#include <iostream>
#include <cstring>using namespace std;const int N = 1010, mod = 1e9 + 7;int n, m;
// f[i]: 表示体积恰好为j时的最大价值;g[i]: f[i]取最大值时的方案数
int f[N], g[N];int main() {cin >> n >> m;memset(f, -0x3f, sizeof f);f[0] = 0;g[0] = 1; // 什么都不选也是一种方案for (int i = 0; i < n; i++) {int v, w;cin >> v >> w;for (int j = m; j >= v; j--) {int maxv = max(f[j], f[j - v] + w);int cnt = 0;if (maxv == f[j]) cnt += g[j];if (maxv == f[j - v] + w) cnt += g[j - v];g[j] = cnt % mod;f[j] = maxv;}}int res = 0; // 最大价值,因为取到最大价值消耗的体积不一定恰好等于背包容量for (int i = 0; i <= m; i++) res = max(res, f[i]);int cnt = 0; // 取到最大价值的方案数for (int i = 0; i <= m; i++)if (res == f[i])cnt = (cnt + g[i]) % mod;cout << cnt << endl;return 0;
}
- Java
import java.util.Arrays;
import java.util.Scanner;// Created by WXX on 2021/2/24 22:08
public class Main {public static final int N = 1010, mod = (int)(1e9 + 7);static int n, m;// f[i]: 表示体积恰好为j时的最大价值;g[i]: f[i]取最大值时的方案数static int[] f = new int[N], g = new int[N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();Arrays.fill(f, Integer.MIN_VALUE);f[0] = 0;g[0] = 1; // 什么都不选也是一种方案for (int i = 0; i < n; i++) {int v = sn.nextInt(), w = sn.nextInt();for (int j = m; j >= v; j--) {int maxv = Math.max(f[j], f[j - v] + w);int cnt = 0;if (maxv == f[j]) cnt += g[j];if (maxv == f[j - v] + w) cnt += g[j - v];g[j] = cnt % mod;f[j] = maxv;}}int res = 0; // 最大价值,因为取到最大价值消耗的体积不一定恰好等于背包容量for (int i = 0; i <= m; i++) res = Math.max(res, f[i]);int cnt = 0; // 取到最大价值的方案数for (int i = 0; i <= m; i++)if (res == f[i])cnt = (cnt + g[i]) % mod;System.out.println(cnt);}
}
八. 求背包问题的方案
问题描述
问题链接:求背包问题的方案
分析
需要说明一点,和二维费用背包以及背包问题求方案数一样。求背包问题的方案可以和 01背包、完全背包、多重背包、分组背包 这四种类型背包中的任何一种组合起来。
我们可以根据背包问题的状态转移方程来求背包的方案,这里状态转移方程是:f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi)f(i,j)=max(f(i-1,j), f(i-1,j-vi)+wi)f(i,j)=max(f(i−1,j),f(i−1,j−vi)+wi)。这里存在三种情况
(1) f(i,j)f(i,j)f(i,j) == f(i−1,j)f(i-1,j)f(i−1,j) ,f(i,j)f(i,j)f(i,j) > f(i−1,j−vi)+wif(i-1,j-vi)+wif(i−1,j−vi)+wi ,则最大价值的方案中必定不包含物品 i ;
(2) f(i,j)f(i,j)f(i,j) > f(i−1,j)f(i-1,j)f(i−1,j) ,f(i,j)f(i,j)f(i,j) == f(i−1,j−vi)+wif(i-1,j-vi)+wif(i−1,j−vi)+wi ,则最大价值的方案中必定包含物品 i ;
(3) f(i,j)f(i,j)f(i,j) == f(i−1,j)f(i-1,j)f(i−1,j) ,f(i,j)f(i,j)f(i,j) == f(i−1,j−vi)+wif(i-1,j-vi)+wif(i−1,j−vi)+wi ,则最大价值的方案中可包含也可不包含物品 i ;
另外,本题还要求输出 字典序最小的方案,因此我们应该反过来遍历物品(从物品n开始遍历到物品1),求出数组 f 后,之后就可以从物品1开始考察,能选则选,然后考虑物品2,…,这样得到的结果字典序最小。如果还是从物品1开始遍历,则之后只能从物品n开始考察,无法保证字典序最小。
注意:我们此时不能将二维数组压缩为一维数组,这是因为数组 f 中间的状态还需要被使用。
除了上面这种解决方案外,还可以通过创建一个bool数组记录某个物品是否应该被选择。
代码
- C++
// Created by WXX on 2021/2/24 22:27
#include <iostream>using namespace std;const int N = 1010;int n, m;
int v[N], w[N];
int f[N][N];int main() {cin >> n >> m;for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];for (int i = n; i >= 1; i--)for (int j = 0; 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]);}int j = m;for (int i = 1; i <= n; i++)if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {cout << i << ' '; // 如果能选物品i的话一定要选,这样可保证字典序最小j -= v[i];}return 0;
}
- Java
import java.util.Scanner;// Created by WXX on 2021/2/24 22:33
public class Main {public static final int N = 1010;static int n, m;static int[] v = new int[N], w = new int[N];static int[][] f = new int[N][N];public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt();}for (int i = n; i >= 1; i--)for (int j = 0; j <= m; j++) {f[i][j] = f[i + 1][j];if (j >= v[i]) f[i][j] = Math.max(f[i][j], f[i + 1][j - v[i]] + w[i]);}int j = m;for (int i = 1; i <= n; i++)if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {System.out.print(i + " ");; // 如果能选物品i的话一定要选,这样可保证字典序最小j -= v[i];}}
}
九. 有依赖的背包问题
问题描述
问题链接:有依赖的背包问题
分析
- 这一题是一道 树形DP 的问题
- 这是一棵树,我们采取的存储方式邻接表,实现方式是数组模拟链表,这部分内容可以参照AcWing的算法基础课:网址
代码
- C++
// Created by WXX on 2021/2/24 23:08
#include <iostream>
#include <cstring>using namespace std;const int N = 110;int n, m;
int v[N], w[N];
int h[N], e[N], ne[N], idx = 0; // 邻接矩阵
int f[N][N];void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}void dfs(int u) {for (int i = h[u]; ~i; i = ne[i]) { // 遍历u的子节点,相当于循环物品组int son = e[i];dfs(son); // 求解完成子节点后才能求解当前节点// 分组背包for (int j = m - v[u]; j >= 0; j--) // 循环体积for (int k = 0; k <= j; k++) // 循环决策f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);}// 将物品u加进去for (int i = m; i >= v[u]; i--) f[u][i] = f[u][i - v[u]] + w[u];for (int i = 0; i < v[u]; i++) f[u][i] = 0; // 不能放入物品u,则整棵子树都不能放入
}int main() {cin >> n >> m;memset(h, -1, sizeof h);int root = 1; // 根节点并不一定是1号点for (int i = 1; i <= n; i++) {int p;cin >> v[i] >> w[i] >> p;if (p == -1) root = i;else add(p, i); // 添加一条由p指向i的边}dfs(root);cout << f[root][m] << endl;return 0;
}
- Java
import java.util.Arrays;
import java.util.Scanner;// Created by WXX on 2021/2/24 23:24
public class Main {public static final int N = 110;static int n, m;static int[] v = new int[N], w = new int[N];// 邻接矩阵static int[] h = new int[N], e = new int[N], ne = new int[N];static int idx = 0;static int[][] f = new int[N][N];private static void add(int a, int b) {e[idx] = b; ne[idx] = h[a]; h[a] = idx++;}private static void dfs(int u) {for (int i = h[u]; i != -1; i = ne[i]) { // 遍历u的子节点,相当于循环物品组int son = e[i];dfs(son); // 求解完成子节点后才能求解当前节点// 分组背包for (int j = m - v[u]; j >= 0; j--) // 循环体积for (int k = 0; k <= j; k++) // 循环决策f[u][j] = Math.max(f[u][j], f[u][j - k] + f[son][k]);}// 将物品u加进去for (int i = m; i >= v[u]; i--) f[u][i] = f[u][i - v[u]] + w[u];for (int i = 0; i < v[u]; i++) f[u][i] = 0; // 不能放入物品u,则整棵子树都不能放入}public static void main(String[] args) {Scanner sn = new Scanner(System.in);n = sn.nextInt(); m = sn.nextInt();Arrays.fill(h, -1);int root = 1; // 根节点并不一定是1号点for (int i = 1; i <= n; i++) {v[i] = sn.nextInt(); w[i] = sn.nextInt();int p = sn.nextInt();if (p == -1) root = i;else add(p, i); // 添加一条由p指向i的边}dfs(root);System.out.println(f[root][m]);}
}
LeetCode上一些背包问题
Leetcode 0279 完全平方数
问题描述
问题链接:Leetcode 0279 完全平方数
分析
- 完全背包问题,把n当做背包容量,每个完全平方数当做物品,其对应的数值数值当做体积,每个数的价值都为1,则问题变成在恰好装满背包的情况下,总价值最少是多少。完全背包问题的时间复杂度为O(体积*物品个数),所以该题的时间复杂度为 O(n∗n)O(n* \sqrt n)O(n∗n)。
代码
- C++
/*** 执行用时:204 ms, 在所有 C++ 提交中击败了69.60%的用户* 内存消耗:8.7 MB, 在所有 C++ 提交中击败了83.16%的用户*/
class Solution {public:int numSquares(int n) {vector<int> f(n + 1, 1e9); // f[i] 代表 i 最少可以由几个完全平方数表示f[0] = 0;for (int i = 1; i * i <= n; i++) // 先循环物品 i*ifor (int j = i * i; j <= n; j++) // 再循环体积:体积从i*i开始f[j] = min(f[j], f[j - i * i] + 1);return f[n];}
};
- Java
/*** 执行用时:30 ms, 在所有 Java 提交中击败了86.04%的用户* 内存消耗:37.8 MB, 在所有 Java 提交中击败了34.09%的用户*/
public class Solution {public int numSquares(int n) {int[] f = new int[n + 1];Arrays.fill(f, Integer.MAX_VALUE);f[0] = 0;for (int i = 1; i * i <= n; i++) // 先循环物品 i*ifor (int j = i * i; j <= n; j++) // 再循环体积:体积从i*i开始f[j] = Math.min(f[j], f[j - i * i] + 1);return f[n];}
}
Leetcode 0322 零钱兑换
问题描述
问题链接:Leetcode 0322 零钱兑换
分析
- 完全背包问题
- amout为容量;物品体积为coins[i],价值为1
- 和上一题十分类似,这里就不详细分析了
代码
- C++
/*** 执行用时:64 ms, 在所有 C++ 提交中击败了89.56%的用户* 内存消耗:13.7 MB, 在所有 C++ 提交中击败了66.94%的用户*/
class Solution {public:int coinChange(vector<int> &coins, int m) {vector<int> f(m + 1, 1e8);f[0] = 0;for (auto v : coins)for (int j = v; j <= m; j++)f[j] = min(f[j], f[j - v] + 1);if (f[m] == 1e8) return -1;return f[m];}
};
- Java
/*** 执行用时:12 ms, 在所有 Java 提交中击败了94.69%的用户* 内存消耗:38.1 MB, 在所有 Java 提交中击败了22.43%的用户*/
public class Solution {public int coinChange(int[] coins, int m) {int[] dp = new int[m + 1];Arrays.fill(dp, m + 1);dp[0] = 0;for (int v : coins)for (int j = v; j <= m; j++)dp[j] = Math.min(dp[j], dp[j - v] + 1);return dp[m] == m + 1 ? -1 : dp[m];}
}
背包问题(背包九讲)相关推荐
- 背包问题——背包九讲
本帖主要为转载,只是供我学习,如有侵权,请联系我删除. 前言 本篇文章是我(dd_engi)正在进行中的一个雄心勃勃的写作计划的一部分,这个计划的内容是写作一份较为完善的NOIP难度的动态规划总结,名 ...
- dp背包九讲(待补充,暂时前两讲)
文章目录 背包九讲 一.01背包 二维dp 优化为一维 二.完全背包 二维dp 优化为一维 三.多重背包 数据范围很小时,变成01背包暴力 当数据范围较大时,利用二进制优化 二进制优化还不够用时,利用 ...
- 完全背包问题python_令人头疼的背包九讲(2)完全背包问题
重磅干货,第一时间送达 背包问题是一个经典的动态规划模型.它既简单形象容易理解,又在某种程度上能够揭示动态规划的本质,故不少教材都把它作为动态规划部分的第一道例题. 题目 完全背包问题 题目要求 有n ...
- 背包九讲之一:01背包问题
文章目录 说明: 01背包问题 题目 基本思路 初始化的细节问题 优化空间复杂度 相关题目练习 题目URL 输入格式 输出格式 数据范围 输入样例 输出样例: 题目解法 说明: 本文所讲内容摘录自崔添 ...
- 背包九讲之二:完全背包问题
文章目录 说明: 完全背包问题 题目 基本思路 一个简单有效的优化 相关题目练习 题目URL 输入格式 输出格式 数据范围 输入样例 输出样例: 题目解法 说明: 本文所讲内容摘录自崔添翼:背包九讲, ...
- dd大牛的背包九讲-背包问题汇总
背包九讲 目录 第一讲 01背包问题 第二讲 完全背包问题 第三讲 多重背包问题 第四讲 混合三种背包问题 第五讲 二维费用的背包问题 第六讲 分组的背包问题 第七讲 有依赖的背包问题 ...
- 第二讲 完全背包问题(对背包九讲的学习)
学习自:背包九讲 题目 有N种物品和一个容量为V的背包,每种物品都有无限件可用.第i种物品的费用是c[i],价值是w[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大. ...
- 【算法】动态规划+“背包九讲”原理超详细讲解+常见dp问题(9种)总结
目录 一.动态规划(DP) 二.背包九讲 (1)完全背包 P1616 疯狂的采药(完全背包) (2)01背包 滚动数组 一维数组 P1048 采药(01背包) 01背包表格图示 (3)多重背包 整数拆 ...
- 算法--背包九讲(详细讲解+代码)
背包九讲 目录 第一讲 01背包问题 第二讲 完全背包问题 第三讲 多重背包问题 第四讲 混合三种背包问题 第五讲 二维费用的背包问题 第六讲 分组的背包问题 第七讲 有依赖的背包问题 ...
- dalao的背包九讲
背包九讲 目录 第一讲 01背包问题 第二讲 完全背包问题 第三讲 多重背包问题 第四讲 混合三种背包问题 第五讲 二维费用的背包问题 第六讲 分组的背包问题 第七讲 有依赖的背包问题 ...
最新文章
- 1.Socket通信
- 我所遇到的C++连接问题汇总
- android自动填充包名,debug/release 修改包名,取不同包名下的agconnect-services.json 文件...
- java 8时间操作_Java8 时间日期类操作
- 如何使用粒子氩气进行位置跟踪
- The Rise of Worse Is Better 论文-学习笔记
- Cookie工具类-cookieUtil.js
- 中国电信天翼云进入4.0阶段 打造一朵无处不在的分布式云
- fc588热敏打印机驱动_为称重仪表设计热敏打印模块 - 工业自动化称重仪表
- 剑指Offer之寻找链表倒数第k个节点
- FreeNAS 安装与用户指导页
- 华为/华三IS-IS单区域配置
- 公司为什么需要自己的SAP内部顾问
- 你是《未来世界的幸存者》么?
- 如何进行航拍全景摄影(上)
- Android2018年最新前沿框架和技术
- 马未都说收藏:陶瓷篇(6、7)宋代八大民窑
- 十年感悟之 python之路
- sqlDBX 链接 mysql 提示ODBC驱动不正确
- windows7电脑备份系统教程
热门文章
- 基于Java毕业设计优课网设计与实现源码+系统+mysql+lw文档+部署软件
- PCB设计布局思路分析
- getch方法_c语言中中getch()函数为什么不起作用?
- linux数组fortran,Fortran77-90-95入门(十)--数组
- 《云计算核心技术剖析》学习笔记
- SQL Server详细教程
- [Python从零到壹] 十.网络爬虫之Selenium爬取在线百科知识万字详解(NLP语料构造必备技能)
- SecureCRT下载、安装、激活
- oracle中avl,AVL CRUISE软件功能简介
- 冰点下载器手机版apk_冰点文库下载器app下载