Chapter 7

スケジューリング問題の地図

本書は「スケジューリング、スケジューリング」と言ってきたけど、 実はその中身は大陸ひとつ分くらい広い。
1 台の機械の話なのか、何百台の話なのか。
ジョブの順序は決まっているのか、機械の選択肢があるのか。
何を最小化したいのか。

この章で、その地図を読めるようになる。

ここから第III部「スケジューリング実戦」に入る。
理論よりも、現場で必要な語彙と直感を優先する。

7.1 共通言語: α | β | γ 表記

1979 年に Graham らが提案した、スケジューリング問題の共通の書き方がある。
バーで囲んだ三つのフィールドで、問題をひとことに記述する。

α 機械の世界 1, P, Q, F, J, FJ … | β ジョブの事情 prec, r_j, d_j, s_ij … | γ 何を最小化 C_max, ΣT_j … 例: 1 | r_j | ΣT_j = 「1 機械で、リリース時刻あり、総遅れを最小化」 例: J | | C_max = 「ジョブショップで makespan を最小化」 (古典中の古典) 例: FJ | s_ij, prec | Σw_j T_j = 現実工場っぽい問題
図 7.1 — α|β|γ 表記。3 つの空欄に何を入れるかで、世界中の論文が同じ問題を指せる。

論文を読むときに「J3 | r_j | C_max は…」と書いてあったら、慌てずに分解する。
α=J3 (3 機械のジョブショップ)、β=r_j (リリース時刻あり)、γ=C_max (makespan)。
これで何の問題かは即わかる。

α: 機械の世界

記号意味
1機械が 1 台だけ
Pm同じ性能の機械が m 台 (並列)
Qm速さの違う機械が m 台
Fmフローショップ: 全ジョブが m 機械を同じ順で通る
Jmジョブショップ: ジョブごとに違う順で機械を通る
FJm柔軟ジョブショップ: 各工程に複数の機械選択肢

β: ジョブの事情

記号意味
rjリリース時刻 (これより前は始められない)
dj納期
precジョブ間に「これが終わってからこれ」の順序
sij段取り時間 (前のジョブによって変わる)
prmp中断可能

γ: 最小化対象

記号意味
Cmaxmakespan (全ジョブの最終終了時刻)
ΣCj総完了時刻
Lmax最大遅刻
ΣTj総遅れ (納期超過のみ)

7.2 ガントチャートで見る JSP

スケジュールを見える化する最強の道具がガントチャート。
横軸が時間、縦軸が機械。タスクは長方形。

3 ジョブ × 3 機械の JSP の例を見てみよう。

0 2 4 6 8 10 12 14 16 18 20 M1 M2 M3 J1-o1 (3) J3-o1 (2) J2-o2 (4) J1-o2 (4) J3-o2 (3) J2-o3 (4) J2-o1 (5) J1-o3 (2) J3-o3 (4) C_max = 16 (makespan)
図 7.2 — JSP のガントチャート。J1 は赤、J2 は青、J3 は緑。最後に終わる時刻が makespan。

この絵をじっと見て確認:

  • 各機械の上で、長方形は重ならない (= 同時に 2 つは無理)
  • 各ジョブの工程は順番通りに並んでいる (J1 なら M1 → M2 → M3)
  • makespan (=16) を縮めるには、どこかを詰める必要がある

7.3 対立グラフ — JSP の構造を見抜く道具

JSP の解を「グラフ」として表現する強力な道具がある。対立グラフ (disjunctive graph)

工程をノード、確定の順序を実線、「どっちが先か未決」の対立辺を破線。
解くこととは、破線の向きを全部決めることに等しい。

s t J1₁ M1, 3 J1₂ M2, 4 J1₃ M3, 2 J2₁ M3, 5 J2₂ M1, 4 J2₃ M2, 4 J3₁ M1, 2 J3₂ M2, 3 J3₃ M3, 4 → : 工程順序 (確定) ⤳ : 同一機械の対立辺 (向きを決めるのが解くこと)
図 7.3 — 対立グラフ表現。同じ機械を使う工程のペアに「どっちが先?」の対立辺がある。

すべての対立辺の向きを決めて、できたグラフがサイクルを含まなければ、それが実行可能解。
そして**makespan は「s から t への最長経路」**に一致する。
これがクリティカルパスの正体だ。

7.4 主要モデルざっくり整理

問題難しさ定番解法
1ΣCj
1Lmax
1sijCmax
PCmax
F2Cmax
Fm≥3Cmax
JCmax
FJCmax

7.5 やってみよう: CP-SAT で JSP を 30 行で書く

ここで第6章で予告した CP-SAT を、JSP に適用する。

from ortools.sat.python import cp_model

# 各ジョブ = [(machine_id, duration), ...]
jobs = [
    [(0, 3), (1, 4), (2, 2)],   # J1: M1→M2→M3
    [(2, 5), (0, 4), (1, 4)],   # J2: M3→M1→M2
    [(0, 2), (1, 3), (2, 4)],   # J3: M1→M2→M3
]
horizon = sum(d for job in jobs for _, d in job)
m = cp_model.CpModel()

intervals, ends, by_machine = {}, {}, {}
for j, job in enumerate(jobs):
    for o, (mc, dur) in enumerate(job):
        s = m.NewIntVar(0, horizon, f"s_{j}_{o}")
        e = m.NewIntVar(0, horizon, f"e_{j}_{o}")
        iv = m.NewIntervalVar(s, dur, e, f"iv_{j}_{o}")
        intervals[(j, o)] = (s, e, iv)
        ends[(j, o)] = e
        by_machine.setdefault(mc, []).append(iv)

# 同じ機械の interval は重ならない
for mc, ivs in by_machine.items():
    m.AddNoOverlap(ivs)

# ジョブ内の工程順序
for j, job in enumerate(jobs):
    for o in range(len(job) - 1):
        m.Add(intervals[(j, o+1)][0] >= intervals[(j, o)][1])

# makespan を最小化
C_max = m.NewIntVar(0, horizon, "C_max")
m.AddMaxEquality(C_max, [ends[(j, len(job)-1)] for j, job in enumerate(jobs)])
m.Minimize(C_max)

solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = 10
solver.parameters.num_search_workers = 8       # 並列で 8 倍速
solver.Solve(m)
print("makespan =", solver.ObjectiveValue())

これだけ。30 行で JSP が解ける。
3×3 なら 0.01 秒、10×10 でも数秒で最適解。
20 年前は数日かかった問題が、今は無料の OSS で一瞬。

7.6 FJSP — 機械の選択肢があるバージョン

現実の工場に近いのは、FJSP (Flexible Job Shop)
各工程に「この機械でもいい、こっちの機械でもいい」と選択肢がある。
当然、難易度は跳ね上がる。

CP-SAT には OptionalIntervalVar という強力な道具がある。
「選ばれたときだけ存在する区間変数」だ。これで FJSP が綺麗に書ける。

# jobs[j][o] = [(machine, duration), ...]  ← 工程ごとに選択肢
for j, job in enumerate(jobs):
    prev_end = None
    for o, choices in enumerate(job):
        presences = []
        for k, (mc, dur) in enumerate(choices):
            p = m.NewBoolVar(...)
            s = m.NewIntVar(...); e = m.NewIntVar(...)
            iv = m.NewOptionalIntervalVar(s, dur, e, p, ...)
            by_machine.setdefault(mc, []).append(iv)
            presences.append(p)
        m.Add(sum(presences) == 1)   # ちょうど 1 つだけ選ぶ
        # ... 開始/終了をマージしてジョブ順序の制約を貼る

7.7 目的関数の選び方は、業務そのもの

αとβは「問題の構造」だが、γ (目的関数) は**「あなたが何を大事にしたいか」**そのもの。 ここを決めるのが、最も業務理解を求められる部分だ。

目的こういう業務
Cmax (makespan)夜間バッチ、納品物全体を早く終わらせたい
ΣCj受注品の平均を早く
ΣwjTj大事な顧客は絶対遅らせない
段取り時間段取りが高コスト (印刷、塗装、押出)
WIP 最小化仕掛在庫を抑えたい (JIT)

現実には、これらを同時に最小化したいことが多い。
これは次章 (第8章) で「多目的問題」として扱う。

7.8 ベンチマークインスタンス (お試し問題集)

ベンチマークは半世紀の蓄積。
新しい手法を試すときは、これに対して既存手法と比べる。

7.9 この章の振り返り

次の章では、いよいよ現実の工場に入る。
教科書の世界と何がどう違うのか。Rolling Horizon という運用パターンの話だ。