本文介绍如何使用 PuLP 建模「每项任务最多分配给一名员工」的逻辑约束,通过引入二元分配变量与大M法(Big-M)将“是否参与”与“实际工时”解耦,解决整数规划中无法直接对决策变量做逻辑比较(如 var > 0)的核心难点。
本文介绍如何使用 pulp 建模「每项任务最多分配给一名员工」的逻辑约束,通过引入二元分配变量与大m法(big-m)将“是否参与”与“实际工时”解耦,解决整数规划中无法直接对决策变量做逻辑比较(如 `var > 0`)的核心难点。
在资源调度类优化问题中,一个常见但易被忽视的关键约束是:单个任务(action)只能由至多一名员工承担。这看似简单,但在数学规划建模中无法通过 vars[action][m] > 0 这类条件判断实现——因为 PuLP 的 LpVariable 是符号变量,不支持与整数的直接比较运算,会触发 TypeError: '>' not supported between instances of 'LpVariable' and 'int'。
正确做法是采用变量分离 + 大M约束(Big-M formulation):
- 引入二元变量 assign[action][person] ∈ {0,1} 表示“该员工是否被分配到该任务”;
- 同时保留连续变量 hours[action][person] ≥ 0 表示实际分配的工时;
- 用线性约束 hours ≤ assign × M 将二者关联(当 assign = 0 时强制 hours = 0;当 assign = 1 时允许 hours ≤ M);
- 最后对每个 action 施加 ∑ assign[action][*] ≤ 1,即“最多一人被分配”。
以下为完整可运行示例(基于 Pandas + PuLP):
import pandas as pd
import pulp
# 定义任务(actions)
actions = pd.DataFrame(
index=pd.Index(name='action', data=('A', 'B', 'C', 'D')),
data={
'value': (5, 2, 1, 1), # 单位工时价值
'max_work': (8, 4, 12, 24), # 该任务最多可投入总工时
}
)
# 定义员工(people)
people = pd.DataFrame(
index=pd.RangeIndex(name='person', start=1, stop=6),
data={
'max_hours': (7, 7, 6, 5, 5), # 每人可用最大工时
}
)
# 构建所有 (action, person) 组合,并定义两类变量:
# - 'assign': 二元变量,表示是否分配(0/1)
# - 'hours': 连续变量,表示实际分配工时(≥0)
combos = pd.DataFrame({
'assign': pulp.LpVariable.matrix(
name='assign', indices=(actions.index, people.index),
cat=pulp.LpBinary
),
'hours': pulp.LpVariable.matrix(
name='hours', indices=(actions.index, people.index),
cat=pulp.LpContinuous, lowBound=0
)
}).stack([0, 1]).to_frame('val').unstack(-1).swaplevel(axis=1)
# 重命名列便于操作
combos.columns = ['assign', 'hours']
combos['value_per_hour'] = actions['value'].reindex(combos.index.get_level_values('action'))
combos['value'] = combos['hours'] * combos['value_per_hour']
# 按员工/任务聚合总工时
people['total_hours'] = combos['hours'].groupby('person').sum()
actions['total_work'] = combos['hours'].groupby('action').sum()
# 创建优化问题
prob = pulp.LpProblem(name='PlanningActions', sense=pulp.LpMaximize)
prob.setObjective(pulp.lpSum(combos['value']))
# 【核心约束1】每人最多承担一项任务(可选,依业务而定)
# for person, group in combos['assign'].groupby('person'):
# prob.addConstraint(pulp.lpSum(group) <= 1, name=f'excl_person_{person}')
# 【核心约束2】每项任务最多分配给一名员工 → 关键!
for action, group in combos['assign'].groupby('action'):
prob.addConstraint(pulp.lpSum(group) <= 1, name=f'excl_action_{action}')
# 【资源约束】每人总工时 ≤ 其可用时间
for person in people.index:
prob.addConstraint(
people.loc[person, 'total_hours'] <= people.loc[person, 'max_hours'],
name=f'hours_limit_{person}'
)
# 【资源约束】每项任务总工时 ≤ 其最大需求
for action in actions.index:
prob.addConstraint(
actions.loc[action, 'total_work'] <= actions.loc[action, 'max_work'],
name=f'work_limit_{action}'
)
# 【大M约束】确保 hours 只在 assign=1 时可取正值
M = 2 * max(actions['max_work'].max(), people['max_hours'].max())
for (action, person), row in combos.iterrows():
prob.addConstraint(
row['hours'] <= row['assign'] * M,
name=f'bigM_{action}_{person}'
)
# 求解
prob.solve(pulp.PULP_CBC_CMD(msg=False))
assert prob.status == pulp.LpStatusOptimal, "未找到最优解"
# 输出结果(转换为数值)
combos = combos.map(pulp.value)
people = people.map(pulp.value)
actions = actions.map(pulp.value)
print("=== 分配详情(assign=1 表示被选中)===")
print(combos[combos['assign'] > 0.5][['assign', 'hours', 'value']])
print("\n=== 员工总工时使用情况 ===")
print(people[['max_hours', 'total_hours']])
print("\n=== 任务完成情况 ===")
print(actions[['value', 'max_work', 'total_work']])✅ 关键要点总结:
- ❌ 禁止对 LpVariable 使用 >, <, == 等比较操作;
- ✅ 必须用二元变量显式建模“是否启用”逻辑;
- ✅ 用 hours ≤ assign × M 实现“启用才可工作”的线性化;
- ✅ M 应取足够大但不过大的值(如 2 × max(最大工时, 最大任务量)),避免数值不稳定;
- ✅ assign.sum(level='action') ≤ 1 是实现「单任务单人」的直接且高效方式。
该建模范式不仅适用于人员调度,也广泛用于设备排程、客户-服务者匹配、仓库-订单分配等需满足“一对一”或“一对多但单源”逻辑的整数规划场景。

















