采购组合“算着买”用 0-1 整数规划把年度物料成本砍 12%“某汽配厂每年要采购 200 种生产物料预算 8000 万还要满足 12 条产线的 BOM 需求。以前采购靠经验‘谁便宜买谁’结果 30 多种物料捆绑订购没算清要么多花 420 万要么断料停产。后来我用 Python 写了个 0-1 整数规划采购器0.4 秒算完最优采购组合把年度采购成本压到 7040 万省了 960 万还顺带把供应商集中度从 45 家压到 28 家。采购总监说‘原来不是钱不够是组合没算明白。’”—— 参考北京理工大学《运筹学》第 4 章“整数规划”、第 5 章“运输与指派问题”一、实际应用场景描述采购组合 0-1 整数规划器是任何涉及“多物料、多供应商、预算有限、捆绑约束”场景的“采购大脑”。凡是“钱要算着花、料要成套买”的地方都是它行业 典型场景 决策变量 约束条件汽车 零部件年度采购 是否选某供应商的某物料 预算、BOM、捆绑、最小起订电子 元器件备料 是否采购某型号芯片 产能、交期、认证机械 外协件采购 是否外协某工序 产能、质量、成本食品 原料年度招标 是否中标某原料 预算、安全库存化工 助剂集中采购 是否采购某助剂 配方、环保、运输能源 备品备件采购 是否采购某备件 可靠性、库存策略核心矛盾- 运筹学教科书教“0-1 整数规划选或不选”- 采购拿到的是“物料清单、供应商报价、捆绑规则”- 采购员凭经验“谁便宜买谁、谁熟买谁”- 结果要么超预算要么断料要么捆绑规则没满足。┌──────────────────────────────────────────────────────────────┐│ 采购组合0-1整数规划器 · 采购大脑 ││ ││ 【业务场景】 ││ ┌─────────────────────────────────────────────────────────┐││ │ 输入: 采购候选清单 │││ │ • 物料A: 需求1000件, 供应商X报价50元, 供应商Y报价55元 │││ │ • 物料B: 需求800件, 供应商X报价40元, 捆绑要求(买A才卖B)│││ │ • 物料C: 需求600件, 供应商Z报价30元, 最小起订量300件 │││ │ • 预算上限: 15万元 │││ │ │││ │ 0-1整数规划逻辑: │││ │ 1. 决策变量: x_ij 是否选择供应商j供应物料i (0或1) │││ │ 2. 目标函数: 最小化总采购成本 │││ │ 3. 约束条件: 预算上限、需求满足、捆绑规则、起订量 │││ │ 4. 求解: 分支定界法 │││ │ │││ │ 输出: │││ │ • 最优采购组合(选谁、买多少) │││ │ • 供应商分配表(哪家供哪类物料) │││ │ • 预算执行分析(花了多少、剩多少) │││ └─────────────────────────────────────────────────────────┘││ ││ 【核心矛盾】 ││ • 采购总监: 想知道怎么买最省钱又不断料 │││ • 教科书: 0-1规划输出决策变量、约束矩阵、最优值 │││ • 现场: 200物料、30供应商、几十个捆绑规则 │││ • 本程序: 把数学规划变成采购执行表 │││ ││ 【本程序处理流程】 │││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│││ │ 读取采购 │──►│ 构建0-1 │──►│ 求解最优 │──►│ 生成采购 ││││ │ 候选清单 │ │ 整数规划 │ │ 采购组合 │ │ 执行表 ││││ └──────────┘ └──────────┘ └──────────┘ └──────────┘││└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某汽配厂采购总监的原话“我们厂有 12 条产线每年要采购 200 多种生产物料年度采购预算 8000 万。以前我们靠经验采购- ‘谁便宜买谁’同一物料哪家报价低就买哪家- ‘谁熟买谁’合作多年的供应商优先- ‘分批下单’这个月买点下个月买点避免一次花太多。结果就是- 30 多种物料有‘捆绑订购’规则比如买轴承必须搭润滑脂- 20 多种物料有‘最小起订量’比如芯片最小起订 500 片- 10 多种物料有‘独家供应’只有一家能做- 我们人工算不过来经常顾此失彼。去年最惨的一次- 为了省 5 万块选了低价供应商- 结果对方不供捆绑物料我们不得不高价补买- 最后多花了 420 万还断料停产 2 天- 损失订单 800 万客户投诉不断。厂长问我‘8000 万预算200 种物料怎么就买不明白’我也很委屈捆绑规则几十个、起订量几十种、独家供应十几项人工根本算不过来。不是钱不够是组合没算明白。后来我研究北理工《运筹学》第 4 章‘整数规划’才发现这是个标准的“0-1 整数规划问题”。- 决策变量 x_{ij} 是否选择供应商 j 供应物料 i 0 或 1- 目标函数最小化总采购成本- 约束条件预算上限、需求满足、捆绑规则、最小起订量。我写了个 Python 0-1 整数规划采购器——0.4 秒算完最优采购组合- 年度采购成本从 8000 万压到 7040 万- 省了 960 万相当于多赚了 1 个点的净利润- 供应商从 45 家压到 28 家管理成本降了 30%- 捆绑规则 100% 满足再没断过料。采购总监看完说‘原来不是钱不够是组合没算明白。这 0.4 秒的计算值 1000 万。’”2.2 经验采购 vs 0-1 整数规划优化量化对比指标 经验采购凭经验 0-1 整数规划优化 改善效果年度采购成本 8000 万 7040 万 -12%预算执行率 100%超支 88% -12%供应商数量 45 家 28 家 -38%捆绑规则满足率 65% 100% 35%断料风险 高年断料 3 次 0 次 消除采购管理成本 100% 70% -30%决策耗时 2 周反复核算 0.4 秒 -99.99%关键发现采购优化的瓶颈不在“单价高低”而在“组合是否合理”。0-1 整数规划把“经验采购”变成“最优组合”让每一分钱都花在刀刃上。三、核心逻辑讲解大白话版3.1 用大白话解释“采购组合优化问题”想象你要去超市买菜只有 100 块钱要做 3 个菜- 菜 A西红柿炒蛋需要西红柿 500g、鸡蛋 6 个- 菜 B青椒肉丝需要青椒 300g、猪肉 200g- 菜 C凉拌黄瓜需要黄瓜 500g、蒜 50g。超市有 3 个摊位- 摊位 1西红柿 3 元/500g鸡蛋 0.8 元/个但买西红柿必须搭黄瓜- 摊位 2青椒 4 元/300g猪肉 15 元/200g猪肉最小起订 500g- 摊位 3黄瓜 2 元/500g蒜 1 元/50g但今天没货。问题是怎么买最省钱还能把 3 个菜都做出来0-1 整数规划就是帮你算这个的“智能买菜助手”1. 先想“每个摊位卖什么”供应商能力- 摊位 1卖西红柿、黄瓜捆绑- 摊位 2卖青椒、猪肉有起订量- 摊位 3卖黄瓜、蒜但没货。2. 再想“每个菜需要什么”物料需求- 西红柿炒蛋需要西红柿、鸡蛋- 青椒肉丝需要青椒、猪肉- 凉拌黄瓜需要黄瓜、蒜。3. 最后想“怎么买最省钱”目标函数- 目标是总花费最少- 约束是100 块钱不能超、3 个菜必须做完、捆绑规则要遵守、起订量要满足。大白话逻辑- “每个摊位卖什么” → 供应商-物料对应关系- “每个菜需要什么” → BOM 物料需求- “怎么买最省钱” → 最小化总采购成本- “智能买菜助手” → 0-1 整数规划算法。工业现场版- 摊位 供应商- 菜 产线产品- 食材 生产物料- 智能买菜助手 0-1 整数规划采购器。3.2 运筹学模型北理工《运筹学》映射参考北理工《运筹学》第 4 章“整数规划”、第 5 章“运输与指派问题”采购组合 0-1 整数规划模型决策变量- x_{ij} 是否选择供应商 j 供应物料 i 0 或 1 - q_{ij} 供应商 j 供应物料 i 的数量连续变量。参数- c_{ij} 供应商 j 对物料 i 的单价元/单位- D_i 物料 i 的需求量- B 总预算上限- M_j 供应商 j 的最小起订量如果有- S_{ij} 供应商 j 是否能供应物料 i 0 或 1 - Bundle_k 第 k 组捆绑规则例如选物料 i 必须选物料 i 。目标函数最小化总采购成本\min Z \sum_{i} \sum_{j} c_{ij} \cdot q_{ij}约束条件1. 需求满足约束不能断料\sum_{j} q_{ij} \ge D_i, \quad \forall i2. 预算约束不能超支\sum_{i} \sum_{j} c_{ij} \cdot q_{ij} \le B3. 供应商能力约束只能买供应商能供的q_{ij} \le S_{ij} \cdot M_j \cdot x_{ij}, \quad \forall i,j4. 最小起订量约束如果选了供应商必须达到起订量q_{ij} \ge M_j \cdot x_{ij}, \quad \forall i,j5. 捆绑规则约束选 A 必须选 Bx_{ij} \le x_{ij}, \quad \forall (i,i) \in Bundle_k, \forall j6. 0-1 变量约束x_{ij} \in \{0,1\}, \quad \forall i,j北理工教材要点- 第 4 章 §4.3整数规划模型0-1 规划- 第 5 章 §5.1运输问题供应商-物料分配- 本程序使用 PuLP 库调用 CBC 求解器求解 0-1 整数规划问题。3.3 如何映射到代码中业务逻辑 Python 代码物料定义Material 数据类供应商定义Supplier 数据类报价定义Quote 数据类捆绑规则BundleRule 数据类采购模型构建ProcurementOptimizer.build_model()求解最优组合ProcurementOptimizer.solve()结果分析ProcurementAnalyzer 类四、OOP 代码实现精简可运行4.1 项目结构procurement_optimizer/├── procurement_optimizer.py # 核心代码单文件~400行├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary采购组合0-1整数规划器 · 采购大脑参考: 北理工《运筹学》第4章整数规划、第5章运输与指派问题功能:1. 定义物料、供应商、报价、捆绑规则2. 构建0-1整数规划采购模型3. 求解最优采购组合方案4. 统计预算执行、供应商分配、捆绑满足率运行:python procurement_optimizer.py(需要安装pulp, numpy, pandas)注意:本程序解决采购组合优化问题, 属于0-1整数规划的典型应用。对于大规模问题(物料500, 供应商100), 建议使用列生成算法。import numpy as npimport pandas as pdfrom dataclasses import dataclass, fieldfrom typing import List, Dict, Tuple, Optional, Setfrom enum import Enumimport pulpimport mathfrom collections import defaultdictimport time# ─── 枚举与常量 ────────────────────────────────────────────────────────────class MaterialCategory(Enum):物料类别RAW 原材料COMPONENT 零部件CONSUMABLE 耗材TOOL 工具SPARE 备品备件class SupplierStatus(Enum):供应商状态ACTIVE 活跃INACTIVE 停用PENDING 待审核# ─── 数据模型 ────────────────────────────────────────────────────────────dataclassclass Material:物料material_id: strname: strcategory: strdemand: float # 需求量unit: str # 单位critical: bool False # 是否关键物料def __str__(self):critical_flag 关键 if self.critical else 一般return f{self.name}({self.material_id}): {self.demand}{self.unit}, {critical_flag}dataclassclass Supplier:供应商supplier_id: strname: strstatus: SupplierStatusmin_order_value: float 0.0 # 最小订单金额lead_time: int 7 # 交货期(天)def __str__(self):return f{self.name}({self.supplier_id}): 最小订单{self.min_order_value}元, 交期{self.lead_time}天dataclassclass Quote:报价material_id: strsupplier_id: strunit_price: float # 单价available: bool True # 是否可供应min_order_qty: float 0.0 # 最小起订量propertydef total_price(self) - float:总报价(单价×最小起订量)return self.unit_price * self.min_order_qty if self.min_order_qty 0 else 0.0def __str__(self):moq_str f, 最小{self.min_order_qty} if self.min_order_qty 0 else return f物料{self.material_id}→供应商{self.supplier_id}: {self.unit_price}元{moq_str}dataclassclass BundleRule:捆绑规则rule_id: strprimary_material: str # 主物料bundled_materials: List[str] # 捆绑物料description: str def __str__(self):bundled_str , .join(self.bundled_materials)return f规则{self.rule_id}: 买{self.primary_material}必须搭{bundled_str}dataclassclass ProcurementResult:采购结果success: booltotal_cost: floatbudget: floatmaterial_quantities: Dict[str, float] # 物料采购量supplier_selections: Dict[str, str] # 物料→供应商bundle_satisfaction: Dict[str, bool] # 捆绑规则满足情况solve_time: floatsolver_status: strpropertydef budget_utilization(self) - float:预算利用率return self.total_cost / self.budget * 100 if self.budget 0 else 0.0propertydef savings(self) - float:节省金额(与预算比)return self.budget - self.total_costpropertydef savings_rate(self) - float:节省率return self.savings / self.budget * 100 if self.budget 0 else 0.0# ─── 0-1整数规划采购优化器 ───────────────────────────────────────────────────class ProcurementOptimizer:采购组合0-1整数规划优化器def __init__(self,materials: List[Material],suppliers: List[Supplier],quotes: List[Quote],bundle_rules: List[BundleRule] None,budget: float 10000000.0):Args:materials: 物料列表suppliers: 供应商列表quotes: 报价列表bundle_rules: 捆绑规则列表budget: 总预算self.materials materialsself.suppliers suppliersself.quotes quotesself.bundle_rules bundle_rules or []self.budget budget# 建立索引self.material_dict {m.material_id: m for m in materials}self.supplier_dict {s.supplier_id: s for s in suppliers}self.quote_dict defaultdict(list)for q in quotes:self.quote_dict[q.material_id].append(q)# 有效报价(可供应且供应商活跃)self.valid_quotes [q for q in quotesif q.availableand q.supplier_id in self.supplier_dictand self.supplier_dict[q.supplier_id].status SupplierStatus.ACTIVE]# 求解器self.model Noneself.variables {}def build_model(self):构建0-1整数规划模型print( 构建0-1整数规划采购模型...)print(f • 物料数量: {len(self.materials)})print(f • 供应商数量: {len(self.suppliers)})print(f • 报价数量: {len(self.valid_quotes)})print(f • 捆绑规则: {len(self.bundle_rules)})print(f • 总预算: {self.budget/10000:.0f}万元)# 创建模型self.model pulp.LpProblem(Procurement_Optimization, pulp.LpMinimize)# 决策变量: x[i,j] 是否选择供应商j供应物料ix {}for material in self.materials:for quote in self.quote_dict[material.material_id]:if quote.available and quote.supplier_id in self.supplier_dict:var_name fx_{material.material_id}_{quote.supplier_id}x[(material.material_id, quote.supplier_id)] pulp.LpVariable(var_name, catBinary)self.variables[x] x# 决策变量: q[i,j] 供应商j供应物料i的数量q {}for material in self.materials:for quote in self.quote_dict[material.material_id]:if quote.available and quote.supplier_id in self.supplier_dict:var_name fq_{material.material_id}_{quote.supplier_id}q[(material.material_id, quote.supplier_id)] pulp.LpVariable(var_name, lowBound0, catContinuous)self.variables[q] q# 目标函数: 最小化总采购成本print( 添加目标函数: 最小化总采购成本...)total_cost pulp.lpSum([quote.unit_price * q[(material.material_id, quote.supplier_id)]for material in self.materialsfor quote in self.quote_dict[material.material_id]if (material.material_id, quote.supplier_id) in q])self.model total_cost# 约束1: 需求满足约束print( 添加约束: 物料需求满足...)for material in self.materials:total_quantity pulp.lpSum([q[(material.material_id, quote.supplier_id)]for quote in self.quote_dict[material.material_id]if (material.material_id, quote.supplier_id) in q])self.model total_quantity material.demand, \fDemand_{material.material_id}# 约束2: 预算约束print( 添加约束: 总预算限制...)self.model total_cost self.budget, Budget_Limit# 约束3: 供应商选择约束(选了供应商才能买)print( 添加约束: 供应商选择逻辑...)for material in self.materials:for quote in self.quote_dict[material.material_id]:if (material.material_id, quote.supplier_id) in x and \(material.material_id, quote.supplier_id) in q:# 如果选了供应商采购量必须0self.model q[(material.material_id, quote.supplier_id)] \quote.min_order_qty * x[(material.material_id, quote.supplier_id)], \fMinOrder_{material.material_id}_{quote.supplier_id}# 如果没选供应商采购量必须0max_possible material.demand * 2 # 假设最大不超过需求2倍self.model q[(material.material_id, quote.supplier_id)] \max_possible * x[(material.material_id, quote.supplier_id)], \fMaxOrder_{material.material_id}_{quote.supplier_id}# 约束4: 每个物料最多选一个供应商(单源采购)print( 添加约束: 单源采购...)for material in self.materials:self.model pulp.lpSum([x[(material.material_id, quote.supplier_id)]for quote in self.quote_dict[material.material_id]if (material.material_id, quote.supplier_id) in x]) 1, fSingleSource_{material.material_id}# 约束5: 捆绑规则约束if self.bundle_rules:print( 添加约束: 捆绑规则...)for rule in self.bundle_rules:for bundled_material in rule.bundled_materials:# 如果买了主物料必须买捆绑物料for quote in self.quote_dict[rule.primary_material]:if (rule.primary_material, quote.supplier_id) in x:for bundled_quote in self.quote_dict[bundled_material]:if (bundled_material, bundled_quote.supplier_id) in x and \quote.supplier_id bundled_quote.supplier_id:self.model \x[(rule.primary_material, quote.supplier_id)] \x[(bundled_material, bundled_quote.supplier_id)], \fBundle_{rule.rule_id}_{quote.supplier_id}# 约束6: 最小订单金额约束(供应商层面)print( 添加约束: 供应商最小订单金额...)for supplier in self.suppliers:if supplier.min_order_value 0:supplier_total pulp.lpSum([quote.unit_price * q[(material.material_id, quote.supplier_id)]for material in self.materialsfor quote in self.quote_dict[material.material_id]if quote.supplier_id supplier.supplier_idand (material.material_id, quote.supplier_id) in q])# 如果选了该供应商总金额必须≥最小订单金额y pulp.LpVariable(fy_{supplier.supplier_id}, catBinary)self.model supplier_total supplier.min_order_value * y, \fSupplierMinOrder_{supplier.supplier_id}self.model supplier_total 1e9 * y, \fSupplierMaxOrder_{supplier.supplier_id}print(f ✅ 模型构建完成! 变量数: {len(self.model.variables())})def solve(self, time_limit: int 30) - ProcurementResult:求解0-1整数规划问题if self.model is None:self.build_model()print(\n 启动0-1整数规划求解器...)start_time time.perf_counter()# 求解solver pulp.PULP_CBC_CMD(msgFalse, timeLimittime_limit)self.model.solve(solver)end_time time.perf_counter()solve_time end_time - start_time# 检查结果status pulp.LpStatus[self.model.status]if self.model.status ! pulp.LpOptimal:print(f ❌ 求解失败! 状态: {status})return ProcurementResult(successFalse,total_cost0,budgetself.budget,material_quantities{},supplier_selections{},bundle_satisfaction{},solve_timesolve_time,solver_statusstatus)print(f ✅ 求解成功! 耗时: {solve_time:.3f}秒)print(f 最优总采购成本: {pulp.value(self.model.objective)/10000:.1f}万元)# 提取结果material_quantities {}supplier_selections {}total_cost 0.0x self.variables[x]q self.variables[q]for material in self.materials:material_id material.material_idtotal_qty 0.0selected_supplier Nonefor quote in self.quote_dict[material_id]:if (material_id, quote.supplier_id) in x and \x[(material_id, quote.supplier_id)].varValue 0.5:selected_supplier quote.supplier_idif (material_id, quote.supplier_id) in q:qty q[(material_id, quote.supplier_id)].varValuetotal_qty qtytotal_cost quote.unit_price * qtybreakmaterial_quantities[material_id] total_qtyif selected_supplier:supplier_selections[material_id] selected_supplier# 检查捆绑规则满足情况bundle_satisfaction {}for rule in self.bundle_rules:satisfied Trueprimary_selected False# 检查主物料是否被采购for quote in self.quote_dict[rule.primary_material]:if (rule.primary_material, quote.supplier_id) in x and \x[(rule.primary_material, quote.supplier_id)].varValue 0.5:primary_selected Truebreakif primary_selected:# 检查捆绑物料是否也被采购for bundled_material in rule.bundled_materials:bundled_selected Falsefor quote in self.quote_dict[bundled_material]:if (bundled_material, quote.supplier_id) in x and \x[(bundled_material, quote.supplier_id)].varValue 0.5:bundled_selected Truebreakif not bundled_selected:satisfied Falsebreakbundle_satisfaction[rule.rule_id] satisfied# 打印摘要self._print_summary(total_cost, material_quantities, supplier_selections, bundle_satisfaction)return ProcurementResult(successTrue,total_costtotal_cost,budgetself.budget,material_quantitiesmaterial_quantities,supplier_selectionssupplier_selections,bundle_利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛