Mathematical Optimization (MILP) Formulation Guide MILP

Behind "Fluffy Sheep Pasture Enclosure":
From Minimum Vertex Cut to Budgeted Maximum-Weight Connected Subgraph

TL;DR — This article explains how a cozy puzzle about enclosing sheep with wooden fences builds upon the classic Minimum s-t Vertex Cut problem while formulating negative-weight tiles (pitfalls) and strict region connectivity using linear constraints.

01Puzzle Rules and Graph Representation

The board is modeled as an undirected grid graph G = (V, E), where orthogonally adjacent passable cells form the edge set E. Each vertex (cell) v ∈ V has the following properties:

Board Tile Set / Symbol Fence Allowed Weight (Score) w_v Role in the Mathematical Model
🐑 Sheep S ⊂ V No +2 Pasture origin (Source; fixed to reach_s = 1)
🐺 Wolf W ⊂ V No 0 Hazard source (fixed to reach_w = 0)
🌱 Grass / 🍓 Strawberry V_fence ⊂ V Yes +1 / +5 Positive-reward vertices (impassable when a fence is placed)
🕳️ Pitfall V_fence ⊂ V Yes -5 Negative-weight vertex (penalizes score if enclosed; can be plugged by driving a fence post into it)
🌉 Stone Bridge V_bridge ⊂ V No +1 Passable but uncuttable vertex (infinite cut capacity)
💧 Water / 🪨 Rock V_wall No — Impassable natural walls (excluded from V)

The player's goal is to place at most K fences so that all paths between the wolves W and the sheep S are blocked, while maximizing the total score of the connected pasture reachable from the sheep S.

02What If We Only Minimized Fence Count? (Minimum s-t Vertex Cut)

Suppose we temporarily ignore tile scores and ask only: "What is the minimum number of fences required to separate the wolves W from the sheep S?"

Whereas the standard minimum cut problem removes graph edges, here we block grid cells (vertices) with fences. This corresponds to the Minimum s-t Vertex Cut problem in graph theory.

💡 Menger's Theorem and Level Design
By Menger's Theorem, the maximum number of vertex-disjoint paths (sharing no fenceable cells) between the wolves W and the sheep S equals the minimum number of fences needed to separate them.
For example, if stone bridges 🌉 (uncuttable vertices) split a wolf's approach into three independent corridors, caging the wolf with a single fence right in front of its spawn is mathematically impossible.

Why This Puzzle Goes Beyond a Pure Minimum Cut

Two structural features make this puzzle richer than a textbook minimum cut problem:

  1. Maximizing Enclosed Value Under a Fence Budget K:
    Even when a narrow choke point can be sealed with 1 fence, spending 2 fences further forward to hold a branch junction may capture a rich strawberry patch (🍓 +5) beyond it. Deciding where to invest the fence budget K introduces a knapsack-like trade-off across multiple fronts.
  2. Negative Weights from Pitfalls (🕳️ -5):
    Even in a dead-end corridor with no wolves, if a pitfall 🕳️ (-5) lies connected to the pasture, spending a fence to seal off that corridor—or driving a fence post directly into the pit to plug it—improves the score by +5. Conversely, if three strawberries (+15) lie behind a pitfall (-5), deliberately leaving the pitfall open inside the pasture to keep the path open yields a net gain of +10 (as seen in Stage 7).

03MILP Formulation (Part 1): Decision Variables and the Flood Constraint

To formulate the puzzle as a Mixed-Integer Linear Program (MILP), we introduce two sets of binary decision variables over the passable cells $v \in V$:

Binary Decision Variables $$ \begin{aligned} f_v &\in \{0, 1\} \quad (\forall v \in V_{\text{fence}}) &&: 1 \text{ if a wooden fence is placed on cell } v\text{, 0 otherwise} \\ r_v &\in \{0, 1\} \quad (\forall v \in V) &&: 1 \text{ if cell } v \text{ is part of the sheep's safe pasture, 0 otherwise} \end{aligned} $$

First, we state the boundary conditions, mutual exclusion, and the fence budget constraint:

Base Constraints (Sources, Hazards, Exclusion, and Budget) $$ \begin{aligned} &\text{(1) Sheep Origin:} && r_s = 1 && (\forall s \in S) \\ &\text{(2) Wolf Exclusion:} && r_w = 0 && (\forall w \in W) \\ &\text{(3) Mutual Exclusion:} && r_v + f_v \le 1 && (\forall v \in V_{\text{fence}}) \\ &\text{(4) Fence Budget:} && \sum_{v \in V_{\text{fence}}} f_v \le K \end{aligned} $$

The Flood / Cut Constraint Across Adjacent Cells

Next, we express the propagation rule: "If cell $u$ is inside the pasture ($r_u = 1$) and its neighbor $v$ has no fence ($f_v = 0$), then $v$ must also be inside the pasture ($r_v = 1$)."

Flood Constraint (for every ordered adjacent pair (u, v) ∈ E) $$ r_u - r_v - f_v \le 0 \quad (\forall (u, v) \in E) \quad (\text{equivalently: } r_u \le r_v + f_v) $$

This single linear inequality $r_u \le r_v + f_v$ simultaneously enforces two essential properties:

  • Role A (Complete Wolf Separation):
    If any open path from a sheep ($r_s = 1$) reaches a wolf ($r_w = 0$) without encountering a fence ($f_v = 1$), chaining the inequality along the path yields $1 = r_s \le \dots \le r_w = 0$, which is infeasible. Thus every wolf-to-sheep path must contain at least one fence.
  • Role B (Preventing Cherry-Picking Around Pitfalls):
    Without this constraint, an optimizer could set $r_v = 1$ on positive tiles (🍓 +5) while arbitrarily setting $r_v = 0$ on an unfenced pitfall (🕳️ -5) right next to the sheep. Because of the flood constraint, any pitfall adjacent to the pasture forces a strict choice: spend a fence post to plug/block it ($f_v = 1, r_v = 0$) or include it in the pasture and accept the $-5$ penalty ($f_v = 0, r_v = 1$).

04MILP Formulation (Part 2): Disconnected Pockets and Single-Commodity Flow

Here we encounter a classic mathematical modeling pitfall: the flood constraint alone is not sufficient to define the pasture accurately.

❌ Loophole Under Flood Constraints Alone
🐺
🌱
💧
🍓
💧
🌱
💧
💧
💧
🌱
🐑
🌱
🍓

The top-right 🍓 is completely walled off from the sheep by a fence and water 💧, yet setting r_v = 1 there does not violate r_u <= r_v + f_v.

⭕ With Single-Commodity Flow Conservation
🐺
🌱
💧
🍓
💧
🌱
💧
💧
💧
🌱
🐑
🌱
🍓

Only cells that can receive network flow originating from the sheep 🐑 can set r_v = 1.

Why does this happen? Because r_u <= r_v + f_v forces every cell reachable from the sheep to have r_v = 1 (a lower bound), but it does not forbid setting r_v = 1 inside an isolated pocket walled off by fences and water.

Solution: Single-Commodity Network Flow

To guarantee that $r_v = 1$ holds only if cell $v$ is genuinely connected to at least one sheep in $S$, we add Single-Commodity Flow conservation constraints.

Over the directed arc set $A = \{(u, v), (v, u) \mid \{u, v\} \in E\}$, we define a non-negative continuous variable $\text{flow}_{u \to v} \ge 0$ representing the amount of flow sent from $u$ to $v$ (where $M = |V|$ is the total number of passable cells):

Single-Commodity Flow Constraints for Connectivity $$ \begin{aligned} &\text{(5) Outflow Bound:} && 0 \le \text{flow}_{u \to v} \le M \cdot r_u && (\forall (u, v) \in A) \\ &\text{(6) Inflow Bound:} && 0 \le \text{flow}_{u \to v} \le M \cdot r_v && (\forall (u, v) \in A) \\ &\text{(7) Net Consumption:} && \sum_{u:(u,v) \in A} \text{flow}_{u \to v} - \sum_{w:(v,w) \in A} \text{flow}_{v \to w} = r_v && (\forall v \in V \setminus S) \\ &\text{(8) Source Supply:} && \sum_{s \in S} \left( \sum_{w:(s,w) \in A} \text{flow}_{s \to w} - \sum_{u:(u,s) \in A} \text{flow}_{u \to s} \right) = \sum_{v \in V \setminus S} r_v \end{aligned} $$
🔍 Why Single-Commodity Flow Guarantees Connectivity
By equation (7), any non-sheep cell with $r_v = 1$ must consume 1 net unit of flow. By (5) and (6), flow can only travel through cells where $r = 1$, and the only nodes exempt from flow conservation (able to act as flow sources) are the sheep cells $S$. Consequently, any pocket disconnected from the sheep receives zero flow and is forced to $r_v = 0$.
💡 Big-M Tightness and Cutting Plane Methods
Big-M Tightness: While $M = |V|$ is valid in (5) and (6), bounding $M$ by the maximum reachable area under fence budget $K$ yields a tighter LP relaxation.
Algorithmic Comparison: Single-Commodity Flow (SCF) introduces continuous variables for all directed arcs ($\approx 4|V|$), solving within milliseconds on $10 \times 10$ boards with WASM HiGHS. For large graphs ($|V| > 10^5$), the standard approach shifts to Cutting Plane / Benders decomposition, dynamically generating connectivity cuts via separation callbacks during branch-and-bound.

05Objective Function and 1-Move Hint Diagnosis (Partial Fixing)

Finally, we combine "maximizing total pasture score (primary objective)" and "using as few fences as possible among tied scores (secondary tie-breaker)" into a single linear objective using a lexicographic weight of $100$:

Objective Function $$ \max \quad \sum_{v \in V} (100 \cdot w_v) \cdot r_v - \sum_{v \in V_{\text{fence}}} 1 \cdot f_v $$

The in-game "💡 1-Move Hint" button does not merely look up a static answer key. Instead, it fixes the player's currently placed fences $F_{\text{cur}}$ ($f_v = 1$ for all $v \in F_{\text{cur}}$) and re-solves the MILP in real time:

  • If the restricted problem is still Feasible and achieves the optimal target score $Z^*$:
    The player's current fences are on a valid optimal path, so the game highlights one remaining unplaced fence $f_v^* = 1 \; (v \notin F_{\text{cur}})$ in yellow.
  • If the restricted problem is Infeasible or its optimal objective falls short of $Z^*$:
    At least one placed fence is blocking the optimal enclosure, so the game identifies that fence and highlights it in red for removal.

Model Construction Excerpt

// 1. Flood constraint (if u is in the pasture and v has no fence, v must also be in the pasture)
if (vFence) {
  model.constraint(expr`${uReach} - ${vReach} - ${vFence} <= 0`);
} else {
  model.constraint(expr`${uReach} - ${vReach} <= 0`);
}

// 2. Single-commodity flow (flow is active only when both u and v are in the pasture)
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. Flow conservation at every non-sheep cell (inflow - outflow == r_v)
model.constraint(expr`sum(${inList}) - sum(${outList}) - ${rVar} == 0`);