基础篇:Pulp库求解规划模型方法
前言
PuLP 是 Python 生态中最流行的线性规划与整数规划建模库之一。它将优化问题的数学建模转化为简洁的 Python 代码,底层自动调用 CBC、Gurobi、CPLEX 等求解器进行计算。对于熟悉线性规划理论但希望用代码快速实现的人来说,PuLP 是首选工具。
适用读者
本教程面向已经了解线性规划基本概念的读者。如果你已经知道以下概念,就可以直接开始:
目标函数、决策变量、约束条件
可行域、最优解、紧约束
线性规划与整数规划的区别
本教程不讲解什么是线性规划、单纯形法的数学原理等内容,而是聚焦于如何用 PuLP 把数学模型写成代码。
内容结构
环境说明
Python 版本:3.8+
PuLP 版本:2.x
默认求解器:CBC(PuLP 内置,无需额外安装)
可视化:Matplotlib(需配置中文字体
SimHei)
所有代码示例均可直接复制运行。可视化部分使用 plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans'] 配置中文显示。
运行方式
# 安装 PuLP(如未安装)
pip install pulp
# 安装可视化依赖
pip install matplotlib numpy
import pulp # 导入即可开始建模
学习建议
先通读第 1 章,掌握五步流程模板
第 2、3 章是核心基础,建议边读边跑代码
第 4 章的场景可以直接作为数模论文的模板套用
第 5 章为进阶内容,按需查阅即可
第1节 快速开始
本节通过一个完整的生产计划问题示例,展示使用 PuLP 求解线性规划的基本流程。
1.1 问题描述
某工厂生产两种产品 A 和 B,每件产品 A 利润为 40 元,每件产品 B 利润为 30 元。生产受到以下资源限制:
目标是确定两种产品的产量,使得总利润最大。
数学模型:
1.2 PuLP 实现
线性规划问题的求解在 PuLP 中遵循固定五步流程:创建问题 → 定义变量 → 添加约束 → 求解 → 提取结果。
import pulp
# 第1步:创建问题实例,pulp.LpMaximize 表示最大化
prob = pulp.LpProblem("生产计划问题", pulp.LpMaximize)
# 第2步:定义决策变量,lowBound=0 表示非负约束
x1 = pulp.LpVariable("产品A产量", lowBound=0, cat="Continuous")
x2 = pulp.LpVariable("产品B产量", lowBound=0, cat="Continuous")
# 第3步:添加目标函数和约束
prob += 40 * x1 + 30 * x2, "总利润" # 目标函数
prob += 2 * x1 + x2 <= 100, "原材料约束" # 约束1
prob += x1 + 2 * x2 <= 80, "工时约束" # 约束2
# 第4步:求解(默认使用 CBC 求解器)
prob.solve()
# 第5步:提取结果
print(f"求解状态: {pulp.LpStatus[prob.status]}")
print(f"最优解: 产品A = {x1.varValue:.2f}, 产品B = {x2.varValue:.2f}")
print(f"最大利润: {pulp.value(prob.objective):.2f} 元")
for name, constraint in prob.constraints.items():
print(f"约束 [{name}] 松弛量 = {constraint.slack:.2f}")
求解状态: Optimal
最优解: 产品A = 40.00, 产品B = 20.00
最大利润: 2200.00 元
约束 [原材料约束] 松弛量 = -0.00
约束 [工时约束] 松弛量 = -0.00
关键点:
LpProblem(name, sense)创建问题,sense为LpMaximize或LpMinimizeLpVariable(name, lowBound, upBound, cat)定义变量,cat控制变量类型prob += expression, "label"同时支持添加目标函数和约束,字符串为标签prob.solve()调用求解器,返回状态码varValue读取变量值,pulp.value()读取表达式值constraint.slack读取约束松弛量(负零表示约束恰好紧)
1.3 结果可视化
对于两个变量的线性规划问题,可以绘制可行域和最优解的几何图示。
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 生成约束边界线
x = np.linspace(0, 60, 200)
y1 = 100 - 2 * x # 原材料约束: 2x1 + x2 <= 100
y2 = (80 - x) / 2 # 工时约束: x1 + 2x2 <= 80
fig, ax = plt.subplots(figsize=(8, 6))
# 填充可行域
y_feasible = np.minimum(y1, y2)
y_feasible = np.maximum(y_feasible, 0)
ax.fill_between(x, 0, y_feasible, where=(x >= 0) & (x <= 50),
alpha=0.2, color='skyblue', label='可行域')
# 绘制约束线
ax.plot(x, y1, '--', color='#FF5722', linewidth=2, label='原材料约束 (2x1+x2=100)')
ax.plot(x, y2, '--', color='#2196F3', linewidth=2, label='工时约束 (x1+2x2=80)')
# 标注最优解
opt_x, opt_y = 40, 20
ax.plot(opt_x, opt_y, 'ro', markersize=12, zorder=5)
ax.annotate(f'最优解\n(40, 20)\n利润=2200', xy=(opt_x, opt_y),
xytext=(opt_x + 5, opt_y + 10), fontsize=11, fontweight='bold',
arrowprops=dict(arrowstyle='->', color='red'))
# 绘制等利润线
x_obj = np.linspace(0, 60, 100)
for z in [800, 1400, 2200]:
y_obj = (z - 40 * x_obj) / 30
mask = (y_obj >= 0) & (y_obj <= 60)
ax.plot(x_obj[mask], y_obj[mask], color='#4CAF50', alpha=0.4, linewidth=1)
ax.plot([], [], color='#4CAF50', alpha=0.6, label='等利润线')
ax.set_xlim(0, 60)
ax.set_ylim(0, 60)
ax.set_xlabel('产品 A 产量', fontsize=12)
ax.set_ylabel('产品 B 产量', fontsize=12)
ax.set_title('线性规划可行域与最优解', fontsize=14, fontweight='bold')
ax.legend(loc='upper right', fontsize=10)
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.show()

几何视角:
可行域是所有约束不等式交集构成的凸多边形区域
最优解一定出现在可行域的某个顶点上(这是线性规划的基本定理)
等利润线沿梯度方向平移,最后接触可行域的点即为最优解
本例中两个约束都是紧约束(松弛量为 0),最优解恰好是两个约束边界的交点
1.4 代码结构模板
所有 PuLP 求解线性规划的代码都遵循相同的结构模板,后续章节的所有例子都套用此模板:
1.5 本节小结
PuLP 求解线性规划遵循"创建问题 → 定义变量 → 添加约束 → 求解 → 提取结果"五步流程
+=运算符同时支持添加目标函数和约束LpStatus[prob.status]将状态码转为可读字符串两个变量的问题可在二维平面上绘制可行域,最优解出现在顶点处
约束的
slack属性告诉我们哪些约束是紧的(松弛量为 0)
第2节 变量与约束详解
PuLP 中的变量和约束定义非常灵活,本节系统讲解各类用法。
2.1 三种变量类型
PuLP 支持三种变量类型,通过 LpVariable 的 cat 参数指定:
对比示例
同样在 x <= 7.5 约束下最大化 x,三种变量的求解结果不同:
import pulp
# 连续变量
prob_c = pulp.LpProblem("连续变量示例", pulp.LpMaximize)
xc = pulp.LpVariable("连续变量", lowBound=0, upBound=10, cat="Continuous")
prob_c += xc
prob_c += xc <= 7.5
prob_c.solve()
print(f"连续变量: x = {xc.varValue}") # 7.5
# 整数变量
prob_i = pulp.LpProblem("整数变量示例", pulp.LpMaximize)
xi = pulp.LpVariable("整数变量", lowBound=0, upBound=10, cat="Integer")
prob_i += xi
prob_i += xi <= 7.5
prob_i.solve()
print(f"整数变量: x = {xi.varValue}") # 7.0
# 二元变量
prob_b = pulp.LpProblem("二元变量示例", pulp.LpMaximize)
xb = pulp.LpVariable("二元变量", cat="Binary")
prob_b += xb
prob_b.solve()
print(f"二元变量: x = {xb.varValue}") # 1.0
连续变量: x = 7.5
整数变量: x = 7.0
二元变量: x = 1.0
关键点:
连续变量会取到约束边界的精确值(7.5)
整数变量会自动向下取整(7.0),因为不能超过 7.5
二元变量在最大化目标下取 1(唯一可选的非零值)
cat参数大小写不敏感,但建议用首字母大写形式

2.2 批量创建变量:LpVariable.dicts
当变量数量较多时,逐个定义非常繁琐。LpVariable.dicts 用字典形式一次性创建多个变量:
products = ['A', 'B', 'C']
prices = {'A': 50, 'B': 30, 'C': 40}
prob = pulp.LpProblem("批量变量示例", pulp.LpMaximize)
x = pulp.LpVariable.dicts("产量", products, lowBound=0, cat='Continuous')
# 目标函数:最大化总收入
prob += pulp.lpSum([prices[p] * x[p] for p in products])
# 约束
prob += x['A'] + x['B'] + x['C'] <= 100, "总产能"
prob += x['A'] <= 40, "产品A上限"
prob += x['B'] <= 35, "产品B上限"
prob.solve()
print(f"求解状态: {pulp.LpStatus[prob.status]}")
for p in products:
print(f" 产品 {p} 产量 = {x[p].varValue:.2f}")
print(f" 总收入 = {pulp.value(prob.objective):.2f}")
求解状态: Optimal
产品 A 产量 = 40.00
产品 B 产量 = 0.00
产品 C 产量 = 60.00
总收入 = 4400.00
关键点:
LpVariable.dicts(name, keys, lowBound, upBound, cat)返回一个字典,键为keys中的每个元素pulp.lpSum()是 PuLP 的求和函数,支持列表推导式写法求解器会优先分配高单价产品(A 单价 50,C 单价 40,B 单价 30)
字典键可以是字符串、数字、元组等任意可哈希类型
2.3 约束的多种添加方式
方式 1:+= 运算符(最常用)
prob += expression, "label"
+= 是 PuLP 最简洁的约束添加方式,支持目标函数和约束的统一定义:
prob += 2 * x1 + x2 <= 100, "原材料约束"
prob += x1 + 2 * x2 <= 80, "工时约束"
方式 2:addConstraint 方法
prob.addConstraint(expression, name)
与方法 1 等价,适合需要条件判断添加约束的场景:
for product in products:
if product in limited_products:
prob.addConstraint(x[product] <= limits[product], f"{product}_上限")
方式 3:不等式两边都放表达式
prob += 2 * w + 3 <= 15, "2w+3<=15"
PuLP 支持不等式两边都放表达式,会自动整理为标准形式:
prob = pulp.LpProblem("方式3", pulp.LpMaximize)
w = pulp.LpVariable("w", lowBound=0)
prob += w
prob += 2*w + 3 <= 15, "2w+3<=15"
prob.solve()
print(f"w = {w.varValue}") # 6.0
方式 4:使用 lpSum 构建复杂约束
items = pulp.LpVariable.dicts("item", [1, 2, 3], lowBound=0, cat='Integer')
prob += pulp.lpSum(items[i] for i in [1, 2, 3]) <= 10, "总和约束"
prob += items[1] >= 2, "item1下限"
lpSum 支持生成器表达式,写法简洁:
items = pulp.LpVariable.dicts("item", [1, 2, 3], lowBound=0, cat='Integer')
prob = pulp.LpProblem("方式4", pulp.LpMaximize)
prob += pulp.lpSum(items[i] for i in [1, 2, 3])
prob += pulp.lpSum(items[i] for i in [1, 2, 3]) <= 10
prob += items[1] >= 2
prob.solve()
print(f"item1={items[1].varValue}, item2={items[2].varValue}, item3={items[3].varValue}")
item1=2.0, item2=8.0, item3=0.0
方式 5:等式约束
除了 <= 和 >=,PuLP 同样支持等式约束 ==:
prob += x1 + x2 == 100, "总产量必须等于100"
2.4 变量范围设置
变量定义时可通过 lowBound 和 upBound 直接指定范围:
v1 = pulp.LpVariable("v1_无界") # 无界,默认 lowBound=-inf
v2 = pulp.LpVariable("v2_下界", lowBound=5) # 下界 5
v3 = pulp.LpVariable("v3_上界", upBound=20) # 上界 20
v4 = pulp.LpVariable("v4_双侧", lowBound=3, upBound=15) # 双侧界 [3, 15]
综合示例:
prob = pulp.LpProblem("变量范围示例", pulp.LpMaximize)
v1 = pulp.LpVariable("v1_无界")
v2 = pulp.LpVariable("v2_下界", lowBound=5)
v3 = pulp.LpVariable("v3_上界", upBound=20)
v4 = pulp.LpVariable("v4_双侧", lowBound=3, upBound=15)
prob += v1 + v2 + v3 + v4
prob += v1 <= 10
prob += v2 <= 25
prob += v3 >= 5
prob += v4 <= 20 # 冗余约束(已定义 upBound=15)
prob.solve()
print(f"v1 (无界): = {v1.varValue:.2f}") # 10.00
print(f"v2 (low>=5): = {v2.varValue:.2f}") # 25.00
print(f"v3 (up<=20): = {v3.varValue:.2f}") # 20.00
print(f"v4 (3<=v<=15): = {v4.varValue:.2f}") # 15.00
v1 (无界): = 10.00
v2 (low>=5): = 25.00
v3 (up<=20): = 20.00
v4 (3<=v<=15): = 15.00
关键点:
默认
lowBound为负无穷,upBound为正无穷变量定义时的边界和后续添加的约束共同生效
冗余约束不影响求解结果,但会增加求解时间
2.5 本节小结
三种变量类型:
Continuous(连续)、Integer(整数)、Binary(二元)LpVariable.dicts批量创建变量,搭配pulp.lpSum写目标函数和约束更简洁约束添加最常用
+=方式,addConstraint适合条件化添加支持
<=、>=、==三种关系运算符变量范围在定义时通过
lowBound/upBound指定
第3节 求解与结果提取
本节讲解如何配置求解器、解读求解状态、提取完整的求解结果,以及理解影子价格。
3.1 求解状态码
prob.solve() 返回求解状态码,通过 pulp.LpStatus 字典可以将其转换为可读字符串。
各状态示例
import pulp
# Optimal — 找到最优解
prob = pulp.LpProblem("最优示例", pulp.LpMaximize)
x = pulp.LpVariable("x", lowBound=0)
prob += x
prob += x <= 5
prob.solve()
print(f"状态码={prob.status}, 名称={pulp.LpStatus[prob.status]}")
# 状态码=1, 名称=Optimal
# Unbounded — 无界(最大化 x 但无上界)
prob2 = pulp.LpProblem("无界示例", pulp.LpMaximize)
y = pulp.LpVariable("y", lowBound=0)
prob2 += y
prob2.solve()
print(f"状态码={prob2.status}, 名称={pulp.LpStatus[prob2.status]}")
# 状态码=-2, 名称=Unbounded
# Infeasible — 不可行(矛盾约束)
prob3 = pulp.LpProblem("不可行示例", pulp.LpMaximize)
z = pulp.LpVariable("z", lowBound=0)
prob3 += z
prob3 += z <= 3
prob3 += z >= 5
prob3.solve()
print(f"状态码={prob3.status}, 名称={pulp.LpStatus[prob3.status]}")
# 状态码=-1, 名称=Infeasible
关键点:
务必在读取结果前检查状态码,只有
Optimal时结果才可靠实际代码中建议用条件判断:
if pulp.LpStatus[prob.status] == 'Optimal': ...
3.2 提取求解结果
求解完成后,可通过以下属性提取完整信息:
prob = pulp.LpProblem("完整示例", pulp.LpMaximize)
a = pulp.LpVariable("a", lowBound=0)
b = pulp.LpVariable("b", lowBound=0)
c = pulp.LpVariable("c", lowBound=0)
prob += 3*a + 5*b + 2*c, "目标函数"
prob += a + b + c <= 10, "资源1"
prob += 2*a + b <= 12, "资源2"
prob += b + 2*c <= 14, "资源3"
prob.solve()
# 1. 求解状态
print(f"状态: {pulp.LpStatus[prob.status]}") # Optimal
# 2. 目标函数值
print(f"目标值: {pulp.value(prob.objective)}") # 50.0
# 3. 变量值
for var in prob.variables():
print(f" {var.name} = {var.varValue}")
# a = 0.0
# b = 10.0
# c = 0.0
# 4. 约束松弛量
for name, constr in prob.constraints.items():
print(f" [{name}] 松弛量 = {constr.slack}")
# [资源1] 松弛量 = -0.0 (紧约束)
# [资源2] 松弛量 = 2.0 (松弛 2 单位)
# [资源3] 松弛量 = 4.0 (松弛 4 单位)
关键点:
prob.variables()返回所有变量的迭代器,按字典序排列var.varValue是变量值,未求解时为Noneconstr.slack为约束右端减左端的差值,0表示紧约束pulp.value(expr)可计算任意 PuLP 表达式的值

3.3 影子价格(对偶值)
影子价格(constr.pi)表示约束右端增加一个单位时,目标函数的变化量:
for name, constr in prob.constraints.items():
print(f" [{name}] 影子价格 = {constr.pi}")
[资源1] 影子价格 = 5.0000
[资源2] 影子价格 = -0.0000
[资源3] 影子价格 = -0.0000
经济含义:资源 1 的影子价格为 5,意味着资源 1 的可用量每增加 1 单位,总利润将增加 5。资源 2 和 3 的影子价格为 0(因为它们有松弛量),增加这些资源不会改善目标值。
验证:
# 资源1从10增加到11
prob2 = pulp.LpProblem("验证", pulp.LpMaximize)
a2 = pulp.LpVariable("a2", lowBound=0)
b2 = pulp.LpVariable("b2", lowBound=0)
c2 = pulp.LpVariable("c2", lowBound=0)
prob2 += 3*a2 + 5*b2 + 2*c2
prob2 += a2 + b2 + c2 <= 11 # 增加 1 单位
prob2 += 2*a2 + b2 <= 12
prob2 += b2 + 2*c2 <= 14
prob2.solve()
print(f"新目标值: {pulp.value(prob2.objective)}") # 55.0
print(f"差值: {55.0 - 50.0}") # 5.0 (等于影子价格)
3.4 求解器配置
PuLP 默认使用内置的 CBC 求解器(开源免费)。可用求解器列表:
# 列出所有已安装的求解器
solvers = pulp.listSolvers(onlyAvailable=True)
print(solvers) # ['PULP_CBC_CMD', 'HiGHS']
切换求解器
# 使用默认 CBC(不传参数)
prob.solve()
# 显式指定 CBC 并关闭输出
prob.solve(pulp.PULP_CBC_CMD(msg=0))
# 使用 HiGHS 求解器
prob.solve(pulp.HiGHS())
# 使用 Gurobi(需安装 Gurobi 许可证)
# prob.solve(pulp.GUROBI())
常用求解器对比
求解器参数
大多数求解器支持额外参数:
# CBC: 设置时间限制和日志级别
prob.solve(pulp.PULP_CBC_CMD(timeLimit=60, msg=1, threads=4))
# Gurobi: 设置时间限制和 MIP 间隙
prob.solve(pulp.GUROBI(timeLimit=300, mipgap=0.01))
常用参数:
msg=0:关闭求解器控制台输出(推荐在脚本中使用)timeLimit:最大求解时间(秒),超时后返回当前最优解mipgap:MIP 问题的可接受间隙,设为 0.01 表示 1% 以内即停止
3.5 本节小结
通过
pulp.LpStatus[prob.status]检查求解状态,确保为Optimalprob.variables()迭代所有变量,var.varValue读取变量值constr.slack读取约束松弛量,constr.pi读取影子价格影子价格为正表示该约束是紧的,增加右端可以改善目标
默认 CBC 求解器免费可用,也可切换到 HiGHS、Gurobi、CPLEX 等
msg=0关闭求解器输出,timeLimit设置超时
第4节 常见应用场景
本节给出 5 个经典优化问题的完整 PuLP 实现,每个场景都是可直接运行的代码。
4.1 运输问题
问题:3 个工厂向 4 个市场供货,已知各工厂供应量、各市场需求量及单位运输成本,求最小总运输成本。
import pulp
factories = ['工厂1', '工厂2', '工厂3']
markets = ['市场A', '市场B', '市场C', '市场D']
supply = {'工厂1': 8, '工厂2': 5, '工厂3': 7}
demand = {'市场A': 4, '市场B': 8, '市场C': 3, '市场D': 5}
cost = {
('工厂1', '市场A'): 2, ('工厂1', '市场B'): 3, ('工厂1', '市场C'): 1, ('工厂1', '市场D'): 4,
('工厂2', '市场A'): 3, ('工厂2', '市场B'): 2, ('工厂2', '市场C'): 4, ('工厂2', '市场D'): 1,
('工厂3', '市场A'): 1, ('工厂3', '市场B'): 4, ('工厂3', '市场C'): 3, ('工厂3', '市场D'): 2,
}
prob = pulp.LpProblem("运输问题", pulp.LpMinimize)
# 决策变量:x[f,m] = 从工厂f运往市场m的数量
x = pulp.LpVariable.dicts("运输量", [(f, m) for f in factories for m in markets],
lowBound=0, cat='Continuous')
# 目标:最小化总运输成本
prob += pulp.lpSum(cost[f, m] * x[f, m] for f in factories for m in markets)
# 约束:每个工厂的运出量不超过供应量
for f in factories:
prob += pulp.lpSum(x[f, m] for m in markets) <= supply[f], f"{f}_供应"
# 约束:每个市场的收到量等于需求量
for m in markets:
prob += pulp.lpSum(x[f, m] for f in factories) == demand[m], f"{m}_需求"
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print(f"最小运输成本: {pulp.value(prob.objective):.2f}")
for f in factories:
for m in markets:
if x[f, m].varValue > 0.01:
print(f" {f} -> {m}: {x[f, m].varValue:.1f}")
最小运输成本: 36.00
工厂1 -> 市场B: 5.0
工厂1 -> 市场C: 3.0
工厂2 -> 市场B: 3.0
工厂2 -> 市场D: 2.0
工厂3 -> 市场A: 4.0
工厂3 -> 市场D: 3.0
关键点:
使用元组
(f, m)作为LpVariable.dicts的键,方便组织多维变量供应约束用
<=,需求约束用==(必须满足)用条件判断过滤掉零值,只打印有运输量的路线
4.2 指派问题
问题:4 个工人完成 4 项任务,每人只能做一个任务,每个任务只能由一人完成,求最小总成本。
workers = ['工人1', '工人2', '工人3', '工人4']
tasks = ['任务A', '任务B', '任务C', '任务D']
cost_matrix = {
('工人1', '任务A'): 9, ('工人1', '任务B'): 2, ('工人1', '任务C'): 7, ('工人1', '任务D'): 8,
('工人2', '任务A'): 6, ('工人2', '任务B'): 4, ('工人2', '任务C'): 3, ('工人2', '任务D'): 7,
('工人3', '任务A'): 5, ('工人3', '任务B'): 8, ('工人3', '任务C'): 1, ('工人3', '任务D'): 8,
('工人4', '任务A'): 7, ('工人4', '任务B'): 6, ('工人4', '任务C'): 9, ('工人4', '任务D'): 4,
}
prob = pulp.LpProblem("指派问题", pulp.LpMinimize)
# 二元变量:y[w,t] = 1 表示工人w被指派给任务t
y = pulp.LpVariable.dicts("指派", [(w, t) for w in workers for t in tasks], cat='Binary')
# 目标:最小化总成本
prob += pulp.lpSum(cost_matrix[w, t] * y[w, t] for w in workers for t in tasks)
# 约束:每个工人只能做一个任务
for w in workers:
prob += pulp.lpSum(y[w, t] for t in tasks) == 1, f"{w}_只能做一个任务"
# 约束:每个任务只能被一人做
for t in tasks:
prob += pulp.lpSum(y[w, t] for w in workers) == 1, f"{t}_只能被一人做"
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print(f"最小总成本: {pulp.value(prob.objective):.0f}")
for w in workers:
for t in tasks:
if y[w, t].varValue > 0.5:
print(f" {w} -> {t} (成本: {cost_matrix[w, t]})")
最小总成本: 13
工人1 -> 任务B (成本: 2)
工人2 -> 任务A (成本: 6)
工人3 -> 任务C (成本: 1)
工人4 -> 任务D (成本: 4)
关键点:
指派问题必须用
Binary变量(0-1 决策)两个方向的等式约束保证一一对应
这是运输问题的特例(供应量和需求量都为 1)
4.3 背包问题
问题:背包容量为 10,有 5 件物品可选,每件物品有价值与重量,求最大总价值。
items = ['物品1', '物品2', '物品3', '物品4', '物品5']
values = [10, 15, 7, 8, 12]
weights = [4, 6, 3, 5, 2]
capacity = 10
prob = pulp.LpProblem("背包问题", pulp.LpMaximize)
# 二元变量:z[i] = 1 表示选择物品i
z = pulp.LpVariable.dicts("选择", items, cat='Binary')
# 目标:最大化总价值
prob += pulp.lpSum(values[i] * z[items[i]] for i in range(len(items)))
# 约束:总重量不超过容量
prob += pulp.lpSum(weights[i] * z[items[i]] for i in range(len(items))) <= capacity
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print(f"最大价值: {pulp.value(prob.objective):.0f}")
total_weight = sum(weights[i] * z[items[i]].varValue for i in range(len(items)))
print(f"总重量: {total_weight:.0f}/{capacity}")
for i in range(len(items)):
if z[items[i]].varValue > 0.5:
print(f" {items[i]}: 价值={values[i]}, 重量={weights[i]}")
最大价值: 29
总重量: 9/10
物品1: 价值=10, 重量=4
物品3: 价值=7, 重量=3
物品5: 价值=12, 重量=2
关键点:
背包问题是经典的 NP-hard 问题,小规模时 CBC 可以精确求解
选择物品 1、3、5,总价值 29,总重量 9(未装满但已是最优)
如果允许物品分割(分数背包),贪心算法即可求解;但 0-1 背包必须用整数规划
4.4 饮食/营养配比问题
问题:在满足最低营养需求的前提下,选择食物组合使得花费最小。
营养需求:热量 ≥ 500,蛋白质 ≥ 30,脂肪 ≤ 40
foods = ['面包', '牛奶', '鸡蛋', '苹果', '鸡肉']
costs = [3, 5, 2, 4, 12]
calories = [200, 150, 80, 95, 250]
protein = [5, 8, 6, 0.5, 30]
fat = [2, 5, 5, 0.3, 10]
min_calories = 500
min_protein = 30
max_fat = 40
prob = pulp.LpProblem("饮食问题", pulp.LpMinimize)
food_vars = pulp.LpVariable.dicts("食物量", foods, lowBound=0, cat='Continuous')
# 目标:最小化总花费
prob += pulp.lpSum(costs[i] * food_vars[foods[i]] for i in range(len(foods)))
# 约束:满足营养需求
prob += pulp.lpSum(calories[i] * food_vars[foods[i]] for i in range(len(foods))) >= min_calories
prob += pulp.lpSum(protein[i] * food_vars[foods[i]] for i in range(len(foods))) >= min_protein
prob += pulp.lpSum(fat[i] * food_vars[foods[i]] for i in range(len(foods))) <= max_fat
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print(f"最小花费: {pulp.value(prob.objective):.2f} 元")
for i in range(len(foods)):
val = food_vars[foods[i]].varValue
if val > 0.01:
print(f" {foods[i]}: {val:.2f} 份")
最小花费: 11.00 元
面包: 0.75 份
鸡蛋: 4.38 份
关键点:
饮食问题同时包含
>=(最低营养)和<=(最高脂肪)约束求解器自动选择了性价比最高的食物组合(面包提供热量,鸡蛋提供蛋白质)
实际应用中可以为变量加
upBound限制单种食物的最大摄入量
4.5 生产计划问题(整数规划)
问题:3 种产品的生产计划,受原材料、工时和市场需求的限制,求最大利润。
资源总量:原材料 100 kg,工时 80 小时。市场需求上限:A ≤ 30,B ≤ 25,C ≤ 20。
products = ['产品A', '产品B', '产品C']
profits = [40, 30, 50]
materials = [2, 1, 3]
labor = [1, 2, 2]
total_material = 100
total_labor = 80
prob = pulp.LpProblem("生产计划(整数)", pulp.LpMaximize)
# 整数变量:产量必须为整数
p = pulp.LpVariable.dicts("产量", products, lowBound=0, cat='Integer')
# 目标:最大化总利润
prob += pulp.lpSum(profits[i] * p[products[i]] for i in range(len(products)))
# 约束
prob += pulp.lpSum(materials[i] * p[products[i]] for i in range(len(products))) <= total_material
prob += pulp.lpSum(labor[i] * p[products[i]] for i in range(len(products))) <= total_labor
prob += p['产品A'] <= 30
prob += p['产品B'] <= 25
prob += p['产品C'] <= 20
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print(f"最大利润: {pulp.value(prob.objective):.0f}")
for i in range(len(products)):
print(f" {products[i]}: {int(p[products[i]].varValue)} 件")
最大利润: 2090
产品A: 30 件
产品B: 18 件
产品C: 7 件
关键点:
使用
Integer变量保证产量为整数(实际生产中不可能生产半件产品)与第 1 章的连续版本对比,整数约束会略微降低目标值
求解时间会比线性规划稍长(分支定界法)

4.6 本节小结
运输问题:用元组键
LpVariable.dicts组织二维变量,供应约束<=,需求约束==指派问题:
Binary变量 + 双向等式约束保证一一对应背包问题:
Binary选择变量 + 重量上限约束饮食问题:混合
>=和<=约束,同时考虑最小营养和最大限制生产计划:
Integer变量保证离散决策,是线性规划到整数规划的过渡
第5节 高级特性概览
本节简要介绍 PuLP 的几个进阶用法,了解这些技巧可以让你的建模更加灵活。
5.1 约束的动态生成
当约束数量较多或需要根据数据动态生成时,用 for 循环遍历是最自然的方式。
排班问题示例
5 个员工排 7 天班,每天至少 2 人,每人最多上 4 天,求最小总人次。
import pulp
employees = ['员工1', '员工2', '员工3', '员工4', '员工5']
days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
min_per_day = 2
max_days_per_employee = 4
prob = pulp.LpProblem("排班问题", pulp.LpMinimize)
# 二元变量:s[e,d] = 1 表示员工e在日d上班
s = pulp.LpVariable.dicts("排班", [(e, d) for e in employees for d in days], cat='Binary')
# 目标:最小化总上班人次
prob += pulp.lpSum(s[e, d] for e in employees for d in days)
# 约束1:每天至少 min_per_day 人上班(动态生成 7 条约束)
for d in days:
prob += pulp.lpSum(s[e, d] for e in employees) >= min_per_day, f"{d}_最少人数"
# 约束2:每人最多上 max_days_per_employee 天(动态生成 5 条约束)
for e in employees:
prob += pulp.lpSum(s[e, d] for d in days) <= max_days_per_employee, f"{e}_最多天数"
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print(f"总上班人次: {pulp.value(prob.objective)}")
for d in days:
workers_on_duty = [e for e in employees if s[e, d].varValue > 0.5]
print(f" {d}: {', '.join(workers_on_duty)}")
总上班人次: 14.0
周一: 员工3, 员工4
周二: 员工3, 员工4
周三: 员工1, 员工3
周四: 员工2, 员工5
周五: 员工1, 员工4
周六: 员工1, 员工4
周日: 员工1, 员工3
关键点:
用
for循环批量生成约束是处理大规模问题的标准做法约束标签用 f-string 嵌入变量名,方便调试时定位
排班问题是整数规划的经典应用,规模大时求解时间会增长
5.2 约束的修改与移除
有时需要反复求解同一模型但修改部分约束(如灵敏度分析),可以直接操作 prob.constraints 字典。
prob = pulp.LpProblem("修改示例", pulp.LpMaximize)
x = pulp.LpVariable("x", lowBound=0)
y = pulp.LpVariable("y", lowBound=0)
prob += 3 * x + 4 * y
prob += x + y <= 10, "资源约束"
prob += x <= 6, "x上限"
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print(f"原始: x={x.varValue}, y={y.varValue}, 目标={pulp.value(prob.objective)}")
# 原始: x=0.0, y=10.0, 目标=40.0
# 修改约束右端值:通过修改 constant 属性
prob.constraints['资源约束'].constant = -12 # <=10 变为 <=12
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print(f"修改后: x={x.varValue}, y={y.varValue}, 目标={pulp.value(prob.objective)}")
# 修改后: x=0.0, y=12.0, 目标=48.0
# 移除约束:从字典中删除
del prob.constraints['x上限']
prob.solve(pulp.PULP_CBC_CMD(msg=0))
# 添加新约束:直接用 +=
prob += x <= 8, "新x上限"
prob.solve(pulp.PULP_CBC_CMD(msg=0))
关键点:
prob.constraints是一个字典,键为约束标签修改
constant属性等价于修改约束右端值(注意符号:<=10对应constant=-10)删除约束用
del prob.constraints[label]添加新约束直接用
+=,与初始建模无异
5.3 多目标优化
PuLP 本身不直接支持多目标,但可以通过以下两种常用方法实现:
方法 1:加权法
将多个目标加权合并为单目标:
products = ['产品X', '产品Y']
costs = [10, 8] # X 成本高
quality = [8, 5] # X 质量高
resource = [2, 1] # X 耗资源多
total_resource = 20
w_cost = 0.7 # 成本权重
w_quality = 0.3 # 质量权重
scale = 2.0 # 质量缩放因子,平衡量级
prob = pulp.LpProblem("多目标", pulp.LpMinimize)
x = pulp.LpVariable.dicts("产量", products, lowBound=0, cat='Integer')
# 最小化成本,同时最大化质量(质量前加负号)
prob += pulp.lpSum(
w_cost * costs[i] * x[products[i]] - w_quality * scale * quality[i] * x[products[i]]
for i in range(len(products))
)
prob += pulp.lpSum(resource[i] * x[products[i]] for i in range(len(products))) <= total_resource
prob += pulp.lpSum(x[p] for p in products) >= 1 # 避免零解
prob.solve(pulp.PULP_CBC_CMD(msg=0))
不同权重对比:
关键点:
权重变化会显著改变最优解,需要根据实际需求调整
scale因子用于平衡不同目标的量级需要加最小产量约束避免零解
方法 2:ε-约束法
一个目标为主目标,另一个设为约束下限:
# 最小化成本,同时要求质量 >= 某值
for min_q in [10, 30, 50, 70]:
prob = pulp.LpProblem(f"eps_{min_q}", pulp.LpMinimize)
x = pulp.LpVariable.dicts("产量", products, lowBound=0, cat='Integer')
prob += pulp.lpSum(costs[i] * x[products[i]] for i in range(len(products))) # 主目标
prob += pulp.lpSum(resource[i] * x[products[i]] for i in range(len(products))) <= total_resource
prob += pulp.lpSum(quality[i] * x[products[i]] for i in range(len(products))) >= min_q # ε-约束
prob.solve(pulp.PULP_CBC_CMD(msg=0))
质量>=10: 成本=16.0, 实际质量=10.0, X=0, Y=2
质量>=30: 成本=40.0, 实际质量=32.0, X=4, Y=0
质量>=50: 成本=66.0, 实际质量=50.0, X=5, Y=2
质量>=70: 成本=90.0, 实际质量=72.0, X=9, Y=0
关键点:
ε-约束法更直观:直接指定质量下限,然后最小化成本
逐步调整 ε 值可以得到 Pareto 前沿
如果 ε 设置过高可能导致不可行
5.4 约束灵敏度分析
灵敏度分析用于研究约束右端值变化对目标函数的影响。
# 逐步改变约束右端值
rhs_values = [8, 9, 10, 11, 12, 13, 14, 15]
for rhs in rhs_values:
prob = pulp.LpProblem(f"sens_{rhs}", pulp.LpMaximize)
x = pulp.LpVariable("x", lowBound=0)
y = pulp.LpVariable("y", lowBound=0)
prob += 5 * x + 3 * y
prob += 2 * x + y <= rhs # 变化的约束
prob += x + 2 * y <= 8
prob.solve(pulp.PULP_CBC_CMD(msg=0))
print(f"右端={rhs}: 目标值={pulp.value(prob.objective):.1f}")
右端=8: 目标值=21.3
右端=9: 目标值=23.7
右端=10: 目标值=26.0
右端=11: 目标值=28.3
右端=12: 目标值=30.7
右端=13: 目标值=33.0
右端=14: 目标值=35.3
右端=15: 目标值=37.7
关键点:
目标值随右端值的变化率等于影子价格(本例约 2.33)
在影子价格的有效范围内,目标值呈线性变化
超出有效范围后,影子价格会发生变化(最优基改变)

5.5 本节小结
动态生成约束:用
for循环遍历数据批量添加约束,是处理大规模问题的标准做法约束修改:通过
prob.constraints字典可以修改、删除、添加约束,适合灵敏度分析和迭代求解多目标优化:PuLP 不直接支持多目标,可用加权法或 ε-约束法间接实现
灵敏度分析:逐步改变约束右端值,观察目标函数变化,可验证影子价格的有效性
附录:完整代码获取
本教程所有代码均可通过以下链接下载: