You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python PulP时段分配优化模型:跨时段预约员工分配约束问题

问题解决方案

核心问题拆解

  • 硬约束:同一跨时段预约的所有时段必须分配给同一名员工
  • 软约束:员工任务量尽量均匀分配(允许小范围偏差)
  • 可选优化:避免连续的不同预约分配给同一名员工
  • 技术障碍:Pulp弹性约束默认不支持右侧为变量,需手动构建偏差惩罚

关键调整步骤

1. 重构数据结构:明确预约与时段的关联

原slot_appo_dict仅记录时段任务数,无法区分同一预约的连续时段,需新增预约字典,直接绑定预约ID与覆盖时段:

# 示例:预约ID: 覆盖的时段列表
appointment_slots = {
    "A": [3,4],  # 预约A覆盖时段3、4(对应6:10-6:20)
    "B": [5,6],  # 预约B覆盖时段5、6(对应6:20-6:30)
    "C": [5,6]   # 预约C覆盖时段5、6(第二个并行预约)
}
# 从预约字典反向生成时段任务数(替代原slot_appo_dict)
slot_appo_dict = {slot:0 for slot in range(1,7)}
for appo in appointment_slots.values():
    for slot in appo:
        slot_appo_dict[slot] +=1

2. 修改变量定义:以预约为单位分配员工

原变量empl_appo_bin是(时段,员工)二元变量,改为**(预约,员工)二元变量**,确保同一预约的所有时段绑定同一员工:

# 二元变量:empl_appo_bin[appo][e] = 1 表示员工e分配到预约appo
empl_appo_bin = LpVariable.dicts("employ_appoi_bin", (appointment_slots.keys(), employee_dict), cat=LpBinary)

3. 添加硬约束:跨时段预约绑定同一员工

  • 每个预约必须恰好分配给1名员工:
    for appo in appointment_slots.keys():
        prob += lpSum(empl_appo_bin[appo][e] for e in employee_dict) == 1
    
  • 时段任务数约束:每个时段的分配员工数等于该时段的预约数(由预约映射而来):
    for slot in slot_appo_dict:
        # 收集所有覆盖该时段的预约
        relevant_appos = [appo for appo, slots in appointment_slots.items() if slot in slots]
        prob += lpSum(empl_appo_bin[appo][e] for appo in relevant_appos for e in employee_dict) == slot_appo_dict[slot]
    

4. 修复弹性约束:手动构建偏差惩罚(支持变量右侧)

PulP的makeElasticSubProblem仅支持右侧为常数,若需动态计算基准值,需手动引入偏差变量:

# 员工总任务数:分配的所有预约覆盖的时段总数
empl_appo_count = LpVariable.dicts("employ_appoi_count", employee_dict, cat=LpInteger, lowBound=0)
for e in employee_dict:
    prob += empl_appo_count[e] == lpSum(
        len(appointment_slots[appo]) * empl_appo_bin[appo][e] 
        for appo in appointment_slots.keys()
    )

# 手动构建均匀分配软约束
dev_pos = LpVariable.dicts("dev_pos", employee_dict, cat=LpInteger, lowBound=0)  # 超过平均值的偏差
dev_neg = LpVariable.dicts("dev_neg", employee_dict, cat=LpInteger, lowBound=0)  # 低于平均值的偏差
avg_appointments = appointment_sum / employee_count

for e in employee_dict:
    prob += empl_appo_count[e] - avg_appointments == dev_pos[e] - dev_neg[e]

# 更新目标函数:优先最小化总任务数,其次惩罚偏差
prob += lpSum(empl_appo_count[e] for e in employee_dict) + 0.1 * lpSum(dev_pos[e] + dev_neg[e] for e in employee_dict)

5. 可选优化:避免连续不同预约分配给同一名员工

添加软约束惩罚连续时段被同一名员工分配不同预约的情况:

# 定义连续时段对
consecutive_slots = [(slot, slot+1) for slot in range(1, max(slot_appo_dict.keys()))]

# 二元变量:标记员工在连续时段分配不同预约的冲突
conflict = LpVariable.dicts("conflict", (employee_dict, consecutive_slots), cat=LpBinary)

for e in employee_dict:
    for (s1, s2) in consecutive_slots:
        appos_s1 = [appo for appo, slots in appointment_slots.items() if s1 in slots]
        appos_s2 = [appo for appo, slots in appointment_slots.items() if s2 in slots]
        # 若员工在s1和s2分配了不同预约,触发冲突标记
        prob += lpSum(empl_appo_bin[appo1][e] for appo1 in appos_s1) + lpSum(empl_appo_bin[appo2][e] for appo2 in appos_s2) - 2 * lpSum(empl_appo_bin[appo][e] for appo in appos_s1 if appo in appos_s2) <= 1 + conflict[e][(s1,s2)]

# 目标函数添加冲突惩罚(权重可调整)
prob += lpSum(empl_appo_count[e] for e in employee_dict) + 0.1 * lpSum(dev_pos[e] + dev_neg[e] for e in employee_dict) + 0.5 * lpSum(conflict[e][pair] for e in employee_dict for pair in consecutive_slots)

完整修改代码

def problem_modulation(date):
    prob = LpProblem("equally_employee_allocation", LpMinimize)

    # 员工字典
    employee_dict = {1:"Steve", 2:"Marcus", 3:"Rodger", 4:"Sara"}
    employee_count = len(employee_dict)

    # 预约字典:键=预约ID,值=覆盖的时段列表
    appointment_slots = {
        "A": [3,4],  # 6:10-6:20的预约
        "B": [5,6],  # 6:20-6:30的预约1
        "C": [5,6]   # 6:20-6:30的预约2
    }

    # 生成时段任务数字典(从预约反向推导)
    slot_appo_dict = {slot:0 for slot in range(1,7)}
    for appo_slots in appointment_slots.values():
        for slot in appo_slots:
            slot_appo_dict[slot] += 1

    # 总任务数(所有时段任务数之和)
    appointment_sum = sum(slot_appo_dict.values())
    avg_appointments = appointment_sum / employee_count

    # 二元变量:empl_appo_bin[appo][e] = 1 表示员工e分配到预约appo
    empl_appo_bin = LpVariable.dicts("employ_appoi_bin", (appointment_slots.keys(), employee_dict), cat=LpBinary)

    # 员工总任务数变量
    empl_appo_count = LpVariable.dicts("employ_appoi_count", employee_dict, cat=LpInteger, lowBound=0)

    # 偏差变量:用于均匀分配软约束
    dev_pos = LpVariable.dicts("dev_pos", employee_dict, cat=LpInteger, lowBound=0)
    dev_neg = LpVariable.dicts("dev_neg", employee_dict, cat=LpInteger, lowBound=0)

    # 目标函数:总任务数 + 偏差惩罚 + 冲突惩罚
    prob += lpSum(empl_appo_count[e] for e in employee_dict) + \
            0.1 * lpSum(dev_pos[e] + dev_neg[e] for e in employee_dict) + \
            0.5 * lpSum(LpVariable.dicts("conflict", (employee_dict, [(s,s+1) for s in range(1,6)]), cat=LpBinary)[e][pair] for e in employee_dict for pair in [(s,s+1) for s in range(1,6)])

    # 硬约束1:每个预约必须分配给1名员工
    for appo in appointment_slots.keys():
        prob += lpSum(empl_appo_bin[appo][e] for e in employee_dict) == 1

    # 硬约束2:每个时段的分配员工数等于该时段的任务数
    for slot in slot_appo_dict:
        relevant_appos = [appo for appo, slots in appointment_slots.items() if slot in slots]
        prob += lpSum(empl_appo_bin[appo][e] for appo in relevant_appos for e in employee_dict) == slot_appo_dict[slot]

    # 硬约束3:计算员工总任务数
    for e in employee_dict:
        prob += empl_appo_count[e] == lpSum(
            len(appointment_slots[appo]) * empl_appo_bin[appo][e] 
            for appo in appointment_slots.keys()
        )

    # 软约束:均匀分配(手动构建偏差)
    for e in employee_dict:
        prob += empl_appo_count[e] - avg_appointments == dev_pos[e] - dev_neg[e]

    # 可选:添加连续不同预约的冲突惩罚约束
    consecutive_slots = [(s, s+1) for s in range(1, max(slot_appo_dict.keys()))]
    conflict = LpVariable.dicts("conflict", (employee_dict, consecutive_slots), cat=LpBinary)

    for e in employee_dict:
        for (s1, s2) in consecutive_slots:
            appos_s1 = [appo for appo, slots in appointment_slots.items() if s1 in slots]
            appos_s2 = [appo for appo, slots in appointment_slots.items() if s2 in slots]
            # 若员工在s1和s2分配了不同预约,标记冲突
            prob += lpSum(empl_appo_bin[appo1][e] for appo1 in appos_s1) + lpSum(empl_appo_bin[appo2][e] for appo2 in appos_s2) - 2 * lpSum(empl_appo_bin[appo][e] for appo in appos_s1 if appo in appos_s2) <= 1 + conflict[e][(s1,s2)]

    prob.writeLP("data/equally_allocation.lp")
    prob.solve()

    # 输出结果
    for e in employee_dict:
        print(f"员工{employee_dict[e]}的总任务数:{empl_appo_count[e].value()}")
        assigned_appos = [appo for appo in appointment_slots.keys() if empl_appo_bin[appo][e].value() == 1]
        print(f"分配的预约:{assigned_appos}")

关键说明

  • 数据结构重构是解决跨时段预约绑定的核心,必须明确每个预约覆盖的时段
  • 以预约为单位分配员工,避免了对每个时段重复约束,提升模型效率
  • 手动构建偏差变量替代Pulp的弹性约束,支持动态基准值(若需)
  • 连续不同预约的约束为软约束,可通过调整惩罚权重平衡排班效率与员工休息需求

内容的提问来源于stack exchange,提问作者Lukas Wisniewski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 19:45:21