スタッフスケジューリング

🔥 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シフト、休憩、公平性 | | 目的 | 最小勤務数の最大化(マキシミン公平性) | | 応用 | 病院、コールセンター、小売、工場、警備 |

会員限定無料チュートリアル

このチャプターは登録会員限定の無料コンテンツです!ログインまたは登録してすぐにロックを解除してください。

今すぐログイン / 登録