graph_flow/hopcroft_karp.rs
1//! Hopcroft-Karp maximum bipartite matching.
2//!
3//! Finds the maximum matching in a bipartite graph using alternating BFS and
4//! DFS phases, achieving O(E · √V) — significantly faster than the naive
5//! O(V · E) augmenting-path approach for large graphs.
6
7use std::collections::VecDeque;
8
9/// The result of a Hopcroft-Karp bipartite matching computation.
10#[derive(Debug, Clone)]
11pub struct BipartiteMatching {
12 /// For each node in the **left** partition, the matched node in the right
13 /// partition (`Some(right_node)`) or `None` if unmatched.
14 ///
15 /// Indexed `0..left_size`.
16 pub match_left: Vec<Option<usize>>,
17 /// For each node in the **right** partition, the matched node in the left
18 /// partition (`Some(left_node)`) or `None` if unmatched.
19 ///
20 /// Indexed `0..right_size`.
21 pub match_right: Vec<Option<usize>>,
22 /// The size of the maximum matching (number of matched pairs).
23 pub matching_size: usize,
24}
25
26impl BipartiteMatching {
27 /// Returns all matched pairs as `(left_node, right_node)` tuples.
28 ///
29 /// # Examples
30 ///
31 /// ```
32 /// use graph_flow::hopcroft_karp;
33 ///
34 /// // Left: {0, 1}, Right: {0, 1}
35 /// // Edges: 0→0, 0→1, 1→1
36 /// let adj = vec![vec![0usize, 1], vec![1]];
37 /// let matching = hopcroft_karp(&adj, 2);
38 /// let pairs = matching.pairs();
39 /// assert_eq!(pairs.len(), 2);
40 /// ```
41 pub fn pairs(&self) -> Vec<(usize, usize)> {
42 self.match_left
43 .iter()
44 .enumerate()
45 .filter_map(|(l, &r)| r.map(|r| (l, r)))
46 .collect()
47 }
48}
49
50const UNMATCHED: usize = usize::MAX;
51const INF: usize = usize::MAX;
52
53/// Computes the **maximum bipartite matching** using the Hopcroft-Karp
54/// algorithm.
55///
56/// The bipartite graph is described by an adjacency list for the **left**
57/// partition only: `adj[u]` contains the indices of right-partition nodes that
58/// left-node `u` is connected to.
59///
60/// # Arguments
61///
62/// - `adj` — adjacency list for left nodes; `adj[u]` = list of right nodes
63/// adjacent to left node `u`. Left nodes are indexed `0..adj.len()`.
64/// - `right_size` — the number of nodes in the right partition, indexed
65/// `0..right_size`.
66///
67/// # Returns
68///
69/// A [`BipartiteMatching`] with the match assignments and total matching size.
70///
71/// # Algorithm
72///
73/// Hopcroft-Karp alternates between two phases:
74///
75/// 1. **BFS phase** — finds the shortest augmenting paths layer by layer,
76/// producing a layered graph.
77/// 2. **DFS phase** — finds a maximal set of vertex-disjoint augmenting paths
78/// within the layered graph and augments all of them simultaneously.
79///
80/// Each round of BFS + DFS increases the length of the shortest remaining
81/// augmenting path. Since augmenting path lengths are odd integers bounded by
82/// V, there are at most √V rounds, each taking O(E) — giving O(E · √V) total.
83///
84/// # Complexity
85///
86/// O(E · √V).
87///
88/// # Examples
89///
90/// ```
91/// use graph_flow::hopcroft_karp;
92///
93/// // Bipartite graph:
94/// // Left 0 — Right 0
95/// // Left 0 — Right 1
96/// // Left 1 — Right 1
97/// // Left 2 — Right 2
98/// //
99/// // Maximum matching: (0,0), (1,1), (2,2) — size 3.
100/// let adj = vec![
101/// vec![0usize, 1], // left 0 connects to right 0 and 1
102/// vec![1], // left 1 connects to right 1
103/// vec![2], // left 2 connects to right 2
104/// ];
105/// let matching = hopcroft_karp(&adj, 3);
106/// assert_eq!(matching.matching_size, 3);
107/// ```
108pub fn hopcroft_karp(adj: &[Vec<usize>], right_size: usize) -> BipartiteMatching {
109 let left_size = adj.len();
110
111 // match_left[u] = right node matched to left u (UNMATCHED if none).
112 let mut match_left = vec![UNMATCHED; left_size];
113 // match_right[v] = left node matched to right v (UNMATCHED if none).
114 let mut match_right = vec![UNMATCHED; right_size];
115
116 let mut matching_size = 0;
117
118 loop {
119 // BFS phase: build layered graph and compute dist[] for left nodes.
120 let dist = bfs_phase(adj, &match_left, &match_right, left_size);
121
122 if dist[left_size] == INF {
123 // No augmenting path exists — maximum matching found.
124 break;
125 }
126
127 // DFS phase: find maximal set of augmenting paths in the layered graph.
128 // We need dist to be mutable for the DFS to mark used layers.
129 let mut dist = dist;
130
131 for u in 0..left_size {
132 if match_left[u] == UNMATCHED
133 && dfs_phase(
134 adj,
135 &mut match_left,
136 &mut match_right,
137 &mut dist,
138 u,
139 left_size,
140 )
141 {
142 matching_size += 1;
143 }
144 }
145 }
146
147 // Convert UNMATCHED sentinels to Option.
148 let match_left_opt: Vec<Option<usize>> = match_left
149 .iter()
150 .map(|&m| if m == UNMATCHED { None } else { Some(m) })
151 .collect();
152 let match_right_opt: Vec<Option<usize>> = match_right
153 .iter()
154 .map(|&m| if m == UNMATCHED { None } else { Some(m) })
155 .collect();
156
157 BipartiteMatching {
158 match_left: match_left_opt,
159 match_right: match_right_opt,
160 matching_size,
161 }
162}
163
164/// BFS phase: computes shortest distances from free left nodes to the virtual
165/// sink node (`dist[left_size]`).
166///
167/// Left nodes are `0..left_size`. We use `left_size` as a virtual sink
168/// sentinel representing "reached an unmatched right node".
169///
170/// Returns `dist` where `dist[u]` is the BFS layer of left node `u`.
171fn bfs_phase(
172 adj: &[Vec<usize>],
173 match_left: &[usize],
174 match_right: &[usize],
175 left_size: usize,
176) -> Vec<usize> {
177 let mut dist = vec![INF; left_size + 1]; // +1 for the virtual sink
178 let mut queue = VecDeque::new();
179
180 // Seed BFS with all free (unmatched) left nodes at layer 0.
181 for u in 0..left_size {
182 if match_left[u] == UNMATCHED {
183 dist[u] = 0;
184 queue.push_back(u);
185 }
186 }
187
188 // dist[left_size] represents reaching a free right node (augmenting path found).
189 dist[left_size] = INF;
190
191 while let Some(u) = queue.pop_front() {
192 if dist[u] < dist[left_size] {
193 for &v in &adj[u] {
194 // The next left node along the alternating path is the one
195 // currently matched to right node v.
196 let next_left = match_right[v];
197 if next_left == UNMATCHED {
198 // v is a free right node: we've found an augmenting path.
199 dist[left_size] = dist[u] + 1;
200 } else if dist[next_left] == INF {
201 dist[next_left] = dist[u] + 1;
202 queue.push_back(next_left);
203 }
204 }
205 }
206 }
207
208 dist
209}
210
211/// DFS phase: finds and augments one alternating path from left node `u` to a
212/// free right node, using only edges consistent with the layered graph.
213///
214/// Returns `true` if an augmenting path was found and flow was pushed.
215fn dfs_phase(
216 adj: &[Vec<usize>],
217 match_left: &mut Vec<usize>,
218 match_right: &mut Vec<usize>,
219 dist: &mut Vec<usize>,
220 u: usize,
221 left_size: usize,
222) -> bool {
223 if u == left_size {
224 return true; // Reached the virtual sink — augmenting path complete.
225 }
226
227 for &v in &adj[u] {
228 let next_left = match_right[v];
229 let next_left_idx = if next_left == UNMATCHED {
230 left_size
231 } else {
232 next_left
233 };
234
235 // Only follow edges that advance us in the layered graph.
236 if dist[next_left_idx] == dist[u] + 1
237 && dfs_phase(adj, match_left, match_right, dist, next_left_idx, left_size)
238 {
239 match_left[u] = v;
240 match_right[v] = u;
241 return true;
242 }
243 }
244
245 // No augmenting path from u — block this node from future DFS visits
246 // this round by setting its distance to INF.
247 dist[u] = INF;
248 false
249}