数理最適化(MILP)モデリング解説

『魔王城の24時間警備シフト』の裏側:
勤務スケジューリング問題(NSP)と論理条件の線形化

対象読者:混合整数線形計画(MIP)の基礎(二値変数・線形不等式・目的関数)をご存じの方。
現実の病院(ナース・スケジューリング)やコールセンター・警備現場で用いられるシフト最適化モデルが、ゲーム内の「夜勤明け日勤の禁止」「連勤上限」「ペア相性(二次項の線形化)」「単独夜勤の禁止」にどう1対1対応しているのかを解説します。

1. 警備シフトパズルと勤務スケジューリング問題(NSP)

オペレーションズ・リサーチ(OR)の代表的な応用分野にナース・スケジューリング問題(Nurse Scheduling Problem; NSP)や人員配置問題(Staff Rostering)があります。『魔王城の24時間警備シフト』は、この古典的かつ実用的なMILPモデルをそのままファンタジー世界の拠点防衛パズルに翻案したものです。

ゲームの要素 実務のシフト最適化での対応物 数理モデル上の表現
警備モンスター m ∈ M スタッフ・看護師・警備員 添字集合 M(各人のスキル・体力上限を持つ)
日付 d ∈ D と昼番/夜番 s ∈ {day, night} 計画期間の各日・各シフト区分 二値決定変数 x_{m,d,day}, x_{m,d,night} ∈ {0, 1}
勇者軍の襲来戦力 R_{d,s} 時間帯ごとの必要スキル量・需要予測 被覆制約(Covering Constraint)の右辺定数
夜番→翌日昼番の禁止 勤務間インターバル規制(夜勤明け日勤禁止) 隣接シフト間の排他制約 x_{m,d,night} + x_{m,d+1,day} <= 1
連勤上限・週出勤上限・指定休 労働基準ルール・公休・希望休 スライディング・ウィンドウ制約と変数固定
プルルの「夜間単独配置の禁止」 新人スタッフの単独夜勤禁止(OJT同伴制約) 含意制約(Implication Constraint)
仲良しコンビ・不仲ペア ペア配置推奨・同時配置回避 積 x_A * x_B の McCormick 線形化変数 p_{A,B,d,s}

2. 決定変数と労務制約の定式化

計画対象の日集合を D = {0, 1, …, T-1}、出撃可能な魔物の集合を M とします。各魔物 m ∈ M・各日 d ∈ D に対して、昼番に入るかどうかの二値変数 day_{m,d} ∈ {0, 1} と、夜番に入るかどうかの二値変数 night_{m,d} ∈ {0, 1} を定義します(両方 0 ならその日は「休み」です)。

労務・インターバル制約(すべての魔物 m ∈ M について) (1) 1日1シフトまで:         day_{m,d} + night_{m,d} <= 1            (∀d ∈ D)
(2) 夜番→翌日昼番の禁止:    night_{m,d} + day_{m,d+1} <= 1          (∀d ∈ {0, …, T-2})
(3) 週あたり最大出勤日数:    Σ_{d ∈ D} (day_{m,d} + night_{m,d}) <= W_m
(4) 指定休(希望休):        day_{m,d} + night_{m,d} == 0            (∀(m, d) ∈ OffRequests)

最大連勤日数制約(スライディング・ウィンドウ法)

たとえば「古竜ドラゴは最大 L_m = 1 連勤まで(=1日働いたら翌日は必ず休み)」「ウェアウルフのフェンリルは最大 L_m = 3 連勤まで」といった連勤制限は、幅 L_m + 1 日のスライディング・ウィンドウを1日ずつずらしながら、「連続するどの L_m + 1 日間を切り取っても、その中の出勤日数は最大 L_m 日以下である(=最低1日は休みがある)」と書けます。

スライディング・ウィンドウによる連勤上限(幅 L_m + 1) Σ_{k = 0}^{L_m} (day_{m, start + k} + night_{m, start + k}) <= L_m    (∀start ∈ {0, …, T - L_m - 1})
💡 なぜスライディング・ウィンドウが有効なのか 「何連勤目か」を数える状態変数や非線形な条件分岐を一切使わずに、純粋な線形不等式だけで任意の連勤上限 L_m を漏れなく表現できます。しかもLP緩和が比較的タイトになるため、MILPソルバーの分枝限定法が高速に収束します。

3. 論理条件の線形化(1):「新米スライムの夜間単独配置禁止」

ゲーム中盤から登場する新米スライムのプルルには、「夜番に入るときは1人きりにせず、必ず他の誰かと一緒に配置しなければならない」というルールがあります(実務のシフト表でも「新人看護師は夜勤に単独配置しない」という全く同じ制約が頻出します)。

論理式で書くと「もし night_{pururu, d} == 1 ならば、Σ_{m ≠ pururu} night_{m, d} >= 1」という含意(Implication)になりますが、二値変数の性質を使うと、たった1本の線形不等式に変換できます。

同伴配置(単独夜勤禁止)の線形不等式 night_{pururu, d} - Σ_{m ∈ M \ {pururu}} night_{m, d} <= 0    (∀d ∈ D)

実際、他のメンバーが誰も夜番に入っていない(Σ_{m ≠ pururu} night_{m, d} == 0)ときは、night_{pururu, d} <= 0 となってプルルも配置できなくなり、逆に他の誰か1人でも夜番に入っていれば(和が 1 以上)、night_{pururu, d} <= 1 となってプルルも配置可能になります。

4. 論理条件の線形化(2):ペア相性(二次項 x_A * x_B)の線形化

さらにゲーム性を深めているのが、メンバー間のペア相性(連携ボーナスと同番拒否)です。

  • 仲良し・連携ペア(Δ_{A,B} > 0):同じシフト (d, s) に A と B の両方が入ると、連携ボーナスで防衛力と士気が +Δ_{A,B} 上がる。
  • 不仲・同番拒否ペア(Δ_{A,B} < 0):同じシフトに A と B が一緒に入ると、反発して防衛力と士気が Δ_{A,B} 下がる。

これを素直に式にすると、A の配置変数 x_{A,d,s} と B の配置変数 x_{B,d,s} の積(二次項)x_{A,d,s} * x_{B,d,s} が現れてしまい、そのままでは線形計画(MILP)になりません。

補助二値変数 pair_{A,B,d,s} ∈ {0, 1} による標準線形化(Fortet / McCormick 線形化)

そこで、「A と B が同じ枠 (d, s) に同時配置されたときだけ 1 になる補助二値変数 pair_{A,B,d,s} ∈ {0, 1}」を導入し、次の3本の線形不等式で積 x_A * x_B と完全に等価にします。

二値変数の積 pair == x_A * x_B を表す3本の線形不等式 (a) pair_{A,B,d,s} - x_{A,d,s} <= 0            : A がいない(0)なら pair も必ず 0
(b) pair_{A,B,d,s} - x_{B,d,s} <= 0            : B がいない(0)なら pair も必ず 0
(c) x_{A,d,s} + x_{B,d,s} - pair_{A,B,d,s} <= 1 : A と B が両方いる(1+1=2)なら pair は必ず 1
✨ 正のシナジーも負のペナルティもこの3本で厳密に表現できる理由 もし正のボーナス(Δ_{A,B} > 0)だけなら式(a)・(b)の上界だけでも目的関数が自然に pair = 1 を選びますが、負のペナルティ(Δ_{A,B} < 0、不仲ペア)がある場合はソルバーがペナルティを逃れようとして pair = 0 に下げようとします。そこに式(c)の下界 pair >= x_A + x_B - 1 があることで、「2人を同時に置いたら必ず pair = 1 になって減点を受ける」ことが厳密に強制されます。

5. 防衛力被覆制約と「別解の列挙(No-Good Cut)」

各時間帯 (d, s) において、個人の実効防衛力 P_{m,s} とペア相性補正の合計が必要防衛力 R_{d,s} 以上になるよう被覆制約(Covering Constraint)を課し、全期間を通じた魔王城の総士気(Morale)を最大化します。

各襲来枠 (d, s) の防衛力・最低人数制約と目的関数 防衛力被覆:   Σ_{m ∈ M} P_{m,s} * x_{m,d,s} + Σ_{(A,B)} Δ_{A,B} * pair_{A,B,d,s} >= R_{d,s}
最低人数:     Σ_{m ∈ M} x_{m,d,s} >= N_{d,s}
目的関数:     maximize   1000 + Σ_{d,s} ( Σ_{m} morale_{m,s} * x_{m,d,s} + Σ_{(A,B)} Δ_{A,B} * pair_{A,B,d,s} )

複数の最適シフト案を次々と提案する「No-Good Cut」

実務でもゲームでも、「条件を満たすシフト表を1つだけでなく、別のパターンも見てみたい」という場面があります。すでに得られた解の 1 の変数集合を S_1、0 の変数集合を S_0 としたとき、以下のNo-Good Cut(整数カット)を1本追加して再求解すると、直前のシフト表と最低1マス以上異なる次善解(または同点の別最適解)を瞬時に得ることができます。

// 直前のシフト割当と完全に同一の0-1パターンだけを禁止する No-Good Cut
// Σ_{v ∈ S_1} v - Σ_{u ∈ S_0} u <= |S_1| - 1
m.constraint(
  expr`${sum(activeOnes)} - ${sum(activeZeros)} <= ${activeOnes.length - 1}`,
  "forbid_exact_schedule"
);