スタッフスケジューリング
🔥 Vibe プロンプト
「スタッフスケジュールを作成:10人の従業員、7日間、3シフト。各シフトの要件を満たし、公平性を最大化。」
スタッフスケジューリングとは?
**スタッフスケジューリング(勤務シフト割当)**は、従業員をシフトに割り当てる問題です。ビジネスニーズ(人員充足)と従業員の希望(公平性)の両方を満たす必要があります。
| 制約タイプ | 例 | |-----------|------| | カバレッジ | 朝シフトに3人必要 | | 1日1シフト | 同一従業員が2シフト勤務不可 | | 休憩時間 | 夜勤→朝勤の連続禁止 | | 公平性 | 全員の勤務数を均等に | | スキル | シニア従業員を各シフトに配置 |
CP-SATによる実装
from ortools.sat.python import cp_model
NUM_EMPLOYEES = 10
DAYS = 7
SHIFTS = ["朝", "午後", "夜"] # 0, 1, 2
# 要件: (day, shift) → 最低人数
requirements = {
(0,0):3, (0,1):2, (0,2):1,
(1,0):3, (1,1):2, (1,2):1,
(2,0):3, (2,1):2, (2,2):1,
(3,0):3, (3,1):2, (3,2):1,
(4,0):3, (4,1):2, (4,2):2,
(5,0):2, (5,1):3, (5,2):2,
(6,0):2, (6,1):2, (6,2):1,
}
model = cp_model.CpModel()
# 決定変数
shifts = {}
for e in range(NUM_EMPLOYEES):
for d in range(DAYS):
for s in range(len(SHIFTS)):
shifts[(e, d, s)] = model.NewBoolVar(f"s_{e}_{d}_{s}")
# 制約1: カバレッジ
for (d, s), required in requirements.items():
total = sum(shifts[(e, d, s)] for e in range(NUM_EMPLOYEES))
model.Add(total == required)
# 制約2: 1日1シフト
for e in range(NUM_EMPLOYEES):
for d in range(DAYS):
total = sum(shifts[(e, d, s)] for s in range(len(SHIFTS)))
model.Add(total <= 1)
# 制約3: 夜勤→朝勤の禁止
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, "min_shifts")
for e in range(NUM_EMPLOYEES):
total = sum(shifts[(e, d, s)] for d in range(DAYS) for s in range(len(SHIFTS)))
model.Add(min_shifts <= total)
model.Maximize(min_shifts)
# 求解
solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = 10
status = solver.Solve(model)
# 結果
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
days_of_week = ["月", "火", "水", "木", "金", "土", "日"]
print(f"\n{'='*75}")
print(f"スタッフスケジュール {'(最適)' if status == cp_model.OPTIMAL else '(実行可能)'}")
print(f"{'='*75}")
print(f"\n{'従業員':<10} " + " ".join(f"{d:<4}" for d in days_of_week))
print(f"{'-'*10} " + "-"*28)
for e in range(NUM_EMPLOYEES):
row = f"社員{e+1:<4} "
for d in range(DAYS):
assigned = "-"
for s in range(len(SHIFTS)):
if solver.Value(shifts[(e, d, s)]) == 1:
assigned = SHIFTS[s][0]
break
row += f"{assigned:<4} "
print(row)
print(f"\nカバレッジ確認:")
for d in range(DAYS):
for s in range(len(SHIFTS)):
assigned = sum(solver.Value(shifts[(e, d, s)]) for e in range(NUM_EMPLOYEES))
required = requirements.get((d, s), 0)
mark = "✅" if assigned == required else "❌"
print(f" {days_of_week[d]} {SHIFTS[s]}: {int(assigned)}/{required} {mark}")
応用
| 産業 | 規模 | |------|------| | 病院 | 100-500人の看護師、24時間365日 | | コールセンター | 50-200人のオペレーター、ピーク時対応 | | 小売 | 20-100人の店員、季節変動対応 | | 工場 | 3交代制、24時間操業 | | 警備 | 24時間365日、現場別配置 |
高度な制約
# 従業員の希望
preferences = {
0: {"朝": 5, "午後": 3, "夜": 1},
1: {"朝": 2, "午後": 5, "夜": 3},
}
# 希望充足度を最大化
pref_score = model.NewIntVar(0, 100, "preference_score")
total_pref = sum(
preferences[e].get(SHIFTS[s], 1) * shifts[(e, d, s)]
for e in preferences
for d in range(DAYS)
for s in range(len(SHIFTS))
)
model.Maximize(pref_score)
# スキル要件
skills = {0: "senior", 1: "junior", 2: "senior", 3: "mid", 4: "junior",
5: "mid", 6: "senior", 7: "junior", 8: "mid", 9: "junior"}
for d in range(DAYS):
for s in range(len(SHIFTS)):
total_senior = sum(shifts[(e, d, s)] for e in range(NUM_EMPLOYEES) if skills[e] == "senior")
model.Add(total_senior >= 1)
まとめ
| 項目 | 詳細 | |------|------| | 問題 | 従業員をシフトに公平に割り当て | | 変数 | バイナリ: 従業員×日×シフト | | 制約 | カバレッジ、1日1シフト、休憩、公平性 | | 目的 | 最小勤務数の最大化(マキシミン公平性) | | 応用 | 病院、コールセンター、小売、工場、警備 |