如何在 PuLP 中约束每项任务仅由一名员工承担
本文介绍如何使用 pulp 建模“每项任务最多分配给一名员工”的逻辑约束,通过引入二元分配变量与大m法(big-m)将“是否参与”与“工作时长”解耦,解决整数规划中无法直接对连续变量做逻辑比较的难题。
本文介绍如何使用 pulp 建模“每项任务最多分配给一名员工”的逻辑约束,通过引入二元分配变量与大m法(big-m)将“是否参与”与“工作时长”解耦,解决整数规划中无法直接对连续变量做逻辑比较的难题。
在资源调度类优化问题中,一个常见但易被忽视的关键约束是:单个任务(action)不应被拆分给多名员工并行执行——例如,“ActionA”必须由且仅由一名员工全权负责,而非由两人各做4小时。初学者常试图用 vars[action][m] > 0 这类布尔表达式直接建模“是否被分配”,但 PuLP 的 LpVariable 不支持与整数的直接比较(会报 TypeError: '>' not supported between instances of 'LpVariable' and 'int'),根本原因在于线性规划模型中所有约束必须是线性的、可求导的代数表达式,而逻辑判断是非线性的。
正确解法是采用混合整数规划(MIP)建模范式:引入两类变量协同建模:
- 二元变量 assign[action, person] ∈ {0,1}:表示该员工是否被分配到该任务(1 = 是,0 = 否);
- 连续变量 hours[action, person] ≥ 0:表示该员工在该任务上实际投入的工时。
二者通过大M约束(Big-M constraint) 关联:
hours[action, person] <= assign[action, person] * M
其中 M 是足够大的常数(如 M = 2 * max(max_work.max(), max_hours.max())),确保当 assign=0 时强制 hours=0;当 assign=1 时,hours 可自由取 [0, M] 内值(再由其他约束进一步收紧)。
在此基础上,“每项任务至多一人承担”就转化为简洁的线性约束:
for action in actions.index:
prob.addConstraint(
pulp.lpSum(assign[action, person] for person in people.index) <= 1,
name=f'excl_{action}'
)
完整实现推荐使用 pulp.LpVariable.matrix() 配合 Pandas MultiIndex 管理变量,提升可读性与可维护性。以下为精简可运行示例:
import pandas as pd
import pulp
# 定义任务与人员数据
actions = pd.DataFrame({
'value': [5, 2, 1, 1],
'max_work': [8, 4, 12, 24]
}, index=pd.Index(['A','B','C','D'], name='action'))
people = pd.DataFrame({
'max_hours': [7, 7, 6, 5, 5]
}, index=pd.RangeIndex(1, 6, name='person'))
# 构建决策变量矩阵(自动创建 MultiIndex)
assign = pulp.LpVariable.matrix(
'assign', (actions.index, people.index), cat='Binary'
)
hours = pulp.LpVariable.matrix(
'hours', (actions.index, people.index), lowBound=0, cat='Continuous'
)
# 转为 DataFrame 方便聚合
combos = pd.DataFrame({
'assign': pd.DataFrame(assign, index=actions.index, columns=people.index).stack(),
'hours': pd.DataFrame(hours, index=actions.index, columns=people.index).stack(),
})
combos['value'] = combos['hours'] * actions['value'].align(combos)[0]
# 创建问题
prob = pulp.LpProblem('PlanningActions', pulp.LpMaximize)
prob.setObjective(pulp.lpSum(combos['value']))
# 约束1:每人总工时 ≤ 其可用时间
for person in people.index:
prob.addConstraint(
pulp.lpSum(hours[a][person] for a in actions.index) <= people.loc[person, 'max_hours'],
name=f'hours_person_{person}'
)
# 约束2:每任务总工时 ≤ 其最大需求
for action in actions.index:
prob.addConstraint(
pulp.lpSum(hours[action][p] for p in people.index) <= actions.loc[action, 'max_work'],
name=f'work_action_{action}'
)
# 约束3:每任务至多分配1人(核心!)
for action in actions.index:
prob.addConstraint(
pulp.lpSum(assign[action][p] for p in people.index) <= 1,
name=f'excl_action_{action}'
)
# 约束4:小时数受分配状态控制(Big-M)
M = 2 * max(actions['max_work'].max(), people['max_hours'].max())
for (a, p), row in combos.iterrows():
prob.addConstraint(
hours[a][p] <= assign[a][p] * M,
name=f'link_{a}_{p}'
)
# 求解
prob.solve(pulp.PULP_CBC_CMD(msg=False))
assert prob.status == pulp.LpStatusOptimal
# 输出结果
result = combos.map(pulp.value).unstack('person')
print("分配矩阵(1=分配,0=未分配):")
print(result['assign'])
print("\n工时分配(小时):")
print(result['hours'])
关键注意事项:
- ❗ M 值不宜过大(避免数值不稳定),但必须严格大于任何可能的 hours 上界;
- ✅ 使用 matrix() + stack() 比嵌套字典更易与 Pandas 生态集成;
- ⚠️ 若业务允许“任务由多人协作”,则应移除 excl_action_* 约束,并考虑增加团队协同成本项;
- ? 实际项目中建议封装为函数,输入 actions_df, people_df, M 等参数,增强复用性。
通过这一建模模式,你不仅能精准实现“一任务一负责人”,还为后续扩展(如技能匹配、偏好权重、加班惩罚等)奠定了清晰、可扩展的结构基础。