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}