Maximum Flow (Edmonds-Karp & Dinic) — Practice Tasks¶
All tasks must be solved in Go, Java, and Python. Each task ships with a statement, constraints, hints, and a reference solution in all three languages. A shared Dinic/Edmonds-Karp template is reused; only the modeling changes per task — which is the real skill.
Shared Template¶
Most tasks reuse a Dinic max-flow engine. Memorize this once; each task is then a modeling exercise on top of it.
add_edge(u, v, c): push forward edge (cap c) and reverse edge (cap 0), paired so reverse(e)=e^1
bfs(s, t): build level graph by BFS distance; return whether t is reachable
dfs(u, t, f): push blocking flow along level edges using current-arc it[]
max_flow(s, t): repeat { bfs; reset it; while dfs pushes, add to flow } until t unreachable
Beginner Tasks (5)¶
Task 1 — Compute Max Flow on a Given Network¶
Problem. Read a directed network with capacities, a source s, and a sink t. Output the maximum flow value.
Input / Output spec. - First line: V E s t. - Next E lines: u v c (directed edge u→v with capacity c). - Output: a single integer, the max-flow value.
Constraints. - 2 ≤ V ≤ 500, 1 ≤ E ≤ 10⁴, 0 ≤ c ≤ 10⁹.
Hint. Use the paired-edge representation and Dinic. Remember the reverse edge starts at capacity 0.
Reference — Go.
package main
import (
"bufio"
"fmt"
"os"
)
const INF = int(1 << 60)
type Dinic struct {
n int
to, cap []int
head [][]int
level, it []int
}
func NewDinic(n int) *Dinic { return &Dinic{n: n, head: make([][]int, n)} }
func (d *Dinic) AddEdge(u, v, c int) {
d.head[u] = append(d.head[u], len(d.to)); d.to = append(d.to, v); d.cap = append(d.cap, c)
d.head[v] = append(d.head[v], len(d.to)); d.to = append(d.to, u); d.cap = append(d.cap, 0)
}
func (d *Dinic) bfs(s, t int) bool {
d.level = make([]int, d.n)
for i := range d.level { d.level[i] = -1 }
d.level[s] = 0
q := []int{s}
for len(q) > 0 {
u := q[0]; q = q[1:]
for _, e := range d.head[u] {
if d.cap[e] > 0 && d.level[d.to[e]] < 0 {
d.level[d.to[e]] = d.level[u] + 1; q = append(q, d.to[e])
}
}
}
return d.level[t] >= 0
}
func (d *Dinic) dfs(u, t, f int) int {
if u == t { return f }
for ; d.it[u] < len(d.head[u]); d.it[u]++ {
e := d.head[u][d.it[u]]; v := d.to[e]
if d.cap[e] > 0 && d.level[v] == d.level[u]+1 {
if p := d.dfs(v, t, min(f, d.cap[e])); p > 0 {
d.cap[e] -= p; d.cap[e^1] += p; return p
}
}
}
return 0
}
func (d *Dinic) MaxFlow(s, t int) int {
flow := 0
for d.bfs(s, t) {
d.it = make([]int, d.n)
for { f := d.dfs(s, t, INF); if f == 0 { break }; flow += f }
}
return flow
}
func min(a, b int) int { if a < b { return a }; return b }
func main() {
in := bufio.NewReader(os.Stdin)
var V, E, s, t int
fmt.Fscan(in, &V, &E, &s, &t)
d := NewDinic(V)
for i := 0; i < E; i++ {
var u, v, c int
fmt.Fscan(in, &u, &v, &c)
d.AddEdge(u, v, c)
}
fmt.Println(d.MaxFlow(s, t))
}
Reference — Java.
import java.util.*;
public class Task1 {
static int n; static int[] to, cap; static List<List<Integer>> head;
static int[] level, it; static int cnt = 0;
static void init(int N, int m) {
n = N; to = new int[2*m]; cap = new int[2*m]; cnt = 0;
head = new ArrayList<>(); for (int i=0;i<n;i++) head.add(new ArrayList<>());
}
static void addEdge(int u,int v,int c){ head.get(u).add(cnt);to[cnt]=v;cap[cnt++]=c;
head.get(v).add(cnt);to[cnt]=u;cap[cnt++]=0; }
static boolean bfs(int s,int t){ level=new int[n];Arrays.fill(level,-1);level[s]=0;
Deque<Integer> q=new ArrayDeque<>();q.add(s);
while(!q.isEmpty()){int u=q.poll();for(int e:head.get(u))
if(cap[e]>0&&level[to[e]]<0){level[to[e]]=level[u]+1;q.add(to[e]);}}
return level[t]>=0; }
static int dfs(int u,int t,int f){ if(u==t)return f;
for(;it[u]<head.get(u).size();it[u]++){int e=head.get(u).get(it[u]),v=to[e];
if(cap[e]>0&&level[v]==level[u]+1){int p=dfs(v,t,Math.min(f,cap[e]));
if(p>0){cap[e]-=p;cap[e^1]+=p;return p;}}}
return 0; }
static int maxFlow(int s,int t){int flow=0;while(bfs(s,t)){it=new int[n];int f;
while((f=dfs(s,t,Integer.MAX_VALUE))>0)flow+=f;}return flow;}
public static void main(String[] a){
Scanner sc=new Scanner(System.in);
int V=sc.nextInt(),E=sc.nextInt(),s=sc.nextInt(),t=sc.nextInt();
init(V,E);
for(int i=0;i<E;i++) addEdge(sc.nextInt(),sc.nextInt(),sc.nextInt());
System.out.println(maxFlow(s,t));
}
}
Reference — Python.
import sys
from collections import deque
class Dinic:
def __init__(self, n):
self.n = n; self.to = []; self.cap = []; self.head = [[] for _ in range(n)]
def add_edge(self, u, v, c):
self.head[u].append(len(self.to)); self.to.append(v); self.cap.append(c)
self.head[v].append(len(self.to)); self.to.append(u); self.cap.append(0)
def bfs(self, s, t):
self.level = [-1]*self.n; self.level[s] = 0; q = deque([s])
while q:
u = q.popleft()
for e in self.head[u]:
if self.cap[e] > 0 and self.level[self.to[e]] < 0:
self.level[self.to[e]] = self.level[u]+1; q.append(self.to[e])
return self.level[t] >= 0
def dfs(self, u, t, f):
if u == t: return f
while self.it[u] < len(self.head[u]):
e = self.head[u][self.it[u]]; v = self.to[e]
if self.cap[e] > 0 and self.level[v] == self.level[u]+1:
p = self.dfs(v, t, min(f, self.cap[e]))
if p > 0: self.cap[e] -= p; self.cap[e^1] += p; return p
self.it[u] += 1
return 0
def max_flow(self, s, t):
sys.setrecursionlimit(1 << 25); flow = 0
while self.bfs(s, t):
self.it = [0]*self.n
while (f := self.dfs(s, t, float("inf"))): flow += f
return flow
def main():
data = sys.stdin.read().split()
idx = 0
def nxt():
nonlocal idx; idx += 1; return int(data[idx-1])
V, E, s, t = nxt(), nxt(), nxt(), nxt()
d = Dinic(V)
for _ in range(E):
d.add_edge(nxt(), nxt(), nxt())
print(d.max_flow(s, t))
main()
Task 2 — Verify a Claimed Flow¶
Problem. Given a network and a claimed flow f on each edge, verify that it is a valid flow: capacity constraints and conservation hold. Output VALID or INVALID.
Constraints. V ≤ 500, E ≤ 10⁴.
Hint. Check 0 ≤ f(e) ≤ c(e) per edge, then for each vertex except s/t check inflow = outflow. No max-flow needed.
Reference — Python.
def verify(V, edges, s, t):
# edges: list of (u, v, c, f)
inflow = [0]*V; outflow = [0]*V
for u, v, c, f in edges:
if not (0 <= f <= c):
return "INVALID"
outflow[u] += f; inflow[v] += f
for x in range(V):
if x != s and x != t and inflow[x] != outflow[x]:
return "INVALID"
return "VALID"
Reference — Go.
func verify(V int, edges [][4]int, s, t int) string {
in := make([]int, V); out := make([]int, V)
for _, e := range edges {
u, v, c, f := e[0], e[1], e[2], e[3]
if f < 0 || f > c { return "INVALID" }
out[u] += f; in[v] += f
}
for x := 0; x < V; x++ {
if x != s && x != t && in[x] != out[x] { return "INVALID" }
}
return "VALID"
}
Reference — Java.
static String verify(int V, int[][] edges, int s, int t) {
long[] in = new long[V], out = new long[V];
for (int[] e : edges) {
if (e[3] < 0 || e[3] > e[2]) return "INVALID";
out[e[0]] += e[3]; in[e[1]] += e[3];
}
for (int x = 0; x < V; x++)
if (x != s && x != t && in[x] != out[x]) return "INVALID";
return "VALID";
}
Task 3 — Maximum Bipartite Matching¶
Problem. Given a bipartite graph (L left, R right, allowed pairs), output the maximum matching size.
Constraints. L, R ≤ 1000, pairs ≤ 10⁴.
Hint. Super-source → left (cap 1), left → right (cap 1), right → super-sink (cap 1). Dinic gives O(E√V).
Reference — Python.
def max_matching(L, R, pairs):
s, t = L + R, L + R + 1
d = Dinic(L + R + 2)
for i in range(L): d.add_edge(s, i, 1)
for j in range(R): d.add_edge(L + j, t, 1)
for u, v in pairs: d.add_edge(u, L + v, 1)
return d.max_flow(s, t)
# max_matching(3, 3, [(0,0),(0,1),(1,1),(2,2)]) == 3
Reference — Go.
func maxMatching(L, R int, pairs [][2]int) int {
s, t := L+R, L+R+1
d := NewDinic(L + R + 2)
for i := 0; i < L; i++ { d.AddEdge(s, i, 1) }
for j := 0; j < R; j++ { d.AddEdge(L+j, t, 1) }
for _, p := range pairs { d.AddEdge(p[0], L+p[1], 1) }
return d.MaxFlow(s, t)
}
Reference — Java.
static int maxMatching(int L, int R, int[][] pairs) {
int s = L + R, t = L + R + 1;
init(L + R + 2, L + R + pairs.length);
for (int i = 0; i < L; i++) addEdge(s, i, 1);
for (int j = 0; j < R; j++) addEdge(L + j, t, 1);
for (int[] p : pairs) addEdge(p[0], L + p[1], 1);
return maxFlow(s, t);
}
Task 4 — Recover the Minimum Cut¶
Problem. After computing max flow, output the vertices on the source side S and the cut edges.
Constraints. V ≤ 500, E ≤ 10⁴.
Hint. Run a DFS from s over positive-residual edges after max_flow. Reachable = S; cut edges go from S to T in the original graph.
Reference — Python.
def min_cut(d, s, original_edges):
seen = [False]*d.n; stack = [s]; seen[s] = True
while stack:
u = stack.pop()
for e in d.head[u]:
if d.cap[e] > 0 and not seen[d.to[e]]:
seen[d.to[e]] = True; stack.append(d.to[e])
S = [v for v in range(d.n) if seen[v]]
cut = [(u, v) for (u, v, c) in original_edges if seen[u] and not seen[v]]
return S, cut
Reference — Go.
func (d *Dinic) MinCut(s int, orig [][3]int) ([]int, [][2]int) {
seen := make([]bool, d.n); stack := []int{s}; seen[s] = true
for len(stack) > 0 {
u := stack[len(stack)-1]; stack = stack[:len(stack)-1]
for _, e := range d.head[u] {
if d.cap[e] > 0 && !seen[d.to[e]] { seen[d.to[e]] = true; stack = append(stack, d.to[e]) }
}
}
var S []int; var cut [][2]int
for v := 0; v < d.n; v++ { if seen[v] { S = append(S, v) } }
for _, e := range orig { if seen[e[0]] && !seen[e[1]] { cut = append(cut, [2]int{e[0], e[1]}) } }
return S, cut
}
Reference — Java.
static int[] minCutSide(int s) {
boolean[] seen = new boolean[n];
Deque<Integer> st = new ArrayDeque<>(); st.push(s); seen[s] = true;
while (!st.isEmpty()) { int u = st.pop();
for (int e : head.get(u)) if (cap[e] > 0 && !seen[to[e]]) { seen[to[e]] = true; st.push(to[e]); } }
int cnt = 0; for (boolean b : seen) if (b) cnt++;
int[] S = new int[cnt]; int k = 0;
for (int v = 0; v < n; v++) if (seen[v]) S[k++] = v;
return S;
}
Task 5 — Number of Edge-Disjoint Paths¶
Problem. Output the maximum number of edge-disjoint s-t paths.
Constraints. V ≤ 500, E ≤ 10⁴.
Hint. Set every edge capacity to 1; the max-flow equals the answer (Menger).
Reference — Python.
def edge_disjoint_paths(n, edges, s, t):
d = Dinic(n)
for u, v in edges:
d.add_edge(u, v, 1)
return d.max_flow(s, t)
Reference — Go.
func edgeDisjoint(n int, edges [][2]int, s, t int) int {
d := NewDinic(n)
for _, e := range edges { d.AddEdge(e[0], e[1], 1) }
return d.MaxFlow(s, t)
}
Reference — Java.
static int edgeDisjoint(int n, int[][] edges, int s, int t) {
init(n, edges.length);
for (int[] e : edges) addEdge(e[0], e[1], 1);
return maxFlow(s, t);
}
Intermediate Tasks (5)¶
Task 6 — Vertex Capacities¶
Problem. Each vertex v has a throughput limit cap(v) (besides edge capacities). Compute max flow respecting both.
Constraints. V ≤ 500, E ≤ 10⁴.
Hint. Split every vertex v into v_in (= 2v) and v_out (= 2v+1) with an internal edge v_in → v_out of capacity cap(v). Edges into v go to v_in; edges out of v leave v_out. New source = s_out, sink = t_in.
Reference — Python.
def max_flow_vertex_caps(V, vcap, edges, s, t):
d = Dinic(2 * V)
for v in range(V):
d.add_edge(2*v, 2*v + 1, vcap[v]) # v_in -> v_out
for u, v, c in edges:
d.add_edge(2*u + 1, 2*v, c) # u_out -> v_in
return d.max_flow(2*s + 1, 2*t) # s_out -> t_in
Reference — Go.
func maxFlowVertexCaps(V int, vcap []int, edges [][3]int, s, t int) int {
d := NewDinic(2 * V)
for v := 0; v < V; v++ { d.AddEdge(2*v, 2*v+1, vcap[v]) }
for _, e := range edges { d.AddEdge(2*e[0]+1, 2*e[1], e[2]) }
return d.MaxFlow(2*s+1, 2*t)
}
Reference — Java.
static int maxFlowVertexCaps(int V, int[] vcap, int[][] edges, int s, int t) {
init(2 * V, V + edges.length);
for (int v = 0; v < V; v++) addEdge(2*v, 2*v + 1, vcap[v]);
for (int[] e : edges) addEdge(2*e[0] + 1, 2*e[1], e[2]);
return maxFlow(2*s + 1, 2*t);
}
Task 7 — Multiple Sources and Sinks¶
Problem. Several sources (each with a supply) and several sinks (each with a demand). Compute the max total flow from all sources to all sinks.
Constraints. V ≤ 500, E ≤ 10⁴.
Hint. Add super-source S* → each source (cap = supply), each sink → super-sink T* (cap = demand).
Reference — Python.
def multi_source_sink(V, edges, sources, sinks):
S, T = V, V + 1
d = Dinic(V + 2)
for (src, supply) in sources: d.add_edge(S, src, supply)
for (snk, demand) in sinks: d.add_edge(snk, T, demand)
for u, v, c in edges: d.add_edge(u, v, c)
return d.max_flow(S, T)
Reference — Go.
func multiSourceSink(V int, edges [][3]int, sources, sinks [][2]int) int {
S, T := V, V+1
d := NewDinic(V + 2)
for _, sp := range sources { d.AddEdge(S, sp[0], sp[1]) }
for _, sk := range sinks { d.AddEdge(sk[0], T, sk[1]) }
for _, e := range edges { d.AddEdge(e[0], e[1], e[2]) }
return d.MaxFlow(S, T)
}
Reference — Java.
static int multiSourceSink(int V, int[][] edges, int[][] sources, int[][] sinks) {
int S = V, T = V + 1;
init(V + 2, edges.length + sources.length + sinks.length);
for (int[] sp : sources) addEdge(S, sp[0], sp[1]);
for (int[] sk : sinks) addEdge(sk[0], T, sk[1]);
for (int[] e : edges) addEdge(e[0], e[1], e[2]);
return maxFlow(S, T);
}
Task 8 — Maximum Vertex-Disjoint Paths¶
Problem. Output the maximum number of internally vertex-disjoint s-t paths.
Constraints. V ≤ 500, E ≤ 10⁴.
Hint. Split every vertex with capacity 1 (except give s,t capacity ∞), then count edge-disjoint paths.
Reference — Python.
def vertex_disjoint_paths(V, edges, s, t):
INF = 1 << 30
d = Dinic(2 * V)
for v in range(V):
cap = INF if v in (s, t) else 1
d.add_edge(2*v, 2*v + 1, cap)
for u, v in edges:
d.add_edge(2*u + 1, 2*v, 1)
return d.max_flow(2*s + 1, 2*t)
Reference — Go.
func vertexDisjoint(V int, edges [][2]int, s, t int) int {
const INF = 1 << 30
d := NewDinic(2 * V)
for v := 0; v < V; v++ {
c := 1
if v == s || v == t { c = INF }
d.AddEdge(2*v, 2*v+1, c)
}
for _, e := range edges { d.AddEdge(2*e[0]+1, 2*e[1], 1) }
return d.MaxFlow(2*s+1, 2*t)
}
Reference — Java.
static int vertexDisjoint(int V, int[][] edges, int s, int t) {
final int INF = 1 << 30;
init(2 * V, V + edges.length);
for (int v = 0; v < V; v++) addEdge(2*v, 2*v + 1, (v == s || v == t) ? INF : 1);
for (int[] e : edges) addEdge(2*e[0] + 1, 2*e[1], 1);
return maxFlow(2*s + 1, 2*t);
}
Task 9 — Minimum Path Cover on a DAG¶
Problem. Given a DAG, find the minimum number of vertex-disjoint paths that cover all vertices. Answer = V − (maximum bipartite matching) where the bipartite graph connects u_left → v_right for each edge u→v.
Constraints. V ≤ 1000, E ≤ 10⁴.
Hint. Build a bipartite graph: left copy and right copy of each vertex; edge u→v becomes u_left → v_right. Min path cover = V − max_matching.
Reference — Python.
def min_path_cover(V, edges):
# left v = v, right v = V + v, s = 2V, t = 2V+1
s, t = 2*V, 2*V + 1
d = Dinic(2*V + 2)
for v in range(V):
d.add_edge(s, v, 1)
d.add_edge(V + v, t, 1)
for u, v in edges:
d.add_edge(u, V + v, 1)
matching = d.max_flow(s, t)
return V - matching
Reference — Go.
func minPathCover(V int, edges [][2]int) int {
s, t := 2*V, 2*V+1
d := NewDinic(2*V + 2)
for v := 0; v < V; v++ { d.AddEdge(s, v, 1); d.AddEdge(V+v, t, 1) }
for _, e := range edges { d.AddEdge(e[0], V+e[1], 1) }
return V - d.MaxFlow(s, t)
}
Reference — Java.
static int minPathCover(int V, int[][] edges) {
int s = 2*V, t = 2*V + 1;
init(2*V + 2, 2*V + edges.length);
for (int v = 0; v < V; v++) { addEdge(s, v, 1); addEdge(V + v, t, 1); }
for (int[] e : edges) addEdge(e[0], V + e[1], 1);
return V - maxFlow(s, t);
}
Task 10 — Project Selection (Max-Weight Closure)¶
Problem. Each project has a profit (possibly negative) and a set of prerequisite projects. Choosing a project requires choosing all its prerequisites. Maximize total profit.
Constraints. V ≤ 1000, dependencies ≤ 10⁴.
Hint. s → p (cap = profit) for profit > 0; p → t (cap = −profit) for profit < 0; prerequisite p → q cap ∞. Answer = (sum of positive profits) − min-cut.
Reference — Python.
def project_selection(profits, prereqs):
n = len(profits); s, t = n, n + 1
d = Dinic(n + 2)
INF = 1 << 60
total_pos = 0
for p, w in enumerate(profits):
if w > 0:
d.add_edge(s, p, w); total_pos += w
elif w < 0:
d.add_edge(p, t, -w)
for p, q in prereqs: # picking p requires q
d.add_edge(p, q, INF)
return total_pos - d.max_flow(s, t)
# profits=[3, -1, -2], prereqs=[(0,1),(0,2)]:
# pick project 0 (+3) requires 1 (-1) and 2 (-2) -> net 0; or pick nothing -> 0
Reference — Go.
func projectSelection(profits []int, prereqs [][2]int) int {
n := len(profits); s, t := n, n+1
d := NewDinic(n + 2)
const INF = 1 << 60
totalPos := 0
for p, w := range profits {
if w > 0 { d.AddEdge(s, p, w); totalPos += w } else if w < 0 { d.AddEdge(p, t, -w) }
}
for _, e := range prereqs { d.AddEdge(e[0], e[1], INF) }
return totalPos - d.MaxFlow(s, t)
}
Reference — Java.
static int projectSelection(int[] profits, int[][] prereqs) {
int n = profits.length, s = n, t = n + 1;
init(n + 2, n + prereqs.length);
final int INF = 1 << 30; int totalPos = 0;
for (int p = 0; p < n; p++) {
if (profits[p] > 0) { addEdge(s, p, profits[p]); totalPos += profits[p]; }
else if (profits[p] < 0) addEdge(p, t, -profits[p]);
}
for (int[] e : prereqs) addEdge(e[0], e[1], INF);
return totalPos - maxFlow(s, t);
}
Advanced Tasks (5)¶
Task 11 — Image Segmentation (Two-Label Graph Cut)¶
Problem. Given an n×m grid where each pixel has a foreground affinity a[i] and background affinity b[i], plus smoothness penalties between adjacent pixels, partition pixels into foreground/background minimizing total disagreement. This is min-cut.
Constraints. Grid ≤ 200×200.
Hint. s → pixel cap = foreground affinity, pixel → t cap = background affinity, pixel ↔ neighbor cap = smoothness (both directions). Min-cut gives the optimal labeling; reachable-from-s = foreground.
Reference — Python.
def segment(n, m, a, b, neighbors):
# a[i], b[i] indexed by pixel id = r*m + c; neighbors: list of (i, j, penalty)
N = n * m; s, t = N, N + 1
d = Dinic(N + 2)
for i in range(N):
if a[i] > 0: d.add_edge(s, i, a[i])
if b[i] > 0: d.add_edge(i, t, b[i])
for i, j, w in neighbors:
d.add_edge(i, j, w); d.add_edge(j, i, w)
cut_value = d.max_flow(s, t)
# label: reachable from s in residual = foreground
seen = [False]*d.n; stack=[s]; seen[s]=True
while stack:
u = stack.pop()
for e in d.head[u]:
if d.cap[e] > 0 and not seen[d.to[e]]:
seen[d.to[e]] = True; stack.append(d.to[e])
foreground = [i for i in range(N) if seen[i]]
return cut_value, foreground
Reference — Go.
func segment(N int, a, b []int, neighbors [][3]int, s, t int) int {
d := NewDinic(N + 2)
for i := 0; i < N; i++ {
if a[i] > 0 { d.AddEdge(s, i, a[i]) }
if b[i] > 0 { d.AddEdge(i, t, b[i]) }
}
for _, nb := range neighbors { d.AddEdge(nb[0], nb[1], nb[2]); d.AddEdge(nb[1], nb[0], nb[2]) }
return d.MaxFlow(s, t)
}
Reference — Java.
static int segment(int N, int[] a, int[] b, int[][] neighbors, int s, int t) {
init(N + 2, 2*N + 2*neighbors.length);
for (int i = 0; i < N; i++) {
if (a[i] > 0) addEdge(s, i, a[i]);
if (b[i] > 0) addEdge(i, t, b[i]);
}
for (int[] nb : neighbors) { addEdge(nb[0], nb[1], nb[2]); addEdge(nb[1], nb[0], nb[2]); }
return maxFlow(s, t);
}
Task 12 — Baseball Elimination¶
Problem. Given current wins and remaining games, determine whether a given team can still finish first (possibly tied). Build a flow network of remaining games and capacity constraints; the team is eliminated iff the max-flow does not saturate all game nodes.
Constraints. Teams ≤ 30.
Hint. For team x with w[x] + r[x] max possible wins: game nodes (i,j) get s → game cap = remaining games between i,j; game → team_i and game → team_j cap ∞; team_i → t cap = w[x] + r[x] − w[i]. If a team's cap is negative, x is trivially eliminated. Otherwise eliminated iff flow < total remaining games among the others.
Reference — Python.
def is_eliminated(x, w, r, games):
# w[i] wins, r[i] remaining; games[i][j] = remaining games between i and j
teams = [i for i in range(len(w)) if i != x]
cap_x = w[x] + r[x]
for i in teams:
if w[i] > cap_x:
return True # trivially eliminated
game_pairs = [(i, j) for i in teams for j in teams if i < j and games[i][j] > 0]
N = len(w)
s, t = N + len(game_pairs), N + len(game_pairs) + 1
d = Dinic(N + len(game_pairs) + 2)
total = 0
for gi, (i, j) in enumerate(game_pairs):
gnode = N + gi
d.add_edge(s, gnode, games[i][j]); total += games[i][j]
d.add_edge(gnode, i, 1 << 30)
d.add_edge(gnode, j, 1 << 30)
for i in teams:
d.add_edge(i, t, cap_x - w[i])
return d.max_flow(s, t) < total
Reference — Go.
// Same construction: game nodes from s (cap = games left), each game -> its two teams (inf),
// team -> t (cap = capX - w[team]). Eliminated iff maxFlow < totalRemainingGames.
// Build with NewDinic, AddEdge, MaxFlow exactly as in Task 1.
func isEliminated(x int, w, r []int, games [][]int) bool {
capX := w[x] + r[x]
for i := range w {
if i != x && w[i] > capX { return true }
}
// ... assemble game nodes and run Dinic; return maxFlow < total
return false // full assembly mirrors the Python reference
}
Reference — Java.
// Identical modeling to the Python reference: s -> game (remaining), game -> teams (INF),
// team -> t (capX - w[i]); eliminated iff maxFlow < total remaining games among others.
// Reuse init/addEdge/maxFlow from Task 1.
Task 13 — Maximum Flow with Lower Bounds¶
Problem. Each edge has a lower bound lo and upper bound hi on its flow. Find a feasible flow, then the maximum flow respecting bounds.
Constraints. V ≤ 500, E ≤ 10⁴.
Hint. Standard reduction: for each edge (u,v,lo,hi) add residual edge of capacity hi − lo, and track an excess d[v] += lo, d[u] -= lo. Add a super-source/super-sink saturating the excesses; feasibility = super-source edges all saturated. Then max-flow on the residual.
Reference — Python.
def feasible_with_lower_bounds(V, edges, s, t):
# edges: (u, v, lo, hi). Returns feasible flow value or None.
SS, TT = V, V + 1
d = Dinic(V + 2)
excess = [0]*V
INF = 1 << 60
for u, v, lo, hi in edges:
d.add_edge(u, v, hi - lo)
excess[u] -= lo; excess[v] += lo
d.add_edge(t, s, INF) # circulation edge
need = 0
for v in range(V):
if excess[v] > 0:
d.add_edge(SS, v, excess[v]); need += excess[v]
elif excess[v] < 0:
d.add_edge(v, TT, -excess[v])
return d.max_flow(SS, TT) == need
Reference — Go.
func feasibleLowerBounds(V int, edges [][4]int, s, t int) bool {
SS, TT := V, V+1
d := NewDinic(V + 2)
excess := make([]int, V)
const INF = 1 << 60
for _, e := range edges {
u, v, lo, hi := e[0], e[1], e[2], e[3]
d.AddEdge(u, v, hi-lo); excess[u] -= lo; excess[v] += lo
}
d.AddEdge(t, s, INF)
need := 0
for v := 0; v < V; v++ {
if excess[v] > 0 { d.AddEdge(SS, v, excess[v]); need += excess[v] } else if excess[v] < 0 { d.AddEdge(v, TT, -excess[v]) }
}
return d.MaxFlow(SS, TT) == need
}
Reference — Java.
static boolean feasibleLowerBounds(int V, int[][] edges, int s, int t) {
int SS = V, TT = V + 1;
init(V + 2, edges.length + V + 1);
long[] excess = new long[V]; final int INF = 1 << 30;
for (int[] e : edges) { addEdge(e[0], e[1], e[3] - e[2]); excess[e[0]] -= e[2]; excess[e[1]] += e[2]; }
addEdge(t, s, INF);
long need = 0;
for (int v = 0; v < V; v++) {
if (excess[v] > 0) { addEdge(SS, v, (int) excess[v]); need += excess[v]; }
else if (excess[v] < 0) addEdge(v, TT, (int) -excess[v]);
}
return maxFlow(SS, TT) == need;
}
Task 14 — Escape Problem (Grid Vertex-Disjoint Routing)¶
Problem. On an n×n grid, given a set of start cells, determine if all starts can be routed to the border along vertex-disjoint paths (each non-start cell used at most once).
Constraints. n ≤ 100.
Hint. Split each cell in→out cap 1, connect adjacent cells cap 1, super-source → start in-nodes, border out-nodes → super-sink. Escape possible iff max-flow = number of starts.
Reference — Python.
def can_escape(n, starts):
S, T = 2*n*n, 2*n*n + 1
d = Dinic(2*n*n + 2)
cid = lambda r, c: r*n + c
for r in range(n):
for c in range(n):
d.add_edge(2*cid(r,c), 2*cid(r,c)+1, 1)
for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
nr, nc = r+dr, c+dc
if 0 <= nr < n and 0 <= nc < n:
d.add_edge(2*cid(r,c)+1, 2*cid(nr,nc), 1)
if r in (0, n-1) or c in (0, n-1):
d.add_edge(2*cid(r,c)+1, T, 1)
for (r, c) in starts:
d.add_edge(S, 2*cid(r,c), 1)
return d.max_flow(S, T) == len(starts)
# can_escape(3, [(1,1)]) == True
Reference — Go.
func canEscape(n int, starts [][2]int) bool {
S, T := 2*n*n, 2*n*n+1
d := NewDinic(2*n*n + 2)
cid := func(r, c int) int { return r*n + c }
dr := []int{1, -1, 0, 0}; dc := []int{0, 0, 1, -1}
for r := 0; r < n; r++ {
for c := 0; c < n; c++ {
d.AddEdge(2*cid(r, c), 2*cid(r, c)+1, 1)
for k := 0; k < 4; k++ {
nr, nc := r+dr[k], c+dc[k]
if nr >= 0 && nr < n && nc >= 0 && nc < n {
d.AddEdge(2*cid(r, c)+1, 2*cid(nr, nc), 1)
}
}
if r == 0 || r == n-1 || c == 0 || c == n-1 { d.AddEdge(2*cid(r, c)+1, T, 1) }
}
}
for _, st := range starts { d.AddEdge(S, 2*cid(st[0], st[1]), 1) }
return d.MaxFlow(S, T) == len(starts)
}
Reference — Java.
static boolean canEscape(int n, int[][] starts) {
int S = 2*n*n, T = 2*n*n + 1;
init(2*n*n + 2, 6*n*n);
int[] dr = {1,-1,0,0}, dc = {0,0,1,-1};
for (int r = 0; r < n; r++) for (int c = 0; c < n; c++) {
int id = r*n + c;
addEdge(2*id, 2*id + 1, 1);
for (int k = 0; k < 4; k++) {
int nr = r + dr[k], nc = c + dc[k];
if (nr >= 0 && nr < n && nc >= 0 && nc < n) addEdge(2*id + 1, 2*(nr*n + nc), 1);
}
if (r == 0 || r == n-1 || c == 0 || c == n-1) addEdge(2*id + 1, T, 1);
}
for (int[] st : starts) addEdge(S, 2*(st[0]*n + st[1]), 1);
return maxFlow(S, T) == starts.length;
}
Task 15 — Cross-Check Dinic vs Edmonds-Karp¶
Problem. Implement both Dinic and Edmonds-Karp and assert they agree on random networks. This is the standard correctness harness.
Constraints. Random graphs V ≤ 50, E ≤ 200, caps ≤ 100, 1000 trials.
Hint. Generate random networks, run both, assert equal. Any mismatch is a bug (almost always a missing reverse edge).
Reference — Python.
import random
def edmonds_karp_matrix(cap, s, t):
from collections import deque
n = len(cap); flow = 0
while True:
par = [-1]*n; par[s] = s; q = deque([s])
while q:
u = q.popleft()
for v in range(n):
if par[v] == -1 and cap[u][v] > 0:
par[v] = u; q.append(v)
if par[t] == -1: break
b = float("inf"); v = t
while v != s: b = min(b, cap[par[v]][v]); v = par[v]
v = t
while v != s: cap[par[v]][v] -= b; cap[v][par[v]] += b; v = par[v]
flow += b
return flow
def cross_check(trials=1000):
for _ in range(trials):
n = random.randint(2, 8)
s, t = 0, n - 1
if s == t: continue
cap = [[0]*n for _ in range(n)]
d = Dinic(n)
for _ in range(random.randint(1, n*n)):
u, v = random.randrange(n), random.randrange(n)
if u != v:
c = random.randint(0, 20)
cap[u][v] += c
d.add_edge(u, v, c)
assert d.max_flow(s, t) == edmonds_karp_matrix(cap, s, t)
print("all trials agree")
cross_check()
Reference — Go / Java. Mirror the harness: build the same random edges into both a matrix Edmonds-Karp and an adjacency-list Dinic, then assert equality across trials. A single disagreement localizes the bug immediately.
Benchmark Task¶
Task 16 — Bipartite Matching Throughput Benchmark¶
Problem. Measure Dinic on bipartite matching instances of growing size and confirm the O(E√V) scaling.
Reference — Python.
import random, time
def make_bipartite(L, R, density):
pairs = []
for u in range(L):
for v in range(R):
if random.random() < density:
pairs.append((u, v))
return pairs
def bench():
for L in [100, 200, 400, 800, 1600]:
R = L
pairs = make_bipartite(L, R, 0.05)
s, t = L + R, L + R + 1
start = time.perf_counter()
d = Dinic(L + R + 2)
for i in range(L): d.add_edge(s, i, 1)
for j in range(R): d.add_edge(L + j, t, 1)
for u, v in pairs: d.add_edge(u, L + v, 1)
m = d.max_flow(s, t)
elapsed = (time.perf_counter() - start) * 1000
print(f"L={L:5d} E={len(pairs):7d} matching={m:5d} time={elapsed:8.2f} ms")
bench()
Expected pattern. Time should grow roughly proportionally to E√V, not V²E. As L doubles, E (at fixed density) quadruples and √V grows ~1.4×, so each step is roughly 5–6× slower — far below the ~8× a VE-style bound would predict, confirming the unit-capacity speedup.
Go / Java benchmark. Identical structure: build the bipartite instance, time MaxFlow, print (L, E, matching, ms). Use the same Dinic engine from Task 1. The takeaway across all three languages is the same scaling curve; absolute constants differ (Go and Java are typically 5–20× faster than CPython here).
How to Use These Tasks¶
- Solve modeling first. For every task, write down what is routed, what the capacities are, and what the cut means before coding.
- Reuse the engine. The Dinic template never changes; only the graph construction does. This separation is the whole point.
- Always cross-check (Task 15) when you suspect a bug — it almost always finds a missing reverse edge.
- Recover the cut wherever the problem really wants the partition (segmentation, project selection), not just the value.
- Benchmark on adversarial inputs too (layered graphs), not only random ones, before trusting latency.