人員排班與輪值系統

人員排班是 ILP/CP 最經典的應用場景之一:如何安排員工的班次,在滿足所有人力需求的同時,最大化員工滿意度?

模型建立

from ortools.sat.python import cp_model

# === 定義排班需求 ===
DAYS = 7  # 一週 7 天
SHIFTS = ["早班", "中班", "晚班"]
NUM_SHIFTS = len(SHIFTS)
EMPLOYEES = [f"員工{i}" for i in range(10)]
NUM_EMPLOYEES = len(EMPLOYEES)

# 每天每個班次需要的人力
shift_requirements = {
    (0, 0): 2, (0, 1): 2, (0, 2): 1,  # 週一
    (1, 0): 2, (1, 1): 2, (1, 2): 2,  # 週二
    (2, 0): 2, (2, 1): 1, (2, 2): 1,  # 週三
    (3, 0): 3, (3, 1): 2, (3, 2): 1,  # 週四
    (4, 0): 3, (4, 1): 2, (4, 2): 2,  # 週五
    (5, 0): 1, (5, 1): 1, (5, 2): 2,  # 週六
    (6, 0): 1, (6, 1): 1, (6, 2): 2,  # 週日
}

# === 建立模型 ===
model = cp_model.CpModel()

# 變數:shifts[e][d][s] = 1 表示員工 e 在第 d 天的第 s 班
shifts = {}
for e in range(NUM_EMPLOYEES):
    for d in range(DAYS):
        for s in range(NUM_SHIFTS):
            shifts[(e, d, s)] = model.NewBoolVar(f"shift_e{e}_d{d}_s{s}")

# 限制 1:每個班次的人力需求必須滿足
for d in range(DAYS):
    for s in range(NUM_SHIFTS):
        if (d, s) in shift_requirements:
            model.Add(
                sum(shifts[(e, d, s)] for e in range(NUM_EMPLOYEES))
                == shift_requirements[(d, s)]
            )

# 限制 2:每個員工每天最多一個班次
for e in range(NUM_EMPLOYEES):
    for d in range(DAYS):
        model.Add(sum(shifts[(e, d, s)] for s in range(NUM_SHIFTS)) <= 1)

# 限制 3:每個員工每週最多 5 天
for e in range(NUM_EMPLOYEES):
    model.Add(
        sum(shifts[(e, d, s)] for d in range(DAYS) for s in range(NUM_SHIFTS))
        <= 5
    )

# 限制 4:不能連上晚班+早班(禁止晚班後隔天早班)
for e in range(NUM_EMPLOYEES):
    for d in range(DAYS - 1):
        model.Add(shifts[(e, d, 2)] + shifts[(e, d + 1, 0)] <= 1)

# 目標:公平分配(最大化最低班次數)
min_shifts = model.NewIntVar(0, DAYS * NUM_SHIFTS, "min_shifts")
for e in range(NUM_EMPLOYEES):
    total = sum(shifts[(e, d, s)] for d in range(DAYS) for s in range(NUM_SHIFTS))
    model.Add(min_shifts <= total)

model.Maximize(min_shifts)

# === 求解 ===
solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = 30
status = solver.Solve(model)

if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
    print(f"\n=== 人員排班結果 ===")
    
    # 建立排班表
    schedule = [["" for _ in range(DAYS)] for _ in range(NUM_EMPLOYEES)]
    for e in range(NUM_EMPLOYEES):
        for d in range(DAYS):
            for s in range(NUM_SHIFTS):
                if solver.Value(shifts[(e, d, s)]):
                    schedule[e][d] = SHIFTS[s]
    
    # 輸出
    headers = ["員工"] + [f"第{d+1}天" for d in range(DAYS)]
    print(" | ".join(headers))
    print("-" * 50)
    for e in range(NUM_EMPLOYEES):
        row = [EMPLOYEES[e]] + [schedule[e][d] or "-" for d in range(DAYS)]
        print(" | ".join(row))
    
    # 統計
    print(f"\n=== 員工班次統計 ===")
    for e in range(NUM_EMPLOYEES):
        total = sum(1 for d in range(DAYS) if schedule[e][d])
        print(f"{EMPLOYEES[e]}: {total} 天")
else:
    print("找不到可行排班!")


人員排班為什麼這麼難?

如果你曾經排過班表,你就知道這有多痛苦:

  • 早班需要 3 個人,晚班需要 2 個人,大夜班只需要 1 個人
  • 每個人一週不能超過 5 天班
  • 不能連續排 2 天大夜班
  • 特定員工週末不能上班
  • 每個人的偏好不同

手動排班可能花掉主管一整天的時間,而且排出來的班表總是有人不滿意。

CP-SAT 可以在幾秒鐘內解決這個問題。

CP-SAT 的優勢

| 優勢 | 說明 | |:----:|------| | 自然建模 | 直接用「變數 = 決策」的方式描述問題,不需要轉換成線性方程式 | | 全域約束 | 內建 AllDifferentAddExactlyOne 等強大約束 | | 最佳化 | 不僅能找到可行解,還能找出最佳解(最大化滿意度、最小化成本) | | 速度 | 利用 SAT 技術的剪枝能力,比傳統 ILP 求解器快數倍 |

下一章預告:從排班到資源分配

人員排班是「時間 × 人員」的分配問題。下一章的 資源分配 將擴展到更一般的場景——當你有多種資源(設備、空間、材料)需要同時分配時,該如何建模與求解。

會員專屬免費教學

本章節為註冊會員專屬的免費開放內容!請先登入或註冊會員,即可立即解鎖閱讀。

立即登入 / 註冊