4. 资源分配中的线性规划实战

当资源分配问题中的目标函数和约束条件都能写成决策变量的线性表达式时,线性规划(LP)就是最直接、最可靠的求解工具。本节以“生产计划中的机器与人力分配”为案例,完整走一遍从业务描述到模型建立、代码求解、再到灵敏度分析与影子价格解读的闭环。案例中的参数均为教学示例,用于说明方法,不代表任何真实企业的实测数据。

一、把业务问题翻译成线性规划模型

假设某车间生产两种产品 A 和 B,需要消耗两类关键资源:机器工时和人工工时。已知单件消耗、资源可用上限与单位利润如下表(示例数据):

项目产品 A产品 B资源上限
机器工时(小时/件)21100
人工工时(小时/件)13120
单位利润(元/件)4030—

设 A、B 的产量分别为 x 和 y,则模型为:

max  Z = 40x + 30y
s.t. 2x +  y ≤ 100   (机器工时)
      x + 3y ≤ 120   (人工工时)
      x ≥ 0, y ≥ 0

这里有两个关键约定需要在全文保持一致:一是所有约束都写成“≤ 上限”的形式,右端项代表资源可用量;二是目标为最大化利润。若换成最小化成本,影子价格的符号解读方向要相应调整,后文会说明。

二、用 PuLP 建模与求解

PuLP 是纯 Python 的开源建模库,适合教学与中小规模问题。核心步骤是:定义问题方向、声明变量、添加目标与约束、调用求解器、读取结果。

import pulp

prob = pulp.LpProblem("machine_labor_plan", pulp.LpMaximize)
x = pulp.LpVariable("A", lowBound=0)
y = pulp.LpVariable("B", lowBound=0)

prob += 40*x + 30*y                 # 目标函数
prob += 2*x + 1*y <= 100            # 机器工时
prob += 1*x + 3*y <= 120            # 人工工时

prob.solve(pulp.PULP_CBC_CMD(msg=False))
print(pulp.LpStatus[prob.status])   # Optimal
print(x.value(), y.value(), pulp.value(prob.objective))

求解结果为 x = 36、y = 28,最大利润 Z = 40×36 + 30×28 = 2280 元。可以验证:机器工时 2×36+28 = 100,人工工时 36+3×28 = 120,两条约束都取等号,说明两类资源同时被用尽,属于“紧约束”。这正是灵敏度分析要重点关注的信号。

三、Gurobi 的等价写法

若问题规模变大或需要更高求解性能,可换用 Gurobi。建模思路完全一致,只是 API 不同:

import gurobipy as gp
from gurobipy import GRB

m = gp.Model("plan")
x = m.addVar(lb=0, name="A")
y = m.addVar(lb=0, name="B")
m.setObjective(40*x + 30*y, GRB.MAXIMIZE)
m.addConstr(2*x + y <= 100, "machine")
m.addConstr(x + 3*y <= 120, "labor")
m.optimize()
print(x.X, y.X, m.ObjVal)

两种工具得到的解应一致。工程上建议把参数(利润、消耗系数、资源上限)抽成字典或配置文件,模型代码只负责组装,这样改数据不必改逻辑,也便于批量做情景对比。

可行域与最优解(示例:A=36, B=28, Z=2280) 产品A产量 x 产品B产量 y 0 20 40 60 20 40 60 2x+y=100(机器) x+3y=120(人工) 最优解 (36, 28) 等利润线 40x+30y=Z 图例 可行域 机器约束 人工约束 最优顶点

四、灵敏度分析与影子价格

最优解只是“给定参数下的一个答案”,管理者更关心“参数变了会怎样”。灵敏度分析回答两类问题:目标系数在多大范围内变化时最优解不变;约束右端项每增加一个单位,最优目标值能改善多少,后者就是影子价格(对偶价格)。

在 PuLP 中,CBC 求解器可通过导出 LP 文件再读取灵敏度信息,或直接使用支持该功能的求解器接口。更通用的做法是手工做“右端项扰动”实验:把机器工时上限从 100 依次改为 101、102,重新求解并观察 Z 的变化。

for cap in [100, 101, 102, 110]:
    p = pulp.LpProblem("sens", pulp.LpMaximize)
    a = pulp.LpVariable("A", lowBound=0)
    b = pulp.LpVariable("B", lowBound=0)
    p += 40*a + 30*b
    p += 2*a + b <= cap
    p += a + 3*b <= 120
    p.solve(pulp.PULP_CBC_CMD(msg=False))
    print(cap, a.value(), b.value(), pulp.value(p.objective))

示例结果会显示:机器上限每增加 1 小时,利润增加约 18 元,因此机器工时的影子价格约为 18 元/小时;而人工工时在 120 附近增加 1 小时,利润增加约 4 元,影子价格约为 4 元/小时。含义很直观:如果外协机器工时的市场价低于 18 元/小时,就值得购买;高于这个价格则得不偿失。人工同理,以 4 元/小时为决策门槛。

影子价格只在“当前基不变”的范围内有效。当右端项变化过大,最优解的紧约束组合可能改变,影子价格会跳变。因此做扩产决策时,应结合允许增量范围(RHS ranging)判断,而不是无限外推。

五、结果解读与常见陷阱

解读结果时建议按以下顺序检查:先看求解状态是否为 Optimal,再看哪些约束取等号(紧约束),然后核对影子价格与资源外购价格的比较,最后检查目标系数允许范围是否覆盖了未来的价格波动。常见陷阱包括:把“≤”约束的松弛量误读为浪费(松弛量大于零说明该资源未用尽,其影子价格为零);在最小化问题中直接套用最大化问题的符号结论;以及忽略整数性——若产品必须整件生产,应改用整数规划,此时 LP 松弛解只能作为上界参考。

把上述流程固化为可复用的分析脚本后,面对新的产品组合或资源上限,只需替换参数字典即可快速得到新的最优方案与影子价格,从而把线性规划真正变成日常资源分配决策的工具,而不仅是一次性的课堂练习。