1. 问题
分组的背包问题是一种扩展的背包问题,它的特点是将物品分为若干组,每组中的物品互相冲突,最多只能选择一件。给定一个背包的容量和每组物品的重量和价值,求如何选择物品使得背包内的总价值最大
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
2. 算法
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有:
f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于组k}
使用一维数组的伪代码如下:
for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}
注意这里的三层循环的顺序,甚至在本文的第一个beta版中我自己都写错了。“for v=V..0”这一层循环必须在“for 所有的i属于组k”之外。这样才能保证每一组内的物品最多只有一个会被添加到背包中。
另外,显然可以对每组内的物品应用P02中“一个简单有效的优化”。
分组的背包问题可以用动态规划来解决。我们可以定义一个二维数组dp [i] [j],表示前i组物品在容量为j的背包下能获得的最大价值。那么对于第i组物品,我们有两种选择:
- 不选这一组物品,那么dp [i] [j] = dp [i-1] [j];
- 选这一组物品中的某一件,假设是第k件,那么dp [i] [j] = dp [i-1] [j-w [k]] + v [k],其中w [k]和v [k]分别表示第k件物品的重量和价值。
因此,我们可以得到状态转移方程:
dp[i][j]=max{dp[i−1][j],k∈group[i]max(dp[i−1][j−wk]+vk)}
其中group [i]表示第i组物品中所有物品的编号集合。
3. 具体实现
3.1 二维实现
public class GroupKnapsack {
public static int groupKnapsack(int capacity, int[][] items) {
// items[i][0]表示第i个物品的重量
// items[i][1]表示第i个物品的价值
// items[i][2]表示第i个物品所属的组号
int n = items.length; // 物品总数
int m = 0; // 组数
for (int i = 0; i < n; i++) {
m = Math.max(m, items[i][2]); // 找出最大的组号
}
int[][] dp = new int[m + 1][capacity + 1]; // 创建动态规划数组
for (int i = 1; i <= m; i++) { // 遍历每一组
for (int j = 0; j <= capacity; j++) { // 遍历每一种容量
dp[i][j] = dp[i - 1][j]; // 先假设不选这一组物品
for (int k = 0; k < n; k++) { // 遍历每一个物品
if (items[k][2] == i && j >= items[k][0]) { // 如果物品属于这一组且容量足够
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - items[k][0]] + items[k][1]); // 更新最大价值
}
}
}
}
return dp[m][capacity]; // 返回最终结果
}
public static void main(String[] args) {
int capacity = 10; // 背包容量
int[][] items = {{2, 1, 1}, {3, 3, 1}, {4, 8, 2}, {6, 9, 2}, {2, 8, 3}, {3, 9, 3}}; // 物品信息
System.out.println(groupKnapsack(capacity, items)); // 输出最大价值
}
}
3.2 一维实现
public class GroupKnapsack {
public static int groupKnapsack(int capacity, int[][] items) {
// items[i][0]表示第i个物品的重量
// items[i][1]表示第i个物品的价值
// items[i][2]表示第i个物品所属的组号
int n = items.length; // 物品总数
int m = 0; // 组数
for (int i = 0; i < n; i++) {
m = Math.max(m, items[i][2]); // 找出最大的组号
}
int[] dp = new int[capacity + 1]; // 创建动态规划数组
for (int i = 1; i <= m; i++) { // 遍历每一组
for (int j = capacity; j >= 0; j--) { // 逆向遍历每一种容量
for (int k = 0; k < n; k++) { // 遍历每一个物品
if (items[k][2] == i && j >= items[k][0]) { // 如果物品属于这一组且容量足够
dp[j] = Math.max(dp[j], dp[j - items[k][0]] + items[k][1]); // 更新最大价值
}
}
}
}
return dp[capacity]; // 返回最终结果
}
public static void main(String[] args) {
int capacity = 10; // 背包容量
int[][] items = {{2, 1, 1}, {3, 3, 1}, {4, 8, 2}, {6, 9, 2}, {2, 8, 3}, {3, 9, 3}}; // 物品信息
System.out.println(groupKnapsack(capacity, items)); // 输出最大价值
}
}
自有实现及带测试案例:
public class GroupKnapsack {
public static void main(String[] args) {
int capacity = 10; // 背包容量
// items[i][0]表示第i个物品的重量
// items[i][1]表示第i个物品的价值
// items[i][2]表示第i个物品所属的组号
int[][] items = {{2, 1, 1}, {3, 3, 1}, {4, 8, 2}, {6, 9, 2}, {2, 8, 3}, {3, 9, 3}}; // 物品信息
System.out.println(dp(capacity, items));
System.out.println(dp2(capacity, items));
}
public static int dp(int capacity, int[][] items) {
int n = items.length;
int m = 0;
for (int i = 0; i < n; i++) {
m = Math.max(m, items[i][2]);
}
int[][] dp = new int[m + 1][capacity + 1];
for (int i = 1; i <= m; i++) {
for (int j = 0; j <= capacity; j++) {
dp[i][j] = dp[i - 1][j];
for (int k = 0; k < n; k++) {
if (items[k][2] == i && j >= items[k][0]) {
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - items[k][0]] + items[k][1]);
}
}
}
}
return dp[m][capacity];
}
public static int dp2(int capacity, int[][] items) {
int n = items.length;
int m = 0;
for (int i = 0; i < items.length; i++) {
m = Math.max(m, items[i][2]);
}
int[] dp = new int[capacity + 1];
for (int i = 1; i <= m; i++) {
for (int j = capacity; j >= 0; j--) {
for (int k = 0; k < n; k++) {
if (items[k][2] == i && j >=items[k][0]) {
dp[j] = Math.max(dp[j], dp[j - items[k][0]] + items[k][1]);
}
}
}
}
return dp[capacity];
}
}
运行:
20
20
4. 小结
分组的背包问题将彼此互斥的若干物品称为一个组,这建立了一个很好的模型。不少背包问题的变形都可以转化为分组的背包问题(例如P07),由分组的背包问题进一步可定义“泛化物品”的概念,十分有利于解题。
5. 参考资料
- C++ 算法篇 动态规划----背包之六 分组背包 - CSDN博客 https://blog.csdn.net/weixin_43736974/article/details/108606665
- 动态规划之背包问题系列 - 知乎 https://zhuanlan.zhihu.com/p/93857890
- 背包问题算法全解析:动态规划和贪心算法详解 - 知乎 https://zhuanlan.zhihu.com/p/625429755
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
2021-07-01 DDD 中的那些模式 — CQRS
2021-07-01 领域驱动设计系列 (六):CQRS
2021-07-01 中台架构与实现:基于ddd和微服务 下载_终于有人把前台、中台、后台都讲明白了..
2021-07-01 中台架构与实现——基于DDD和微服务
2021-07-01 电商之下:服务类商品订单履约系统如何设计
2021-07-01 从0到1设计订单系统的思路
2021-07-01 京东后台:订单履约系统设计(上)