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

『もふもふ羊の牧場囲い』の裏側:
最小点カット問題から「予算制約付き・最大重み連結領域問題」へ

対象読者:混合整数線形計画(MIP)の基礎(二値変数・線形不等式・目的関数)をご存じの方。
一見ほのぼのとした「柵でヒツジを囲うパズル」が、グラフ理論の最小点カット問題を土台にしつつ、どのように「穴ぼこ(負の重み)」や「孤立領域の排除(連結性)」を線形制約として定式化しているのかを解説します。

1. パズルのルールとグラフ表現

盤面は無向グリッドグラフ G = (V, E) として表せます。上下左右に隣接する通行可能なマスの組が辺集合 E を構成し、各頂点(マス)v ∈ V は以下の性質を持ちます。

盤面のタイル 集合・記号 柵の設置 重み(得点) w_v 数理モデル上の役割
🐑 ヒツジ S ⊂ V 不可 +2 牧場の起点(湧き出し Source、常に reach_s = 1)
🐺 オオカミ W ⊂ V 不可 0 侵入源(常に reach_w = 0 を強制)
🌱 草地 / 🍓 野いちご V_fence ⊂ V 可能 +1 / +5 正の利得を持つ通常頂点(柵を置くと通行不可)
🕳️ 穴ぼこ V_fence ⊂ V 可能 -5 負の重みを持つ頂点(牧場に入ると減点、真上に丸太の柵を打ち込んで塞げる)
🌉 石橋 V_bridge ⊂ V 不可 +1 通行できるがカット不能な頂点(カット容量 ∞)
💧 水辺 / 🪨 岩 V_wall 不可 — 最初から通行不能(グラフ V から除外して扱える天然の壁)

プレイヤーの目的は、最大 K 本までの柵を設置してすべてのオオカミ W からヒツジ S への経路を遮断し、その状態でヒツジ S から歩いて到達できる連結領域(まきば)の合計スコアを最大化することです。

2. もし「最小何本で守れるか」だけなら?(最小 s-t 点カット問題)

まずはスコア(いちごや穴ぼこ)を一旦忘れ、「最小何本の柵でオオカミ W とヒツジ S を分断できるか?」という部分問題だけを取り出してみます。

通常の最小カット問題はグラフの「辺(Edge)」を切りますが、このパズルでは「マス(頂点 Vertex)」の上に柵を置いて通行止めにするため、グラフ理論における最小 s-t 点カット問題(Minimum Vertex Cut)に対応します。

💡 メンガーの定理(Menger's Theorem)とパズルの難易度設計 「オオカミ W からヒツジ S へ至る頂点素な(互いに柵設置可能マスを共有しない)経路の最大本数は、両者を分断するのに必要な柵の最小本数に等しい」という定理が成り立ちます。
たとえば石橋 🌉(カット不能頂点)を介してオオカミの侵入路が3本に分岐していれば、オオカミの目の前を1本の柵で塞ぐことは原理的に不可能になります。

なぜ単なる「最小カット問題」では終わらないのか

このパズルが最小カット問題より一段深く、かつ面白い理由は次の2点にあります。

  1. 「最小本数」ではなく「上限 K 本の予算制約下での領域価値最大化」:
    1本で塞げる狭い通路があっても、あえて2本の柵を使って手前の分岐を抑えた方が、その奥にある野いちご畑(🍓 +5)を牧場に取り込める場合があります。つまり「どの方向の戦線に柵予算 K を投資するか」というナップサック問題的なトレードオフが絡みます。
  2. 負の重み「🕳️ 穴ぼこ(-5 点)」の存在:
    オオカミが全くいない袋小路であっても、そこに 🕳️ (-5) があれば、余った柵を1本使ってその袋小路を切り離す(あるいは 🕳️ の真上に丸太の柵を打ち込んで穴を塞ぐ)ことでスコアが +5 点改善します。逆に、🕳️ (-5) のさらに奥に 🍓×3 (+15) が眠っていれば、あえて穴ぼこを塞がず奥へ道をつなぐ方が得になります(Stage 7『穴ぼこの向こう側』)。

3. MILPによる定式化(1):決定変数と「フラッド制約」

それでは、このパズルを混合整数線形計画(MILP)として定式化していきましょう。通行可能な各マス v ∈ V に対して、以下の2種類の二値変数を導入します。

二値決定変数(Binary Decision Variables) f_v ∈ {0, 1}   (∀v ∈ V_fence)   : マス v に木の柵を置くなら 1、置かないなら 0
r_v ∈ {0, 1}   (∀v ∈ V)         : マス v が「ヒツジの安全な牧場」に含まれるなら 1

まず、自明な境界条件と柵の本数上限(予算制約)を書きます。

基本制約(起点・侵入源・排他・予算) (1) r_s == 1            (∀s ∈ S)       : ヒツジのいるマスは必ず牧場内
(2) r_w == 0            (∀w ∈ W)       : オオカミのいるマスは絶対に牧場に入れてはならない
(3) r_v + f_v <= 1      (∀v ∈ V_fence) : 柵を置いたマス自体は牧場に含まれない
(4) Σ_{v ∈ V_fence} f_v <= K          : 置ける柵は最大 K 本まで

隣接マスへ牧場が広がる「フラッド制約(Flood / Cut Constraint)」

次に、「ヒツジの牧場 r_u = 1 の隣マス v に柵がなければ(f_v = 0)、その隣マス v も自動的に牧場と地続きになる(r_v = 1)」という物理法則を1本の線形不等式で表します。

フラッド制約(すべての隣接ペア (u, v) ∈ E について) r_u - r_v - f_v <= 0    (変形すると:   r_u <= r_v + f_v)

このシンプルな不等式 r_u <= r_v + f_v は、1本で2つの重要な役割を同時に果たします。

  • 役割A(オオカミの完全遮断):
    ヒツジ(r_s = 1)から始まる道の途中で一度も柵(f_v = 1)が置かれないままオオカミ(r_w = 0)に到達すると、1 = r_s <= … <= r_w = 0 となって制約違反(Infeasible)になります。つまり、すべての侵入経路のどこかに必ず f_v = 1 を置かなければ実行可能解になりません。
  • 役割B(穴ぼこ 🕳️ -5 のつまみ食い防止):
    もしこの制約がなければ、ソルバーは「正の得点マス(🍓 +5)だけ r_v = 1 にし、牧場の真ん中にある穴ぼこ(🕳️ -5)は都合よく r_v = 0 にする」という抜け道を使えてしまいます。
    しかしフラッド制約があるため、牧場(r_u = 1)に隣接する穴ぼこ v は、丸太の柵を打ち込んで塞ぐ(f_v = 1, r_v = 0)か、牧場に含めて -5 点を受け入れる(f_v = 0, r_v = 1)かの二択を厳密に迫られます。

4. MILPによる定式化(2):「孤立ポケット問題」と単一品種フロー

ここで数理モデリング上の興味深い落とし穴が登場します。実は、前節のフラッド制約だけでは、まだ正しい定式化にはなりません。

❌ フラッド制約のみの場合の不整合
🐺
🌱
💧
🍓
💧
🌱
💧
💧
💧
🌱
🐑
🌱
🍓

右上の 🍓 は木の柵と水辺💧でヒツジから完全に切り離されているのに、r_v = 1 にしてもフラッド制約 r_u <= r_v + f_v に違反しない。

⭕ 単一品種フロー保存則を入れた場合
🐺
🌱
💧
🍓
💧
🌱
💧
💧
💧
🌱
🐑
🌱
🍓

ヒツジ🐑を水源とする水流(ネットワークフロー)が届くマスだけが r_v = 1 になれる。

なぜこの現象が起きるかというと、フラッド制約 r_u <= r_v + f_v は「ヒツジから到達できるマスはすべて 1 になる(下界)」は強制しますが、「柵や壁で囲まれた別の孤立ポケットの中身をまるごと r_v = 1 にする」ことは禁止していないからです。

解決策:単一品種ネットワークフロー(Single-Commodity Flow)

「r_v = 1 ならば、そのマス v はヒツジ S と実際にパスで繋がっていなければならない」という連結性を線形制約で表す定番の手法が、単一品種フロー保存則です。

隣接する各マスの組に有向連続変数 flow_{u→v} >= 0(マス u から隣マス v へ流す「水」の量)を用意し、以下の制約を加えます(M = |V| は通行可能マスの総数):

連結性を保証する単一品種フロー制約 (5) 0 <= flow_{u→v} <= M * r_u   (∀(u, v) ∈ E) : 牧場内のマス u からしか水を出せない
(6) 0 <= flow_{u→v} <= M * r_v   (∀(u, v) ∈ E) : 牧場内のマス v へしか水を流せない
(7) Σ_{u} flow_{u→v} - Σ_{w} flow_{v→w} == r_v   (∀v ∈ V \ S) : ヒツジ以外の各牧場マス(r_v = 1)で水をちょうど 1 単位消費する
🔍 なぜこれで連結性が厳密に保証されるのか ヒツジ以外のマス v を r_v = 1 にするためには、式(7)より「どこかから 1 単位の水が流れ込んでくる」必要があります。しかし式(5)・(6)より、水は r = 1 のマス同士の間しか流れず、かつ水を新たに生み出せる(流入超過が許される)のは式(7)の対象外であるヒツジのマス S だけです。
したがって、ヒツジ S と柵なしで繋がっていない孤立ポケットには一滴も水が届かず、必ず r_v = 0 になります。

5. 目的関数と「1手ヒント(部分固定再求解)」

最後に目的関数です。「牧場の合計スコアを最大化する(第1優先)」とともに、同じスコアなら「使う柵の本数をできるだけ少なくする(第2優先)」という辞書式順序(Lexicographic Objective)を、重み係数 100 を使って1本の線形目的関数にまとめています。

目的関数(Objective Function) maximize   Σ_{v ∈ V} (100 * w_v) * r_v  -  Σ_{v ∈ V_fence} 1 * f_v

また、ゲーム内の「💡 1手ヒント」ボタンは、あらかじめ用意した固定の答えを見ているのではなく、プレイヤーが現在盤面に置いている柵集合 F_cur を f_v == 1 (∀v ∈ F_cur) として固定した上でリアルタイムに再求解しています:

  • 固定した状態での最適スコアが本来の理論値 Z* に届く場合:
    「現在の柵配置は正解ルートに乗っている」と判定し、残りの最適柵 f_v* = 1 (v ∉ F_cur) のうち1マスを盤面上で黄色く点滅させます。
  • 固定した状態では Infeasible(オオカミを防げない)または Z* 未満にしかならない場合:
    「現在置いている柵の中に邪魔な柵がある」と判定し、外すべき柵のマスを赤く点滅させて教えてくれます。

実際のモデル構築コード(抜粋)

// 1. フラッド制約(u が牧場内で隣 v に柵がなければ、v も必ず牧場内)
if (vFence) {
  model.constraint(expr`${uReach} - ${vReach} - ${vFence} <= 0`);
} else {
  model.constraint(expr`${uReach} - ${vReach} <= 0`);
}

// 2. 単一品種フロー(u と v の両方が牧場内のときのみフローを流せる)
const flowUV = model.continuous(`flow_${r}_${c}_to_${nr}_${nc}`, { lb: 0, ub: maxFlowM });
model.constraint(expr`${flowUV} - ${maxFlowM} * ${uReach} <= 0`);
model.constraint(expr`${flowUV} - ${maxFlowM} * ${vReach} <= 0`);

// 3. 非ヒツジ頂点でのフロー保存則(流入 - 流出 == r_v)
model.constraint(expr`${sum(inList)} - ${sum(outList)} - ${rVar} == 0`);