背包问题:贪心算法与0-1整数规划(基于Java YiShape-Math求解)

109 阅读7分钟

📖 故事的开始

想象一下,你是一位即将踏上冒险之旅的探险家。面前摆放着各种珍贵的物品:闪闪发光的珠宝、厚重的古籍、精密的相机、华丽的手表、强大的笔记本电脑、实用的帐篷,还有维持生命的食物。

但是,你的背包容量有限!你只能带走总重量不超过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个物品是否被选中。

🗺️ 问题的可视化

让我们通过图表来直观理解这个问题:

微信图片_20250929133621_27_15.png

📊 我们的冒险物品清单

让我们看看探险家面临的具体选择:

微信图片_20250929134036_28_15.png

🧮 解决方案的思路

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 文档!");
    }
}

🎯 最优解的发现

通过运行以上程序,我们可以得到如下精确的最优解!

微信图片_20250929134415_29_15.png

让我们先分析一下各物品的价值密度,然后看看实际的最优解:

💡 价值密度分析

物品价值重量价值密度
珠宝💎60106.0
食物🍎50105.0
古籍📚100205.0
相机📷120304.0
帐篷⛺200603.33
笔记本💻150503.0
手表⌚80402.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整数规划的优势

  1. 完全利用容量:最优解恰好用满了100kg的容量限制
  2. 价值最大化:虽然帐篷的价值密度(3.33)低于相机(4.0),但帐篷的绝对价值更高
  3. 整体优化:选择帐篷后剩余空间(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…

💡 关键洞察

  1. 贪心策略的局限性:仅仅选择价值密度最高的物品并不总能得到最优解
  2. 组合优化的复杂性:看似简单的问题可能需要复杂的算法来求解
  3. 权衡的艺术:在有限资源下做出最优选择是一门艺术
  4. 数学建模的力量:将实际问题转化为数学模型,可以用计算机精确求解

背包问题教会我们:在有限的资源下,智慧的选择比盲目的贪婪更有价值。 🎒✨