Skip to content

Commit 8e0817e

Browse files
Clean up the networking_flow directory (#15098)
* Clean up the networking_flow directory - Add networking_flow/README.md covering max-flow / min-cut, with a file-by-file table and guidance on which algorithm to use. - minimum_cut.py: add a module docstring with a Wikipedia URL, type hints, and corner-case doctests; work on a copy so the input graph is no longer mutated. - Add dinic.py: Dinic's algorithm (BFS level graph + DFS blocking flow), adjacency-list based so it handles parallel edges and sparse graphs. - Add push_relabel.py: the Goldberg-Tarjan push-relabel (preflow) method with highest-label selection. Both new algorithms are fully type-hinted, documented with a Wikipedia reference, and validated by doctests; their output was cross-checked against ford_fulkerson.py on thousands of random graphs. * Address review: drop __future__ import, use descriptive names, apply README wording
1 parent 70b9e2f commit 8e0817e

4 files changed

Lines changed: 434 additions & 26 deletions

File tree

networking_flow/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Networking Flow
2+
3+
This directory collects algorithms for the **maximum-flow problem**: given a
4+
directed graph whose edges have capacities, a `source`, and a `sink`, how
5+
much flow can be pushed from `source` to `sink` without exceeding any edge's
6+
capacity?
7+
8+
Maximum flow turns up all over the place — routing traffic through a network,
9+
matching people to jobs, scheduling, image segmentation, and any problem that can
10+
be phrased as "move as much as possible from here to there through a shared
11+
network." Its close relative, the **minimum cut**, finds the cheapest set of
12+
edges whose removal disconnects the sink from the source, and the
13+
[max-flow min-cut theorem](https://en.wikipedia.org/wiki/Max-flow_min-cut_theorem)
14+
says the two always have the same value.
15+
16+
New to the topic? These are good starting points:
17+
18+
* <https://en.wikipedia.org/wiki/Maximum_flow_problem>
19+
* <https://en.wikipedia.org/wiki/Flow_network>
20+
* <https://en.wikipedia.org/wiki/Max-flow_min-cut_theorem>
21+
22+
## What's in this directory
23+
24+
| File | Description |
25+
| ---- | ----------- |
26+
| [`ford_fulkerson.py`](ford_fulkerson.py) | The [Ford-Fulkerson](https://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm) method, finding augmenting paths with a breadth-first search (the [Edmonds-Karp](https://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm) refinement). Uses an adjacency-matrix representation. Runs in `O(V * E^2)`. |
27+
| [`minimum_cut.py`](minimum_cut.py) | Finds the edges of a [minimum s-t cut](https://en.wikipedia.org/wiki/Minimum_cut) from the residual graph left behind by Ford-Fulkerson, illustrating the max-flow min-cut theorem. |
28+
| [`dinic.py`](dinic.py) | [Dinic's algorithm](https://en.wikipedia.org/wiki/Dinic%27s_algorithm): repeatedly build a BFS *level graph* and saturate a *blocking flow* on it. Adjacency-list based, so it handles parallel edges and sparse graphs well. Runs in `O(V^2 * E)`, or `O(E * sqrt(V))` on unit-capacity networks. |
29+
| [`push_relabel.py`](push_relabel.py) | The [push-relabel](https://en.wikipedia.org/wiki/Push%E2%80%93relabel_maximum_flow_algorithm) (Goldberg-Tarjan) method: instead of augmenting whole paths, it maintains a *preflow* and locally pushes excess towards the sink. With highest-label selection it runs in `O(V^2 * sqrt(E))`, and is a strong choice on dense graphs. |
30+
31+
## Which one should I use?
32+
33+
All four compute the same maximum-flow value; they differ in speed and in how
34+
the graph is represented.
35+
36+
* **Just learning the idea?** Start with `ford_fulkerson.py` and
37+
`minimum_cut.py` — the augmenting-path picture is the most intuitive.
38+
* **Sparse graph, or parallel edges?** Reach for `dinic.py`; the adjacency-list
39+
representation and level-graph batching make it fast in practice.
40+
* **Dense graph?** `push_relabel.py` tends to win, because it avoids
41+
re-scanning long augmenting paths.
42+
43+
Each file is self-contained, fully type-hinted, and verified with doctests — run
44+
any of them directly (for example `python networking_flow/dinic.py`) to execute
45+
the tests.

networking_flow/dinic.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""
2+
Dinic's algorithm for the maximum-flow problem.
3+
4+
Dinic's algorithm repeatedly builds a *level graph* with a breadth-first search
5+
(shortest augmenting paths, measured in edges) and then, in one pass, saturates
6+
a *blocking flow* on that level graph using depth-first search. Grouping the
7+
augmenting paths by length this way gives a much better worst case than the
8+
plain Ford-Fulkerson / Edmonds-Karp augmenting-path method:
9+
10+
* Dinic's algorithm: O(V^2 * E)
11+
* on unit-capacity networks: O(E * sqrt(V))
12+
13+
Unlike the adjacency-matrix implementations in ``ford_fulkerson.py`` and
14+
``minimum_cut.py`` in this directory, this version stores the graph as an
15+
adjacency list of residual edges, so it also handles graphs with parallel edges
16+
and is efficient on sparse graphs.
17+
18+
Reference: https://en.wikipedia.org/wiki/Dinic%27s_algorithm
19+
"""
20+
21+
from collections import deque
22+
23+
24+
class Dinic:
25+
"""
26+
Maximum flow in a directed graph with non-negative integer capacities.
27+
28+
Add edges with :meth:`add_edge`, then call :meth:`max_flow`.
29+
30+
>>> g = Dinic(6)
31+
>>> capacities = {
32+
... (0, 1): 16, (0, 2): 13, (1, 2): 10, (1, 3): 12,
33+
... (2, 1): 4, (2, 4): 14, (3, 2): 9, (3, 5): 20,
34+
... (4, 3): 7, (4, 5): 4,
35+
... }
36+
>>> for (u, v), cap in capacities.items():
37+
... g.add_edge(u, v, cap)
38+
>>> g.max_flow(0, 5)
39+
23
40+
41+
A source with no outgoing edges (or a sink with no incoming edges) has zero
42+
maximum flow:
43+
44+
>>> Dinic(3).max_flow(0, 2)
45+
0
46+
47+
Parallel edges between the same pair of vertices are supported and their
48+
capacities add up:
49+
50+
>>> h = Dinic(2)
51+
>>> h.add_edge(0, 1, 3)
52+
>>> h.add_edge(0, 1, 5)
53+
>>> h.max_flow(0, 1)
54+
8
55+
"""
56+
57+
def __init__(self, vertices: int) -> None:
58+
if vertices <= 0:
59+
raise ValueError("number of vertices must be positive")
60+
self.size = vertices
61+
# graph[vertex] holds indices into self.edges for edges leaving that vertex.
62+
self.graph: list[list[int]] = [[] for _ in range(vertices)]
63+
# Each edge is stored as [destination, residual_capacity].
64+
# Edge i and its reverse edge i ^ 1 are always created together.
65+
self.edges: list[list[int]] = []
66+
67+
def add_edge(self, source: int, destination: int, capacity: int) -> None:
68+
"""
69+
Add a directed edge ``source -> destination`` with the given capacity.
70+
71+
>>> g = Dinic(2)
72+
>>> g.add_edge(0, 1, 5)
73+
>>> g.add_edge(0, 1, -1)
74+
Traceback (most recent call last):
75+
...
76+
ValueError: capacity must be non-negative
77+
>>> g.add_edge(0, 2, 5)
78+
Traceback (most recent call last):
79+
...
80+
ValueError: vertex out of range
81+
"""
82+
if capacity < 0:
83+
raise ValueError("capacity must be non-negative")
84+
if not (0 <= source < self.size and 0 <= destination < self.size):
85+
raise ValueError("vertex out of range")
86+
self.graph[source].append(len(self.edges))
87+
self.edges.append([destination, capacity])
88+
self.graph[destination].append(len(self.edges))
89+
self.edges.append([source, 0]) # reverse edge starts saturated
90+
91+
def _build_level_graph(self, source: int) -> list[int]:
92+
"""Breadth-first search; return per-vertex levels (-1 if unreachable)."""
93+
level = [-1] * self.size
94+
level[source] = 0
95+
queue = deque([source])
96+
while queue:
97+
vertex = queue.popleft()
98+
for edge_index in self.graph[vertex]:
99+
destination, residual = self.edges[edge_index]
100+
if residual > 0 and level[destination] == -1:
101+
level[destination] = level[vertex] + 1
102+
queue.append(destination)
103+
return level
104+
105+
def _send_flow(
106+
self,
107+
vertex: int,
108+
pushed: int,
109+
sink: int,
110+
level: list[int],
111+
progress: list[int],
112+
) -> int:
113+
"""Depth-first search that pushes a blocking flow along the level graph."""
114+
if vertex == sink:
115+
return pushed
116+
while progress[vertex] < len(self.graph[vertex]):
117+
edge_index = self.graph[vertex][progress[vertex]]
118+
destination, residual = self.edges[edge_index]
119+
if residual > 0 and level[destination] == level[vertex] + 1:
120+
flow = self._send_flow(
121+
destination, min(pushed, residual), sink, level, progress
122+
)
123+
if flow > 0:
124+
self.edges[edge_index][1] -= flow
125+
self.edges[edge_index ^ 1][1] += flow
126+
return flow
127+
progress[vertex] += 1
128+
return 0
129+
130+
def max_flow(self, source: int, sink: int) -> int:
131+
"""
132+
Return the maximum flow from ``source`` to ``sink``.
133+
134+
>>> g = Dinic(4)
135+
>>> for (u, v), cap in {(0, 1): 3, (0, 2): 2, (1, 2): 5,
136+
... (1, 3): 2, (2, 3): 3}.items():
137+
... g.add_edge(u, v, cap)
138+
>>> g.max_flow(0, 3)
139+
5
140+
>>> g.max_flow(0, 0)
141+
Traceback (most recent call last):
142+
...
143+
ValueError: source and sink must be different
144+
"""
145+
if not (0 <= source < self.size and 0 <= sink < self.size):
146+
raise ValueError("vertex out of range")
147+
if source == sink:
148+
raise ValueError("source and sink must be different")
149+
infinity = sum(capacity for _, capacity in self.edges) + 1
150+
flow = 0
151+
level = self._build_level_graph(source)
152+
while level[sink] != -1:
153+
progress = [0] * self.size
154+
while True:
155+
pushed = self._send_flow(source, infinity, sink, level, progress)
156+
if pushed == 0:
157+
break
158+
flow += pushed
159+
level = self._build_level_graph(source)
160+
return flow
161+
162+
163+
if __name__ == "__main__":
164+
from doctest import testmod
165+
166+
testmod()

networking_flow/minimum_cut.py

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
# Minimum cut on Ford_Fulkerson algorithm.
1+
"""
2+
Minimum cut of a flow network via the Ford-Fulkerson algorithm.
3+
4+
The max-flow min-cut theorem says the value of a maximum flow from the source to
5+
the sink equals the total capacity of the edges in a minimum s-t cut -- the
6+
cheapest set of edges whose removal disconnects the sink from the source. This
7+
module finds those cut edges: it runs Ford-Fulkerson to build the residual
8+
graph, then reports every original edge that goes from a vertex still reachable
9+
from the source to a vertex that is not.
10+
11+
Reference: https://en.wikipedia.org/wiki/Minimum_cut
12+
See also: https://en.wikipedia.org/wiki/Max-flow_min-cut_theorem
13+
"""
214

315
test_graph = [
416
[0, 16, 13, 0, 0, 0],
@@ -10,57 +22,80 @@
1022
]
1123

1224

13-
def bfs(graph, s, t, parent):
14-
# Return True if there is node that has not iterated.
25+
def bfs(graph: list[list[int]], source: int, sink: int, parent: list[int]) -> bool:
26+
"""
27+
Return True if the ``sink`` is reachable from the ``source`` in the
28+
residual ``graph``, recording the traversal tree in ``parent``.
29+
30+
>>> bfs(test_graph, 0, 5, [-1] * 6)
31+
True
32+
>>> bfs([[0, 0], [0, 0]], 0, 1, [-1, -1])
33+
False
34+
"""
1535
visited = [False] * len(graph)
16-
queue = [s]
17-
visited[s] = True
36+
queue = [source]
37+
visited[source] = True
1838

1939
while queue:
20-
u = queue.pop(0)
21-
for ind in range(len(graph[u])):
22-
if visited[ind] is False and graph[u][ind] > 0:
23-
queue.append(ind)
24-
visited[ind] = True
25-
parent[ind] = u
40+
node = queue.pop(0)
41+
for neighbor in range(len(graph[node])):
42+
if visited[neighbor] is False and graph[node][neighbor] > 0:
43+
queue.append(neighbor)
44+
visited[neighbor] = True
45+
parent[neighbor] = node
46+
47+
return visited[sink]
48+
2649

27-
return visited[t]
50+
def mincut(graph: list[list[int]], source: int, sink: int) -> list[tuple[int, int]]:
51+
"""
52+
Return the edges of a minimum s-t cut as ``(from, to)`` tuples.
2853
54+
The input ``graph`` is an adjacency matrix of capacities and is left
55+
unchanged (the algorithm works on an internal copy).
2956
30-
def mincut(graph, source, sink):
31-
"""This array is filled by BFS and to store path
3257
>>> mincut(test_graph, source=0, sink=5)
3358
[(1, 3), (4, 3), (4, 5)]
59+
60+
The capacities of the cut edges sum to the maximum flow (23 here):
61+
62+
>>> sum(test_graph[u][v] for u, v in mincut(test_graph, 0, 5))
63+
23
64+
65+
A single saturated edge is its own minimum cut:
66+
67+
>>> mincut([[0, 7], [0, 0]], source=0, sink=1)
68+
[(0, 1)]
3469
"""
35-
parent = [-1] * (len(graph))
36-
max_flow = 0
70+
residual = [row[:] for row in graph] # work on a copy; keep the input intact
71+
parent = [-1] * (len(residual))
3772
res = []
38-
temp = [i[:] for i in graph] # Record original cut, copy.
39-
while bfs(graph, source, sink, parent):
40-
path_flow = float("Inf")
73+
while bfs(residual, source, sink, parent):
74+
path_flow = float("inf")
4175
s = sink
4276

4377
while s != source:
44-
# Find the minimum value in select path
45-
path_flow = min(path_flow, graph[parent[s]][s])
78+
# Find the minimum residual capacity along the augmenting path.
79+
path_flow = min(path_flow, residual[parent[s]][s])
4680
s = parent[s]
4781

48-
max_flow += path_flow
4982
v = sink
50-
5183
while v != source:
5284
u = parent[v]
53-
graph[u][v] -= path_flow
54-
graph[v][u] += path_flow
85+
residual[u][v] -= path_flow
86+
residual[v][u] += path_flow
5587
v = parent[v]
5688

5789
for i in range(len(graph)):
5890
for j in range(len(graph[0])):
59-
if graph[i][j] == 0 and temp[i][j] > 0:
91+
if graph[i][j] > 0 and residual[i][j] == 0:
6092
res.append((i, j))
6193

6294
return res
6395

6496

6597
if __name__ == "__main__":
98+
from doctest import testmod
99+
100+
testmod()
66101
print(mincut(test_graph, source=0, sink=5))

0 commit comments

Comments
 (0)