线性规划是运筹学中最基础且应用最广泛的优化方法之一,在工业工程、金融经济、物流运输等领域都有重要应用。本书旨在帮助读者掌握使用Python的PuLP库构建和求解各类线性规划问题的实践技能。
本书所有代码示例:
pip install pulp
感谢COIN-OR组织开发维护PuLP这一优秀工具,以及所有为开源优化社区做出贡献的研究人员和开发者。
如有任何问题或建议,欢迎通过出版社联系作者,或访问本书的GitHub页面提交issue。
接下来我们将深入探讨线性规划的基础知识和PuLP的使用方法,从第1章开始我们的优化之旅。
线性规划(Linear Programming,简称LP)是运筹学中应用最为广泛的一种数学优化方法,用于在给定的线性约束条件下,找到使线性目标函数达到最优值(最大或最小)的决策变量取值。
线性规划问题通常包含三个基本要素:
数学上,标准形式的线性规划问题可以表示为:
最大化(或最小化) z = c₁x₁ + c₂x₂ + ... + cₙxₙ
满足约束条件:
a₁₁x₁ + a₁₂x₂ + ... + a₁ₙxₙ ≤ b₁
a₂₁x₁ + a₂₂x₂ + ... + a₂ₙxₙ ≤ b₂
...
aₘ₁x₁ + aₘ₂x₂ + ... + aₘₙxₙ ≤ bₘ
x₁, x₂, ..., xₙ ≥ 0
线性规划在实际中有极其广泛的应用,几乎涵盖了所有需要优化资源配置的领域:
一个完整的线性规划模型包含以下关键组成部分:
PuLP是一个用Python编写的开源线性规划建模工具,具有以下特点和优势:
与其他优化工具相比,PuLP特别适合:
工具/特性 | PuLP | Pyomo | CVXPY | 商业软件(Gurobi等) |
---|---|---|---|---|
许可证 | 开源 | 开源 | 开源 | 商业许可 |
学习曲线 | 简单 | 中等 | 中等 | 中等 |
建模语言 | Python | Python | Python | 专用语言 |
求解器支持 | 多 | 多 | 有限 | 专有 |
性能 | 依赖求解器 | 依赖求解器 | 依赖求解器 | 极高 |
适用场景 | 教学/中小问题 | 研究/复杂问题 | 凸优化 | 工业级大规模问题 |
安装PuLP非常简单,可以通过pip直接安装:
pip install pulp
PuLP依赖于以下软件包,安装时会自动解决依赖关系:
要验证安装是否成功,可以运行以下Python代码:
import pulp
print(pulp.__version__)
PuLP支持多种求解器,默认使用开源的CBC求解器。如果需要使用其他求解器,需要单独安装:
GLPK (GNU Linear Programming Kit):
sudo apt-get install glpk-utils # Linux
brew install glpk # macOS
商业求解器 (如Gurobi、CPLEX):
安装完成后,可以检查可用的求解器:
from pulp import *
listSolvers(onlyAvailable=True)
输出示例:
['PULP_CBC_CMD', 'GLPK_CMD']
至此,您已经完成了PuLP的基本环境配置,可以开始构建和求解线性规划问题了。在接下来的章节中,我们将深入探讨如何使用PuLP建立各种优化模型,并解决实际问题。
在PuLP中,所有优化问题都通过LpProblem
类来创建。以下是创建问题实例的基本方法:
from pulp import LpProblem, LpMaximize, LpMinimize
# 创建最大化问题
max_problem = LpProblem("Maximization_Problem", LpMaximize)
# 创建最小化问题
min_problem = LpProblem("Minimization_Problem", LpMinimize)
参数说明:
LpMaximize
或LpMinimize
)目标函数是优化问题的核心,定义方法如下:
# 先创建变量
x = LpVariable("x", lowBound=0)
y = LpVariable("y", lowBound=0)
# 定义目标函数(最大化3x + 4y)
max_problem += 3*x + 4*y, "Profit"
注意事项:
+=
运算符添加目标函数求解问题后,可以通过以下属性获取状态和结果:
# 求解问题
status = max_problem.solve()
# 获取求解状态
print("Status:", LpStatus[status])
# 输出变量值
print("x =", value(x))
print("y =", value(y))
# 输出目标函数值
print("Optimal value =", value(max_problem.objective))
常见状态值:
Optimal
:找到最优解Infeasible
:无可行解Unbounded
:无界解Not Solved
:未求解PuLP提供灵活的变量定义方式:
from pulp import LpVariable
# 基本变量(连续,≥0)
var1 = LpVariable("variable1")
# 指定下界
var2 = LpVariable("variable2", lowBound=5)
# 指定上下界
var3 = LpVariable("variable3", lowBound=0, upBound=10)
# 整数变量
int_var = LpVariable("integer_var", lowBound=0, cat='Integer')
# 二进制变量
bin_var = LpVariable("binary_var", cat='Binary')
对于大规模问题,可以使用批量创建方法:
# 创建一组变量
products = ["A", "B", "C"]
product_vars = LpVariable.dicts("product", products, lowBound=0)
# 二维变量示例
months = ["Jan", "Feb", "Mar"]
production = LpVariable.dicts("prod",
[(p, m) for p in products for m in months],
lowBound=0)
# 适用于大多数资源分配问题
x = LpVariable("continuous_var")
# 适用于需要整数解的场景(如物品数量)
y = LpVariable("integer_var", cat='Integer')
# 适用于是/否决策(如是否开设工厂)
z = LpVariable("binary_var", cat='Binary')
实际应用示例:
from pulp import *
# 创建混合整数规划问题
prob = LpProblem("Mixed_Integer_Problem", LpMaximize)
# 定义变量
x = LpVariable("x", lowBound=0) # 连续
y = LpVariable("y", cat='Integer', lowBound=0) # 整数
z = LpVariable("z", cat='Binary') # 二进制
# 目标函数
prob += 1.5*x + 3*y + 2.5*z
# 约束条件
prob += x + y + z <= 10
prob += 2*x + 3*y + z <= 15
# 求解
status = prob.solve()
# 输出结果
print("Status:", LpStatus[status])
for v in prob.variables():
print(v.name, "=", v.varValue)
# 简单不等式
prob += x + y <= 10
# 等式约束
prob += 2*x + 3*y == 20
# 范围约束
prob += 5 <= x + y <= 15
# 对列表变量求和
variables = [x1, x2, x3, x4]
prob += lpSum(variables) <= 100
# 带系数的求和
prob += lpSum([3*v for v in variables]) >= 50
# 为多个时间段添加资源约束
time_periods = ["T1", "T2", "T3"]
demand = {
"T1": 100, "T2": 150, "T3": 200}
capacity = 120
# 创建变量
production = LpVariable.dicts("prod", time_periods, lowBound=0)
# 批量添加约束
for t in time_periods:
prob += production[t] <= capacity
prob += production[t] >= demand[t]
from pulp import *
# 初始化问题
prob = LpProblem("Resource_Allocation", LpMaximize)
# 定义产品
products = ["A", "B", "C"]
profit = {
"A": 5, "B": 7, "C": 4}
# 资源消耗系数
resource_use = {
"A": {
"labor": 2, "material": 1},
"B": {
"labor": 3, "material": 2},
"C": {
"labor": 1, "material": 1}
}
# 资源限制
resources = {
"labor": 100, "material": 80}
# 创建决策变量
production = LpVariable.dicts("product", products, lowBound=0)
# 目标函数:最大化总利润
prob += lpSum([profit[p] * production[p] for p in products])
# 添加资源约束
for r in resources:
prob += lpSum([resource_use[p][r] * production[p] for p in products]) <= resources[r]
# 求解问题
prob.solve()
# 输出结果
print("Optimal Production Plan:")
for p in products:
print(f"{
p}: {
production[p].varValue} units")
print(f"Total Profit: ${
value(prob.objective):.2f}")
本章介绍了PuLP的基础操作,包括问题创建、变量定义、约束构建等核心功能。掌握这些基础知识后,您已经可以解决许多常见的线性规划问题。在后续章节中,我们将探讨更复杂的应用场景和高级技巧。
不等式约束是线性规划中最常见的约束类型,PuLP中使用直观的语法实现:
from pulp import *
prob = LpProblem("Simple_Inequality", LpMaximize)
x = LpVariable("x", lowBound=0)
y = LpVariable("y", lowBound=0)
# 添加不等式约束
prob += 2*x + 3*y <= 60 # 资源约束
prob += x + y <= 25 # 产能约束
验证要点:
等式约束使用双等号表示,常用于物料平衡、需求满足等场景:
# 添加等式约束
prob += x - 2*y == 0 # 比例关系约束
常见错误检查:
PuLP支持直接定义变量的取值范围:
# 范围约束的两种等效写法
prob += 10 <= x + y <= 20 # 推荐写法
prob += x + y >= 10
prob += x + y <= 20 # 传统写法
性能提示:
lpSum
是PuLP中处理求和约束的高效工具:
# 创建多个变量
variables = [LpVariable(f"x{
i}", lowBound=0) for i in range(5)]
# 使用lpSum求和
prob += lpSum(variables) <= 100 # 总资源约束
# 带系数的求和
prob += lpSum(i*v for i, v in enumerate(variables, 1)) >= 50
正确性验证:
lpSum
内参数为可迭代对象通过引入二进制变量实现逻辑条件:
M = 1000 # 足够大的常数
b = LpVariable("binary_flag", cat='Binary')
# 条件约束:如果x>0,则y>=5
prob += y >= 5 - M*(1 - b)
prob += x <= M*b
注意事项:
使用SOS2(特殊有序集)或二进制变量实现:
# 分段线性函数近似
breakpoints = [0, 50, 100]
values = [0, 30, 40]
lambda_vars = [LpVariable(f"lambda_{
i}", lowBound=0) for i in range(3)]
prob += lpSum(lambda_vars) == 1 # 凸组合约束
prob += x == lpSum(b*lam for b, lam in zip(breakpoints, lambda_vars))
prob += y == lpSum(v*lam for v, lam in zip(values, lambda_vars))
# 时间周期
periods = range(1, 5)
# 创建生产量和库存量变量
produce = LpVariable.dicts("prod", periods, lowBound=0)
inventory = LpVariable.dicts("inv", periods, lowBound=0)
# 初始库存
initial_inventory = 100
# 库存平衡约束
for t in periods:
if t == 1:
prob += inventory[t] == initial_inventory + produce[t] - demand[t]
else:
prob += inventory[t] == inventory[t-1] + produce[t] - demand[t]
# 产能约束
prob += produce[t] <= max_production
约束验证:
# 创建运输量变量
routes = [(s, d) for s in sources for d in destinations]
transport = LpVariable.dicts("trans", routes, lowBound=0)
# 供应约束
for s in sources:
prob += lpSum(transport[s, d] for d in destinations) <= supply[s]
# 需求约束
for d in destinations:
prob += lpSum(transport[s, d] for s in sources) >= demand[d]
# 员工和班次定义
employees = ["E1", "E2", "E3"]
shifts = ["Morning", "Afternoon", "Night"]
days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
# 创建排班变量
schedule = LpVariable.dicts("work",
[(e, s, d) for e in employees
for s in shifts
for d in days],
cat='Binary')
# 每人每天只能上一个班次
for e in employees:
for d in days:
prob += lpSum(schedule[e, s, d] for s in shifts) <= 1
# 每个班次最少需要2人
for s in shifts:
for d in days:
prob += lpSum(schedule[e, s, d] for e in employees) >= 2
当问题不可行时,可以:
pulp.constants.LpConstraint
的valid()
方法验证约束# 示例:验证约束有效性
constraint = 2*x + 3*y <= 60
print(constraint.valid(x=30, y=0)) # 返回False表示违反约束
引入松弛变量处理严格约束:
# 添加松弛变量
slack = LpVariable("slack", lowBound=0)
prob += x + y <= 100 + slack
# 在目标函数中惩罚松弛量
prob += 2*x + 3*y - 10*slack # 惩罚系数需要合理设置
识别并移除冗余约束:
prob.constraints
查看所有约束# 查看约束列表
for name, constraint in prob.constraints.items():
print(name, ":", constraint)
特点与配置:
prob.solve(PULP_CBC_CMD(
msg=1, # 显示求解日志(0关闭)
timeLimit=60, # 最大求解时间(秒)
fracGap=0.01, # 允许的最优间隙(1%)
threads=4 # 使用的CPU线程数
))
验证方法:
print("Used solver:", prob.solver) # 查看实际使用的求解器
print("Solve time:", prob.solutionTime) # 获取求解时间
安装与使用:
# Linux安装
sudo apt-get install glpk-utils
# macOS安装
brew install glpk
prob.solve(GLPK_CMD(
options=["--tmlim", "60", # 时间限制
"--presol", # 启用预处理
"--cuts"] # 生成切割平面
))
性能对比:
问题规模 | CBC求解时间 | GLPK求解时间 |
---|---|---|
100变量 | 0.5s | 0.8s |
1000变量 | 12s | 18s |
配置示例:
# Gurobi配置
prob.solve(GUROBI(
timeLimit=120,
mipGap=0.001,
logPath="gurobi.log"
))
# CPLEX配置
prob.solve(CPLEX_CMD(
options=["set timelimit 120",
"set mip tolerances mipgap 0.001"]
))
许可证检查:
try:
prob.solve(GUROBI())