贪心算法应用:多重背包启发式问题详解
多重背包问题是经典的组合优化问题,也是贪心算法的重要应用场景。本文将全面深入地探讨Java中如何利用贪心算法解决多重背包问题。
多重背包问题定义
**多重背包问题(Multiple Knapsack Problem)**是背包问题的变种,描述如下:
- 给定一个容量为W的背包
- 有n种物品,每种物品i有:
- 重量w_i
- 价值v_i
- 最大可用数量c_i(每种物品可以选择0到c_i个)
- 目标:在不超过背包容量的前提下,选择物品使总价值最大
与0-1背包(每种物品只能选0或1个)和完全背包(每种物品无限)不同,多重背包中物品有数量限制。
问题分析
数学表达
最大化:Σ(v_i * x_i) 对于i=1到n
约束条件:
- Σ(w_i * x_i) ≤ W 对于i=1到n
- 0 ≤ x_i ≤ c_i 且 x_i为整数
关键特性
- 组合爆炸:可能的组合数量为Π(c_i+1),直接枚举不可行
- NP难问题:没有已知的多项式时间解法
- 贪心可行:虽然不能保证最优解,但能得到近似解
贪心算法解决方案
1. 基本贪心策略
三种常见的贪心策略:
- 价值优先:按价值降序选择
- 重量优先:按重量升序选择
- 价值密度优先:按价值/重量(v_i/w_i)降序选择
实践证明,价值密度优先策略通常效果最好。
2. Java实现基础版
import java.util.Arrays;
import java.util.Comparator;
class Item {
int weight;
int value;
int count;
public Item(int weight, int value, int count) {
this.weight = weight;
this.value = value;
this.count = count;
}
}
public class MultipleKnapsack {
public static int greedySolution(Item[] items, int capacity) {
// 按价值密度排序
Arrays.sort(items, new Comparator<Item>() {
@Override
public int compare(Item a, Item b) {
double densityA = (double)a.value / a.weight;
double densityB = (double)b.value / b.weight;
return Double.compare(densityB, densityA); // 降序
}
});
int totalValue = 0;
int remainingCapacity = capacity;
for (Item item : items) {
if (remainingCapacity <= 0) break;
// 计算可以取多少个当前物品
int maxTake = Math.min(item.count, remainingCapacity / item.weight);
if (maxTake > 0) {
totalValue += maxTake * item.value;
remainingCapacity -= maxTake * item.weight;
}
}
return totalValue;
}
public static void main(String[] args) {
Item[] items = {
new Item(2, 10, 3),
new Item(3, 5, 2),
new Item(5, 15, 2),
new Item(7, 7, 3),
new Item(1, 6, 4)
};
int capacity = 15;
System.out.println("最大价值: " + greedySolution(items, capacity));
}
}
3. 算法复杂度分析
- 排序:O(n log n)
- 贪心选择:O(n)
总时间复杂度:O(n log n)
空间复杂度:O(1)(不包括输入存储)
改进的贪心算法
基础贪心算法可能不是最优的,我们可以通过以下方法改进:
1. 贪心+动态规划混合
public static int hybridSolution(Item[] items, int capacity) {
// 先按贪心算法得到一个解
int greedyValue = greedySolution(items, capacity);
// 对高价值物品尝试动态规划
Arrays.sort(items, (a, b) -> b.value - a.value);
int n = items.length;
int[] dp = new int[capacity + 1];
for (int i = 0; i < n; i++) {
Item item = items[i];
for (int k = 1; k <= item.count; k++) {
for (int w = capacity; w >= k * item.weight; w--) {
dp[w] = Math.max(dp[w], dp[w - k * item.weight] + k * item.value);
}
}
}
return Math.max(greedyValue, dp[capacity]);
}
2. 多阶段贪心选择
public static int multiPhaseGreedy(Item[] items, int capacity) {
// 阶段1:价值密度优先
int phase1 = greedySolution(items, capacity);
// 阶段2:纯价值优先
Arrays.sort(items, (a, b) -> b.value - a.value);
int phase2 = greedySolution(items, capacity);
// 阶段3:纯重量优先
Arrays.sort(items, (a, b) -> a.weight - b.weight);
int phase3 = greedySolution(items, capacity);
return Math.max(phase1, Math.max(phase2, phase3));
}
完全解与贪心解对比
动态规划解法(最优解)
public static int dpSolution(Item[] items, int capacity) {
int[] dp = new int[capacity + 1];
for (Item item : items) {
for (int w = capacity; w >= 0; w--) {
for (int k = 1; k <= item.count; k++) {
if (w >= k * item.weight) {
dp[w] = Math.max(dp[w], dp[w - k * item.weight] + k * item.value);
}
}
}
}
return dp[capacity];
}
对比分析
方法 | 时间复杂度 | 空间复杂度 | 解的质量 |
---|---|---|---|
纯贪心 | O(n log n) | O(1) | 近似 |
动态规划 | O(nWC_max) | O(W) | 最优 |
贪心+动态规划混合 | O(n log n + nWC_limited) | O(W) | 较好 |
其中C_max是最大物品数量,C_limited是限制的高价值物品数量
性能优化技巧
物品预处理:
- 移除重量>W的物品
- 合并相同物品
- 对价值密度相同的物品进行捆绑
搜索剪枝:
public static int greedyWithPruning(Item[] items, int capacity) { Arrays.sort(items, (a, b) -> Double.compare((double)b.value/b.weight, (double)a.value/a.weight)); int[] best = {0}; backtrack(items, 0, capacity, 0, best); return best[0]; } private static void backtrack(Item[] items, int index, int remaining, int currentValue, int[] best) { if (index == items.length || remaining == 0) { if (currentValue > best[0]) best[0] = currentValue; return; } Item item = items[index]; int maxTake = Math.min(item.count, remaining / item.weight); // 从最多开始尝试,贪心顺序 for (int k = maxTake; k >= 0 && (maxTake - k) <= 100; k--) { if (remaining - k * item.weight >= 0) { // 剪枝:如果剩余容量全部用下一个物品也无法超越当前最优 double upperBound = currentValue + k * item.value; if (index + 1 < items.length) { upperBound += (remaining - k * item.weight) * ((double)items[index+1].value/items[index+1].weight); } if (upperBound <= best[0]) break; backtrack(items, index + 1, remaining - k * item.weight, currentValue + k * item.value, best); } } }
并行处理:
public static int parallelGreedy(Item[] items, int capacity) { // 多种贪心策略并行执行 int[] results = new int[3]; Thread t1 = new Thread(() -> { Arrays.sort(items, (a, b) -> Double.compare((double)b.value/b.weight, (double)a.value/a.weight)); results[0] = greedySolution(items, capacity); }); Thread t2 = new Thread(() -> { Arrays.sort(items, (a, b) -> b.value - a.value); results[1] = greedySolution(items, capacity); }); Thread t3 = new Thread(() -> { Arrays.sort(items, (a, b) -> a.weight - b.weight); results[2] = greedySolution(items, capacity); }); t1.start(); t2.start(); t3.start(); try { t1.join(); t2.join(); t3.join(); } catch (InterruptedException e) {} return Math.max(results[0], Math.max(results[1], results[2])); }
实际应用场景
多重背包问题在现实中有广泛应用:
- 资源分配:服务器资源分配,选择最有价值的任务组合
- 投资组合:在有限资金下选择不同数量的多种投资产品
- 生产计划:原材料切割,最大化利用原材料
- 广告投放:在有限广告位中选择不同频次的广告组合
- 运输装载:货车装载多种货物,考虑数量限制
变种问题与扩展
1. 多维多重背包
每种物品有多个维度的重量限制(如体积和重量):
class MultiDimensionalItem {
int[] weights; // 各维度的重量
int value;
int count;
}
public static int multiDimensionalGreedy(MultiDimensionalItem[] items, int[] capacities) {
// 按综合价值密度排序
Arrays.sort(items, (a, b) -> {
double densityA = a.value / Arrays.stream(a.weights).sum();
double densityB = b.value / Arrays.stream(b.weights).sum();
return Double.compare(densityB, densityA);
});
int[] remaining = Arrays.copyOf(capacities, capacities.length);
int totalValue = 0;
for (MultiDimensionalItem item : items) {
boolean canTakeMore = true;
while (canTakeMore) {
// 检查是否可以再取一个当前物品
for (int i = 0; i < remaining.length; i++) {
if (remaining[i] < item.weights[i]) {
canTakeMore = false;
break;
}
}
if (canTakeMore && item.count > 0) {
totalValue += item.value;
item.count--;
for (int i = 0; i < remaining.length; i++) {
remaining[i] -= item.weights[i];
}
} else {
break;
}
}
}
return totalValue;
}
2. 分组多重背包
物品分为若干组,每组只能选择一定数量的物品:
class Group {
Item[] items;
int maxSelect; // 该组最多选择的物品数
}
public static int groupKnapsack(Group[] groups, int capacity) {
// 两层贪心:先对组排序,再对组内物品排序
Arrays.sort(groups, (a, b) -> {
double maxDensityA = Arrays.stream(a.items)
.mapToDouble(item -> (double)item.value/item.weight)
.max().orElse(0);
double maxDensityB = Arrays.stream(b.items)
.mapToDouble(item -> (double)item.value/item.weight)
.max().orElse(0);
return Double.compare(maxDensityB, maxDensityA);
});
int remaining = capacity;
int totalValue = 0;
for (Group group : groups) {
Arrays.sort(group.items, (a, b) -> Double.compare((double)b.value/b.weight, (double)a.value/a.weight));
int groupSelected = 0;
for (Item item : group.items) {
if (groupSelected >= group.maxSelect) break;
if (remaining <= 0) break;
int maxTake = Math.min(item.count, remaining / item.weight);
maxTake = Math.min(maxTake, group.maxSelect - groupSelected);
if (maxTake > 0) {
totalValue += maxTake * item.value;
remaining -= maxTake * item.weight;
groupSelected += maxTake;
}
}
}
return totalValue;
}
测试与验证
测试用例设计
应包含以下类型测试用例:
- 基础用例:简单验证功能
- 边界用例:空输入、单个物品、容量为0等
- 性能用例:大量物品测试算法效率
- 极端用例:所有物品重量相同、价值相同等特殊情况
JUnit测试示例
import org.junit.Test;
import static org.junit.Assert.*;
public class MultipleKnapsackTest {
@Test
public void testEmptyItems() {
Item[] items = {};
assertEquals(0, MultipleKnapsack.greedySolution(items, 10));
}
@Test
public void testZeroCapacity() {
Item[] items = {
new Item(2, 10, 3),
new Item(3, 5, 2)
};
assertEquals(0, MultipleKnapsack.greedySolution(items, 0));
}
@Test
public void testSingleItemWithinCapacity() {
Item[] items = {new Item(5, 20, 2)};
assertEquals(40, MultipleKnapsack.greedySolution(items, 10));
}
@Test
public void testSingleItemExceedCapacity() {
Item[] items = {new Item(15, 30, 3)};
assertEquals(0, MultipleKnapsack.greedySolution(items, 10));
}
@Test
public void testMixedItems1() {
Item[] items = {
new Item(2, 10, 3),
new Item(3, 5, 2),
new Item(5, 15, 2),
new Item(7, 7, 3),
new Item(1, 6, 4)
};
// 贪心解:3个重量1价值6 + 3个重量2价值10 = 3*6 + 3*10 = 48
assertEquals(48, MultipleKnapsack.greedySolution(items, 15));
}
@Test
public void testMixedItems2() {
Item[] items = {
new Item(10, 60, 1),
new Item(20, 100, 1),
new Item(30, 120, 1)
};
// 最优解是取重量20和30的物品
assertEquals(220, MultipleKnapsack.hybridSolution(items, 50));
}
@Test
public void testLargeInput() {
// 生成100个随机物品测试性能
Item[] items = new Item[100];
for (int i = 0; i < 100; i++) {
int w = 1 + (int)(Math.random() * 10);
int v = 1 + (int)(Math.random() * 20);
int c = 1 + (int)(Math.random() * 5);
items[i] = new Item(w, v, c);
}
int capacity = 100;
long start = System.nanoTime();
int result = MultipleKnapsack.greedySolution(items, capacity);
long end = System.nanoTime();
System.out.println("Large test result: " + result);
System.out.println("Time taken: " + (end - start)/1e6 + " ms");
assertTrue(result > 0);
}
}
算法选择指南
在实际应用中如何选择算法:
- 小规模问题(n < 100,W < 1000):使用动态规划获取精确解
- 中规模问题(100 < n < 10^4):使用贪心+动态规划混合方法
- 大规模问题(n > 10^4):使用纯贪心算法或并行贪心
- 实时系统:必须使用纯贪心算法保证响应时间
- 离线处理:可以使用更复杂的混合方法
常见问题与解决
贪心解与最优解差距大:
- 尝试多种贪心策略取最大值
- 结合局部动态规划
- 调整物品排序标准
性能瓶颈:
- 预处理移除无用物品
- 限制动态规划的物品范围
- 使用并行计算
整数溢出:
- 使用long类型存储大数值
- 添加边界检查
浮点精度问题:
- 使用分数比较代替浮点数
- 实现自定义比较器
总结
多重背包问题是组合优化中的经典问题,贪心算法提供了高效的近似解决方案。Java实现时需要注意:
- 物品排序策略的选择
- 贪心与动态规划的平衡
- 性能与解质量的权衡
- 各种边界条件的处理
通过合理选择和改进贪心策略,可以在大多数实际应用中获得满意的结果。对于需要精确解的小规模问题,可以结合动态规划;对于大规模问题,贪心算法是唯一可行的选择。
关键点总结:
- 价值密度优先的贪心策略通常效果最好
- 混合方法可以平衡效率和质量
- 预处理和剪枝能显著提高性能
- 测试要充分,特别是边界情况