目录
一、问题描述
多维费用的多重背包问题是0-1背包问题的常见拓展,具体可以描述为:
假设有:
-
n种物品。 -
一个最大容量为
W的背包,但限制不仅有重量,还有体积等。为简化,我们以二维(重量、体积)为例,可以轻松推广到更高维。 -
对于第
i种物品:-
有
c_i件可用(有限数量)。 -
每件的重量是
w_i。 -
每件的体积是
v_i。 -
每件的价值是
val_i。
-
-
目标:选择若干物品放入背包,使得总重量不超过
W,总体积不超过V,且总价值最大。每种物品最多选择c_i件。
二、常规动态规划解法
1. 状态定义
将状态从一维扩展到多维,以容纳多种约束和物品数量限制。
我们定义一个二维数组 dp:
dp[j][k]表示:在总重量不超过 j,总体积不超过 k 的条件下,能获得的最大价值。
2. 状态转移方程(重中之重!)
对于每一种物品 i,我们都需要考虑选择 0, 1, 2, ..., c_i 件的情况。
最直观的转移是枚举当前物品选择的件数 t:
dp[j][k] = max( dp[j][k], dp[j - t * w_i][k - t * v_i] + t * val_i )
注意:t的取值范围是0 <= t <= min(c_i, j // w_i, k // v_i)。即不能超过物品的件数上限,也不能超过当前剩余重量和体积。
3.代码实现
public class MultiDimensionalKnapsack {
/**
* 多维费用的多重背包问题
* @param n 物品数量
* @param W 背包重量容量
* @param V 背包体积容量
* @param weights 物品重量数组
* @param volumes 物品体积数组
* @param values 物品价值数组
* @param counts 物品数量限制数组
* @return 最大价值
*/
public static int solveMultiDimensionalKnapsack(int n, int W, int V,
int[] weights, int[] volumes,
int[] values, int[] counts) {
// 初始化 DP 表,所有值设为0("不超过"初始化)
int[][] dp = new int[W + 1][V + 1];
// 遍历每种物品
for (int i = 0; i < n; i++) {
int w_i = weights[i];
int v_i = volumes[i];
int val_i = values[i];
int c_i = counts[i];
// 遍历背包的重量容量(逆序)
for (int j = W; j >= w_i; j--) {
// 遍历背包的体积容量(逆序)
for (int k = V; k >= v_i; k--) {
// 枚举选择当前物品的件数 t
int maxT = Math.min(c_i, Math.min(j / w_i, k / v_i));
for (int t = 0; t <= maxT; t++) {
if (j >= t * w_i && k >= t * v_i) {
dp[j][k] = Math.max(dp[j][k],
dp[j - t * w_i][k - t * v_i] + t * val_i);
}
}
}
}
}
// 答案就是 dp[W][V]
return dp[W][V];
}
public static void main(String[] args) {
System.out.println("=== 示例1:基础示例 ===");
int n = 3; // 物品数量
int W = 10; // 背包重量容量
int V = 8; // 背包体积容量
// 物品重量数组
int[] weights = {2, 3, 4};
// 物品体积数组
int[] volumes = {1, 2, 3};
// 物品价值数组
int[] values = {3, 4, 5};
// 物品数量限制数组
int[] counts = {2, 3, 1};
// 调用多维背包算法
int result = solveMultiDimensionalKnapsack(
n, W, V, weights, volumes, values, counts);
System.out.println("物品数量: " + n);
System.out.println("背包容量: 重量=" + W + ", 体积=" + V);
System.out.println("物品重量: " + java.util.Arrays.toString(weights));
System.out.println("物品体积: " + java.util.Arrays.toString(volumes));
System.out.println("物品价值: " + java.util.Arrays.toString(values));
System.out.println("物品数量限制: " + java.util.Arrays.toString(counts));
System.out.println("最大价值: " + result);
System.out.println();
}
}
三、二进制拆分转化为0-1背包问题
上述解法有四重循环,在物品数量、背包容量或物品件数较大时效率很低。可以利用二进制拆分将多重背包转化为0-1背包,从而去掉最内层枚举件数 t 的循环。
1.为什么需要二进制拆分?
枚举每个物品选择0,1,2,...,c_i件,这导致时间复杂度为O(c_i)。当c_i很大时(比如1000),效率很低。
对于任意正整数c,都可以用2的幂次方的和来表示:
c = 2^0 + 2^1 + 2^2 + ... + 2^(k-1) + r 其中 0 ≤ r < 2^k
2. 详细拆分过程
-
拆分步骤:
-
从1开始,每次乘以2,直到超过剩余数量
-
处理剩余部分作为最后一组
-
以c_i = 13为例的详细拆分:
步骤1:按2的幂次拆分
剩余数量s = 13 k = 1: 取1件,剩余12件 k = 2: 取2件,剩余10件 k = 4: 取4件,剩余6件 k = 8: 8 > 6,停止
步骤2:处理剩余部分
剩余6件,直接作为最后一组
最终分组:1, 2, 4, 6
-
验证组合能力:
1 = 1 2 = 2 3 = 1+2 4 = 4 5 = 1+4 6 = 2+4 7 = 1+2+4 8 = 2+6 9 = 1+2+6 10 = 4+6 11 = 1+4+6 12 = 2+4+6 13 = 1+2+4+6可以看到,用这4个数可以组合出1~13的所有整数!
3.代码实现
import java.util.ArrayList;
import java.util.List;
public class MultiDimensionalKnapsackBinary {
/**
* 物品类,用于存储拆分后的新物品
*/
static class Good {
int weight;
int volume;
int value;
public Good(int weight, int volume, int value) {
this.weight = weight;
this.volume = volume;
this.value = value;
}
}
/**
* 使用二进制优化的多维费用多重背包(预拆分版本)
*/
public static int solveMultiDimensionalKnapsackBinary(int n, int W, int V,
int[] weights, int[] volumes,
int[] values, int[] counts) {
// 初始化 DP 表
int[][] dp = new int[W + 1][V + 1];
// 用于存储拆分后的"新物品"
List<Good> goods = new ArrayList<>();
// 二进制拆分过程
for (int i = 0; i < n; i++) {
int w_i = weights[i];
int v_i = volumes[i];
int val_i = values[i];
int c_i = counts[i];
int s = c_i; // 当前物品的原始数量
int k = 1;
// 二进制拆分
while (k <= s) {
// 创建一个"新物品"
goods.add(new Good(k * w_i, k * v_i, k * val_i));
s -= k;
k *= 2;
}
// 处理剩余部分
if (s > 0) {
goods.add(new Good(s * w_i, s * v_i, s * val_i));
}
}
// 现在问题转化为:对 goods 列表中的"新物品"做二维费用的0-1背包问题
for (Good good : goods) {
int w = good.weight;
int v = good.volume;
int val = good.value;
// 逆序遍历二维容量
for (int j = W; j >= w; j--) {
for (int k = V; k >= v; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - w][k - v] + val);
}
}
}
// 答案就是 dp[W][V]
return dp[W][V];
}
public static void main(String[] args) {
int n = 3, W = 10, V = 8;
int[] weights = {2, 3, 4};
int[] volumes = {1, 2, 3};
int[] values = {3, 4, 5};
int[] counts = {13, 7, 5};
// 二进制优化版本
int result = solveWithBinaryOptimization(n, W, V, weights, volumes, values, counts);
System.out.println("最大价值: " + result); // 输出: 最大价值: 15
}
}
四、算法提升效率对比
-
复杂度分析
| 算法版本 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 暴力解法 | O(n×W×V×cᵢ) | O(W×V) |
| 二进制优化 | O(n×W×V×logcᵢ) | O(W×V) |
-
效率提升
假设 c_i = 1000:
-
基础版本:内层循环1000次
-
二进制优化:内层循环约10次(log₂1000 ≈ 10)
-
效率提升约100倍!
五、适用场景
-
货物装载:卡车、集装箱装载优化
-
资源分配:云计算中的VM分配
-
投资组合:资金和风险双约束下的最优投资
-
生产计划:原材料和产能限制下的生产安排
六、进阶思考
-
能否进一步优化空间复杂度?(单调队列优化)
-
如何记录具体的选择方案?
-
如果有三个约束维度(重量、体积、价值)怎么办?

1325

被折叠的 条评论
为什么被折叠?



