📖 故事的开始
想象一下,你是一位即将踏上冒险之旅的探险家。面前摆放着各种珍贵的物品:闪闪发光的珠宝、厚重的古籍、精密的相机、华丽的手表、强大的笔记本电脑、实用的帐篷,还有维持生命的食物。
但是,你的背包容量有限!你只能带走总重量不超过100公斤的物品。每件物品都有其独特的价值和重量,你需要做出明智的选择,让背包里的物品总价值最大化。
这就是经典的0-1背包问题——一个看似简单,却蕴含深刻数学智慧的优化问题。
🎯 问题的本质
什么是0-1背包问题?
0-1背包问题是这样定义的:
- 有一个容量为
W的背包 - 有
n个物品,每个物品有重量w[i]和价值v[i] - 每个物品要么完整地放入背包(选择1),要么不放入(选择0)
- 目标是在不超过背包容量的前提下,使背包中物品的总价值最大
数学表达
最大化:∑(v[i] * x[i]) 其中 i = 1 到 n
约束条件:∑(w[i] * x[i]) ≤ W
变量约束:x[i] ∈ {0, 1}
其中 x[i] 是决策变量,表示第i个物品是否被选中。
🗺️ 问题的可视化
让我们通过图表来直观理解这个问题:
📊 我们的冒险物品清单
让我们看看探险家面临的具体选择:
🧮 解决方案的思路
1. 暴力枚举法
最直接的方法是尝试所有可能的组合。对于7个物品,我们有 2^7 = 128 种可能的选择。
graph TD
A[所有可能组合] --> B[组合1: 0000000<br/>什么都不选]
A --> C[组合2: 0000001<br/>只选食物]
A --> D[组合3: 0000010<br/>只选帐篷]
A --> E[...]
A --> F[组合128: 1111111<br/>全部选择]
B --> G{检查重量约束}
C --> G
D --> G
E --> G
F --> G
G -->|满足约束| H[计算总价值]
G -->|违反约束| I[丢弃此组合]
H --> J[找出最大价值组合]
2. 0-1整数规划法
将问题建模为整数规划问题,使用我们开发的Java数学库:YiShape-Math。
package model_zoo.knapsack;
import com.reremouse.lab.math.optimize.linpg.*;
import com.reremouse.lab.math.linalg.IMatrix;
import com.reremouse.lab.math.linalg.IVector;
import com.reremouse.lab.math.linalg.Linalg;
import com.reremouse.lab.math.optimize.OptResult;
/**
* 🎒 探险家的背包问题:一场智慧与选择的博弈
*
*/
public class KnapsackProblem {
public static void main(String[] args) {
System.out.println("🎒=== 探险家的背包问题 ===🎒");
System.out.println("一场智慧与选择的博弈即将开始...");
System.out.println();
// 🎯 探险家的珍贵物品清单
// 每件物品都有其独特的价值和重量
String[] itemNames = {
"珠宝💎",
"古籍📚",
"相机📷",
"手表⌚",
"笔记本💻",
"帐篷⛺",
"食物🍎"
};
// 💰 每件物品的价值(探险家的评估)
double[] values = {60.0, 100.0, 120.0, 80.0, 150.0, 200.0, 50.0};
// ⚖️ 每件物品的重量(公斤)
double[] weights = {10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 10.0};
// 🎒 背包的最大承重能力
double capacity = 100.0;
// 📋 展示探险家面临的选择
System.out.println("🎯 探险家的物品清单:");
System.out.println("物品名称\t\t\t价值\t重量");
System.out.println("========================================");
for (int i = 0; i < itemNames.length; i++) {
System.out.printf("%-20s\t%.1f\t%.1f\n", itemNames[i], values[i], weights[i]);
}
System.out.println("========================================");
System.out.println("🎒 背包最大承重: " + capacity + " 公斤");
System.out.println();
// 🧮 将探险家的选择转化为数学问题
System.out.println("📐 数学建模(将现实问题转化为数学语言):");
System.out.println();
System.out.println("🎯 目标函数(最大化总价值):");
System.out.println(" 最大化: 60*x1 + 100*x2 + 120*x3 + 80*x4 + 150*x5 + 200*x6 + 50*x7");
System.out.println(" 其中 xi = 1 表示选择第i个物品,xi = 0 表示不选择");
System.out.println();
System.out.println("⚖️ 约束条件:");
System.out.println(" 重量约束(不能超过背包容量):");
System.out.println(" 10*x1 + 20*x2 + 30*x3 + 40*x4 + 50*x5 + 60*x6 + 10*x7 ≤ 100");
System.out.println();
System.out.println(" 0-1变量约束(每个物品要么选要么不选):");
System.out.println(" x1, x2, x3, x4, x5, x6, x7 ∈ {0, 1}");
System.out.println();
// 🔄 转换为求解器可处理的形式(求解器执行最小化)
// 最小化: -sum(values[i] * x[i]) 等价于最大化 sum(values[i] * x[i])
var c = Linalg.vector(values).multiplyScalar(-1.0);
// 📊 约束矩阵(重量约束)(向量先转为单列矩阵再转置为单行矩阵)
var A_ub = Linalg.vector(weights).asColumnVector().t();
// 📏 约束向量(背包容量限制)
var b_ub = Linalg.vector(new double[]{capacity});
// 🤖 创建整数规划求解器
RereIntegerProg solver = new RereIntegerProg();
// 🔢 设置所有变量为二进制变量(0-1变量)
solver.setAllVariablesBinary();
System.out.println("🔍 正在求解0-1整数规划问题...");
System.out.println("💭 探险家正在思考最优的选择策略...");
System.out.println();
// 🚀 求解0-1整数规划问题
OptResult result = solver.solve(c, A_ub, b_ub);
// ✅ 检查是否找到可行解
if (result == null) {
System.out.println("❌ 没有找到可行解!探险家陷入了困境...");
return;
}
// 📊 提取最优解
IVector solution = result.getOptimalPoint();
double optimalValue = -result.getOptimalValue(); // 转换回最大化问题的结果
// 🎉 输出最优解
System.out.println("🏆=== 探险家的最优选择 ===🏆");
System.out.println("📋 决策向量: " + solution);
System.out.println("💰 最大总价值: " + optimalValue);
System.out.println();
// 🔍 详细的解决方案分析
System.out.println("📈=== 选择分析 ===📈");
double totalWeight = 0;
double totalValue = 0;
System.out.println("🎒 探险家最终选择的物品:");
System.out.println("物品名称\t\t\t选择\t价值\t重量");
System.out.println("================================================");
for (int i = 0; i < solution.size(); i++) {
// 四舍五入处理数值精度问题
int selected = (int) Math.round(solution.get(i).doubleValue());
if (selected == 1) {
System.out.printf("%-20s\t%s\t%.1f\t%.1f\n", itemNames[i], "✅", values[i], weights[i]);
totalWeight += weights[i];
totalValue += values[i];
} else {
System.out.printf("%-20s\t%s\t%.1f\t%.1f\n", itemNames[i], "❌", values[i], weights[i]);
}
}
System.out.println("================================================");
System.out.println("📦 总重量: " + totalWeight + " ≤ " + capacity + " 公斤");
System.out.println("💎 总价值: " + totalValue);
System.out.println();
// 🔍 验证0-1约束
System.out.println("🔍=== 0-1约束验证 ===🔍");
boolean allBinary = true;
for (int i = 0; i < solution.size(); i++) {
double value = solution.get(i).doubleValue();
// 检查数值是否为0或1(考虑数值误差)
boolean isBinary = Math.abs(value) < 1e-6 || Math.abs(value - 1.0) < 1e-6;
allBinary &= isBinary;
System.out.printf("x%d = %.6f (是否为0-1: %s)\n", i+1, value, isBinary ? "✅是" : "❌否");
}
System.out.println("所有变量都是0-1: " + (allBinary ? "✅是" : "❌否"));
System.out.println();
// 📝 智慧总结
System.out.println("🎓=== 探险家的智慧总结 ===🎓");
System.out.println("这是一个经典的0-1整数规划问题(0-1背包问题)。");
System.out.println();
System.out.println("🔑 关键特征:");
System.out.println("1. 🔢 每个变量只能是0或1(要么选择,要么不选择)");
System.out.println("2. 🎯 目标是最大化总价值");
System.out.println("3. ⚖️ 受到重量约束的限制");
System.out.println("4. 🌳 使用分支定界法求解");
System.out.println();
System.out.println("💡 探险家学到的道理:");
System.out.println(" 在有限的资源下,智慧的选择比盲目的贪婪更有价值!");
System.out.println();
System.out.println("📖 想了解更多?请查看 knapsack_introduction.md 文档!");
}
}
🎯 最优解的发现
通过运行以上程序,我们可以得到如下精确的最优解!
让我们先分析一下各物品的价值密度,然后看看实际的最优解:
💡 价值密度分析
| 物品 | 价值 | 重量 | 价值密度 |
|---|---|---|---|
| 珠宝💎 | 60 | 10 | 6.0 |
| 食物🍎 | 50 | 10 | 5.0 |
| 古籍📚 | 100 | 20 | 5.0 |
| 相机📷 | 120 | 30 | 4.0 |
| 帐篷⛺ | 200 | 60 | 3.33 |
| 笔记本💻 | 150 | 50 | 3.0 |
| 手表⌚ | 80 | 40 | 2.0 |
🏆 实际最优解
运行Java程序后,我们发现最优策略是:
pie title 最优背包配置
"帐篷 (价值200)" : 200
"古籍 (价值100)" : 100
"珠宝 (价值60)" : 60
"食物 (价值50)" : 50
最优选择:珠宝💎 + 古籍📚 + 帐篷⛺ + 食物🍎
- 总价值:60 + 100 + 200 + 50 = 410
- 总重量:10 + 20 + 60 + 10 = 100 ≤ 100 ✅
这个结果告诉我们,贪心策略(仅按价值密度选择)并不总能得到最优解!虽然帐篷的价值密度不是最高的,但选择它能够获得更大的总价值。
🔍 最优解验证
让我们验证一下为什么这个解是最优的:
对比分析:
- 贪心策略解:珠宝(60) + 古籍(100) + 相机(120) + 食物(50) = 330,重量70kg
- 实际最优解:珠宝(60) + 古籍(100) + 帐篷(200) + 食物(50) = 410,重量100kg
0-1整数规划的优势:
- 完全利用容量:最优解恰好用满了100kg的容量限制
- 价值最大化:虽然帐篷的价值密度(3.33)低于相机(4.0),但帐篷的绝对价值更高
- 整体优化:选择帐篷后剩余空间(40kg)无法容纳其他高价值物品,因此这是全局最优解
这展示了整数规划相比贪心算法的优势:能够找到全局最优解而非局部最优解!
🔗 代码结构对应
| 文档概念 | Java代码实现 | 说明 |
|---|---|---|
| 物品清单 | itemNames[], values[], weights[] | 定义了7个物品的属性 |
| 背包容量 | capacity = 100.0 | 背包最大承重100公斤 |
| 目标函数 | IVector<Double> c | 转换为最小化问题的系数向量 |
| 重量约束 | IMatrix<Double> A_ub, IVector<Double> b_ub | 约束矩阵和约束向量 |
| 0-1变量 | solver.setAllVariablesBinary() | 设置所有变量为二进制 |
| 求解过程 | solver.solve(c, A_ub, b_ub) | 调用整数规划求解器 |
| 结果分析 | 详细的输出和验证代码 | 展示最优解和验证约束 |
🚀 算法的应用场景
背包问题不仅仅是一个数学游戏,它在现实世界中有着广泛的应用:
mindmap
root((背包问题应用))
资源分配
预算分配
人员配置
时间管理
投资组合
股票选择
项目投资
风险控制
物流运输
货物装载
路径优化
仓储管理
计算机科学
内存分配
任务调度
网络优化
🎯 代码下载:
GitHub: github.com/ScaleFree-T…
Gitee: gitee.com/scalefree-t…
💡 关键洞察
- 贪心策略的局限性:仅仅选择价值密度最高的物品并不总能得到最优解
- 组合优化的复杂性:看似简单的问题可能需要复杂的算法来求解
- 权衡的艺术:在有限资源下做出最优选择是一门艺术
- 数学建模的力量:将实际问题转化为数学模型,可以用计算机精确求解
背包问题教会我们:在有限的资源下,智慧的选择比盲目的贪婪更有价值。 🎒✨