Skip to main content

rust_igraph/algorithms/flow/
max_flow.rs

1//! `max_flow_value` (ALGO-FL-002) — Dinic's algorithm.
2//!
3//! Counterpart of `igraph_maxflow_value` in
4//! `references/igraph/src/flow/flow.c` (`igraph_maxflow_value` is a
5//! thin wrapper around `igraph_maxflow`; the C implementation is
6//! Goldberg-Tarjan push-relabel, ~600 LOC). We implement Dinic's
7//! algorithm here instead — O(V²·E) worst-case, but with much smaller
8//! constants on the sparse networks rust-igraph targets, and a far
9//! simpler control-flow that fits the self-roll preference of the
10//! project. The scalar max-flow value is unique (Ford-Fulkerson
11//! correctness + max-flow / min-cut duality), so cross-backend
12//! conformance against the C reference is exact on integer capacities
13//! and within 1e-9 on `f64`.
14//!
15//! ## Pre-conditions
16//!
17//! - `source < vcount()` and `target < vcount()`, else
18//!   [`IgraphError::VertexOutOfRange`].
19//! - `source != target`, else [`IgraphError::InvalidArgument`] (matches
20//!   igraph C's `IGRAPH_EINVAL`).
21//! - When `capacity` is supplied, its length must equal `ecount()`,
22//!   else [`IgraphError::InvalidArgument`]. Each capacity must be
23//!   finite and non-negative. Negative or non-finite capacities raise
24//!   [`IgraphError::InvalidArgument`].
25//! - When `capacity` is `None`, each edge contributes unit capacity
26//!   (the igraph C default when the capacity vector is `NULL`).
27//!
28//! ## Undirected handling
29//!
30//! igraph C converts each undirected edge `(i, j)` of capacity `c`
31//! into two directed arcs `i → j` and `j → i`, both with capacity
32//! `c`, before running the directed algorithm. We follow the same
33//! pattern: we always materialise a directed residual network whose
34//! arc count is `2 · ecount()` (forward) plus `2 · ecount()`
35//! (reverse residual arcs). For directed input, each input edge
36//! contributes one forward arc with capacity `c[e]` and one zero-cap
37//! reverse arc; for undirected input, each input edge contributes two
38//! forward arcs (each with capacity `c[e]`) and two reverse arcs (the
39//! "reverse" arc of the second forward is the first forward, so the
40//! residual structure naturally encodes the undirected semantics).
41
42// Arc indices fit in u32 by the residual-network construction (each
43// input edge contributes two arcs, so the arc count is `2 * ecount()`
44// and `ecount() <= u32::MAX / 2` is an invariant inherited from
45// `Graph`). Vertex ids are u32 by definition. Inner-loop casts here are
46// either provably bounded (2 * ecount() or 2 * ecount() + 1) or
47// round-trips of values that were already u32 in storage.
48#![allow(clippy::cast_possible_truncation)]
49
50use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
51
52/// Scalar maximum-flow value from `source` to `target`.
53///
54/// Counterpart of `igraph_maxflow_value` in
55/// `references/igraph/src/flow/flow.c` (igraph C uses Goldberg-Tarjan
56/// push-relabel; this implementation uses Dinic's algorithm — the
57/// scalar value matches by the max-flow / min-cut uniqueness theorem).
58///
59/// # Arguments
60///
61/// * `graph` — input graph (directed or undirected).
62/// * `source` — source vertex id (`0 ≤ source < vcount()`).
63/// * `target` — sink vertex id (`0 ≤ target < vcount()`, `target != source`).
64/// * `capacity` — optional per-edge capacity in the graph's edge-id
65///   order. When `None`, each edge contributes unit capacity. When
66///   `Some(c)`, `c.len()` must equal `graph.ecount()`, and every entry
67///   must be finite and `≥ 0`.
68///
69/// # Returns
70///
71/// The maximum total flow value as `f64`, summing to `0.0` when source
72/// and target are in disjoint connected components or when every
73/// `source → target` path is blocked by zero-capacity edges.
74///
75/// # Errors
76///
77/// * [`IgraphError::VertexOutOfRange`] if `source` or `target` is
78///   outside `[0, vcount())`.
79/// * [`IgraphError::InvalidArgument`] if `source == target`, the
80///   capacity slice length differs from `ecount()`, or any capacity is
81///   negative / non-finite.
82///
83/// # Examples
84///
85/// ```
86/// use rust_igraph::{Graph, max_flow_value};
87///
88/// // Two parallel paths of capacity 1 each → max flow = 2.
89/// let mut g = Graph::new(4, true).unwrap();
90/// g.add_edge(0, 1).unwrap();
91/// g.add_edge(1, 3).unwrap();
92/// g.add_edge(0, 2).unwrap();
93/// g.add_edge(2, 3).unwrap();
94/// let cap = vec![1.0, 1.0, 1.0, 1.0];
95/// let f = max_flow_value(&g, 0, 3, Some(&cap)).unwrap();
96/// assert!((f - 2.0).abs() < 1e-12);
97/// ```
98pub fn max_flow_value(
99    graph: &Graph,
100    source: VertexId,
101    target: VertexId,
102    capacity: Option<&[f64]>,
103) -> IgraphResult<f64> {
104    max_flow_with_residual(graph, source, target, capacity).map(|(v, _)| v)
105}
106
107/// Full maximum-flow computation: value, per-edge flow, cut edges,
108/// and source-side / sink-side vertex partitions.
109///
110/// Counterpart of `igraph_maxflow` in
111/// `references/igraph/src/flow/flow.c` (igraph C uses Goldberg-Tarjan
112/// push-relabel; this implementation uses Dinic's algorithm). The
113/// scalar max-flow value is unique by the max-flow / min-cut theorem;
114/// the flow decomposition and partition may differ between algorithms,
115/// but the invariants are the same.
116///
117/// # Arguments
118///
119/// * `graph` — input graph (directed or undirected).
120/// * `source` — source vertex id (`0 ≤ source < vcount()`).
121/// * `target` — sink vertex id (`0 ≤ target < vcount()`, `target != source`).
122/// * `capacity` — optional per-edge capacity in the graph's edge-id
123///   order. When `None`, each edge contributes unit capacity. When
124///   `Some(c)`, `c.len()` must equal `graph.ecount()`, and every entry
125///   must be finite and `≥ 0`.
126///
127/// # Returns
128///
129/// A [`MaxFlow`] containing:
130///
131/// * `value` — the maximum total flow from `source` to `target`.
132/// * `flow` — per-edge flow values (`flow.len() == ecount()`). For
133///   directed graphs the flow on each edge is non-negative and respects
134///   the capacity constraint. For undirected graphs the flow may be
135///   negative (indicating flow in the reverse direction of the edge's
136///   stored orientation).
137/// * `cut` — edge ids of the minimum cut (saturated edges crossing the
138///   source-side / sink-side partition boundary).
139/// * `partition` — source-side vertex ids (sorted ascending).
140/// * `partition2` — sink-side vertex ids (sorted ascending).
141///
142/// # Errors
143///
144/// * [`IgraphError::VertexOutOfRange`] if `source` or `target` is
145///   outside `[0, vcount())`.
146/// * [`IgraphError::InvalidArgument`] if `source == target`, the
147///   capacity slice length differs from `ecount()`, or any capacity is
148///   negative / non-finite.
149///
150/// # Examples
151///
152/// ```
153/// use rust_igraph::{Graph, max_flow};
154///
155/// // Two parallel paths of capacity 1 each → max flow = 2.
156/// let mut g = Graph::new(4, true).unwrap();
157/// g.add_edge(0, 1).unwrap();
158/// g.add_edge(1, 3).unwrap();
159/// g.add_edge(0, 2).unwrap();
160/// g.add_edge(2, 3).unwrap();
161/// let cap = vec![1.0, 1.0, 1.0, 1.0];
162/// let result = max_flow(&g, 0, 3, Some(&cap)).unwrap();
163/// assert!((result.value - 2.0).abs() < 1e-12);
164/// assert_eq!(result.flow.len(), 4);
165/// ```
166pub fn max_flow(
167    graph: &Graph,
168    source: VertexId,
169    target: VertexId,
170    capacity: Option<&[f64]>,
171) -> IgraphResult<MaxFlow> {
172    let m = graph.ecount();
173    let directed = graph.is_directed();
174    let (value, net) = max_flow_with_residual(graph, source, target, capacity)?;
175
176    // Extract per-edge flow from the residual network.
177    // For each original edge e, the forward arc is at index 2*e and
178    // the paired reverse arc at 2*e+1.
179    //
180    // Directed: flow[e] = cap_initial[e] - residual[2*e], always ≥ 0.
181    //   The reverse arc started at 0 capacity, so only forward matters.
182    //
183    // Undirected: both arcs started at cap_initial. When Dinic pushes
184    //   flow f on the forward arc, residual[fwd] decreases by f and
185    //   residual[rev] increases by f (XOR pairing). So:
186    //     residual[rev] - residual[fwd] = 2*f
187    //   The net flow on the original edge is f, so we halve the
188    //   difference. This matches igraph C's approach of computing
189    //   flow_fwd - flow_rev where each is extracted from the directed
190    //   doubled graph.
191    let mut flow_vec: Vec<f64> = Vec::with_capacity(m);
192    for e in 0..m {
193        let fwd = 2 * e;
194        let rev = fwd + 1;
195        if directed {
196            let initial = capacity.map_or(1.0, |c| c[e]);
197            flow_vec.push(initial - net.cap[fwd]);
198        } else {
199            flow_vec.push((net.cap[rev] - net.cap[fwd]) / 2.0);
200        }
201    }
202
203    // BFS from source in the residual to find the source-side partition.
204    let n = net.n;
205    let mut in_source = vec![false; n];
206    in_source[source as usize] = true;
207    let mut queue: Vec<u32> = Vec::with_capacity(n);
208    queue.push(source);
209    let mut head_ptr = 0_usize;
210    while head_ptr < queue.len() {
211        let v = queue[head_ptr] as usize;
212        head_ptr += 1;
213        for &arc in &net.arcs_out[v] {
214            let arc_us = arc as usize;
215            if net.cap[arc_us] <= 0.0 {
216                continue;
217            }
218            let w = net.head[arc_us] as usize;
219            if !in_source[w] {
220                in_source[w] = true;
221                queue.push(w as u32);
222            }
223        }
224    }
225
226    let mut partition: Vec<u32> = Vec::with_capacity(queue.len());
227    let mut partition2: Vec<u32> = Vec::with_capacity(n.saturating_sub(queue.len()));
228    for (v, &is_src) in in_source.iter().enumerate() {
229        if is_src {
230            partition.push(v as u32);
231        } else {
232            partition2.push(v as u32);
233        }
234    }
235
236    // Cut edges: crossing the partition boundary.
237    let edge_count = u32::try_from(m).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
238    let mut cut: Vec<u32> = Vec::new();
239    for eid in 0..edge_count {
240        let (u, v) = graph.edge(eid)?;
241        let u_in = in_source[u as usize];
242        let v_in = in_source[v as usize];
243        let crosses = if directed {
244            u_in && !v_in
245        } else {
246            u_in != v_in
247        };
248        if crosses {
249            cut.push(eid);
250        }
251    }
252
253    Ok(MaxFlow {
254        value,
255        flow: flow_vec,
256        cut,
257        partition,
258        partition2,
259    })
260}
261
262/// Output of [`max_flow`]: scalar value, per-edge flow vector, cut
263/// edge ids, and source-side / sink-side vertex partitions.
264///
265/// Mirrors the output parameters of `igraph_maxflow` in
266/// `references/igraph/src/flow/flow.c` (`value`, `flow`, `cut`,
267/// `partition`, `partition2`).
268#[derive(Debug, Clone)]
269pub struct MaxFlow {
270    /// The maximum total flow from source to target.
271    pub value: f64,
272    /// Per-edge flow values in edge-id order (`flow.len() == ecount()`).
273    /// For directed graphs each entry is non-negative. For undirected
274    /// graphs the sign indicates direction: positive means flow in the
275    /// stored edge direction (source → target), negative means reverse.
276    pub flow: Vec<f64>,
277    /// Edge ids of the minimum cut (saturated edges crossing the
278    /// source/sink partition boundary).
279    pub cut: Vec<u32>,
280    /// Source-side partition (vertices reachable from `source` in the
281    /// residual network). Contains `source`. Sorted ascending.
282    pub partition: Vec<u32>,
283    /// Sink-side partition (complement of `partition`). Contains
284    /// `target`. Sorted ascending.
285    pub partition2: Vec<u32>,
286}
287
288/// Internal entry point: runs Dinic and returns both the scalar flow
289/// value AND the post-augmentation residual network. Crate-private —
290/// the public API exposes [`max_flow_value`] (value only), [`max_flow`]
291/// (full result), and FL-018's `st_mincut` (which uses the residual to
292/// extract a min-cut partition).
293///
294/// Same validation contract as [`max_flow_value`].
295pub(crate) fn max_flow_with_residual(
296    graph: &Graph,
297    source: VertexId,
298    target: VertexId,
299    capacity: Option<&[f64]>,
300) -> IgraphResult<(f64, Network)> {
301    let n = graph.vcount();
302    if n == 0 || source >= n {
303        return Err(IgraphError::VertexOutOfRange { id: source, n });
304    }
305    if target >= n {
306        return Err(IgraphError::VertexOutOfRange { id: target, n });
307    }
308    if source == target {
309        return Err(IgraphError::InvalidArgument(
310            "source and target must be distinct".to_string(),
311        ));
312    }
313
314    let m = graph.ecount();
315    if let Some(c) = capacity {
316        if c.len() != m {
317            return Err(IgraphError::InvalidArgument(format!(
318                "capacity length {} does not match edge count {}",
319                c.len(),
320                m
321            )));
322        }
323        for (i, &v) in c.iter().enumerate() {
324            if !v.is_finite() || v < 0.0 {
325                return Err(IgraphError::InvalidArgument(format!(
326                    "capacity[{i}] = {v} is not a finite non-negative number"
327                )));
328            }
329        }
330    }
331
332    let net = Network::build(graph, capacity)?;
333    let mut state = DinicState::new(net);
334    let value = state.run(source, target);
335    Ok((value, state.into_network()))
336}
337
338/// Residual network in flat-CSR form with paired arcs.
339///
340/// Arcs are stored in pairs: for each input forward arc at index `2k`,
341/// its reverse residual sits at index `2k + 1` (and vice versa). The
342/// XOR `idx ^ 1` recovers the paired arc.
343///
344/// Crate-private: FL-018 (`st_mincut`) reads `cap` / `head` / `arcs_out`
345/// after Dinic terminates to BFS the source-side partition off the
346/// residual graph. Field visibility is `pub(crate)` for that reason
347/// only — outside `crate::algorithms::flow`, treat this as opaque.
348pub(crate) struct Network {
349    pub(crate) n: usize,
350    /// Head (destination vertex) of each arc.
351    pub(crate) head: Vec<u32>,
352    /// Residual capacity of each arc (may be modified during the flow
353    /// computation). For input forward arcs this starts at `capacity[e]`
354    /// (or `1.0` if `capacity` is `None`); for the reverse residual
355    /// this starts at `0.0` on directed input, or also at `capacity[e]`
356    /// on undirected input.
357    pub(crate) cap: Vec<f64>,
358    /// CSR-style adjacency: `arcs_out[v]` lists every arc index whose
359    /// tail is `v`. Built once at construction.
360    pub(crate) arcs_out: Vec<Vec<u32>>,
361}
362
363impl Network {
364    fn build(graph: &Graph, capacity: Option<&[f64]>) -> IgraphResult<Self> {
365        let n = graph.vcount() as usize;
366        let m = graph.ecount();
367        let directed = graph.is_directed();
368
369        // Two arcs per input edge in either case: directed → (fwd, rev0);
370        // undirected → (fwd_u→v, fwd_v→u). The "reverse residual" arc
371        // for undirected is simply the other forward arc with the same
372        // capacity, since the XOR pairing matches our layout.
373        let arc_count = m
374            .checked_mul(2)
375            .ok_or(IgraphError::Internal("arc count overflows usize"))?;
376
377        let mut head = vec![0_u32; arc_count];
378        let mut cap = vec![0.0_f64; arc_count];
379        let mut arcs_out: Vec<Vec<u32>> = vec![Vec::new(); n];
380
381        let edge_count =
382            u32::try_from(m).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
383        for eid in 0..edge_count {
384            let (src, dst) = graph.edge(eid)?;
385            let e_us = eid as usize;
386            let cap_val = capacity.map_or(1.0, |c| c[e_us]);
387
388            let fwd = 2 * e_us;
389            let rev = fwd + 1;
390            head[fwd] = dst;
391            head[rev] = src;
392            cap[fwd] = cap_val;
393            cap[rev] = if directed { 0.0 } else { cap_val };
394            // Both arcs are usable for traversal in either mode. For
395            // directed input, `cap[rev] == 0.0` so the BFS/DFS skip it
396            // until augmenting flow gives it residual capacity.
397            arcs_out[src as usize].push(fwd as u32);
398            arcs_out[dst as usize].push(rev as u32);
399        }
400
401        Ok(Self {
402            n,
403            head,
404            cap,
405            arcs_out,
406        })
407    }
408}
409
410struct DinicState {
411    net: Network,
412    /// BFS distance from source; `u32::MAX` means unreachable.
413    level: Vec<u32>,
414    /// DFS current-arc pointer into `arcs_out[v]` so we don't revisit
415    /// saturated arcs within the same blocking-flow phase.
416    iter: Vec<u32>,
417    /// BFS queue scratch buffer.
418    queue: Vec<u32>,
419}
420
421impl DinicState {
422    fn new(net: Network) -> Self {
423        let n = net.n;
424        Self {
425            net,
426            level: vec![u32::MAX; n],
427            iter: vec![0_u32; n],
428            queue: Vec::with_capacity(n),
429        }
430    }
431
432    /// Surrender the residual network after `run`. Used by FL-018 to
433    /// BFS the source-side partition off the final residual.
434    fn into_network(self) -> Network {
435        self.net
436    }
437
438    fn run(&mut self, source: u32, target: u32) -> f64 {
439        let mut total = 0.0_f64;
440        let src = source as usize;
441        let dst = target as usize;
442        while self.bfs(src, dst) {
443            for it in &mut self.iter {
444                *it = 0;
445            }
446            loop {
447                let pushed = self.dfs(src, dst, f64::INFINITY);
448                if pushed <= 0.0 {
449                    break;
450                }
451                total += pushed;
452            }
453        }
454        total
455    }
456
457    /// BFS in the residual graph (only arcs with positive capacity).
458    /// Returns true iff `target` is reachable.
459    fn bfs(&mut self, source: usize, target: usize) -> bool {
460        for l in &mut self.level {
461            *l = u32::MAX;
462        }
463        self.level[source] = 0;
464        self.queue.clear();
465        self.queue.push(source as u32);
466        let mut head_ptr = 0_usize;
467        while head_ptr < self.queue.len() {
468            let v = self.queue[head_ptr] as usize;
469            head_ptr += 1;
470            let next_level = self.level[v].saturating_add(1);
471            for &a in &self.net.arcs_out[v] {
472                let a_us = a as usize;
473                if self.net.cap[a_us] <= 0.0 {
474                    continue;
475                }
476                let w = self.net.head[a_us] as usize;
477                if self.level[w] == u32::MAX {
478                    self.level[w] = next_level;
479                    self.queue.push(w as u32);
480                }
481            }
482        }
483        self.level[target] != u32::MAX
484    }
485
486    /// DFS that pushes up to `limit` of residual capacity from `v` to
487    /// `target` along level-monotone arcs. Returns the actual amount
488    /// pushed (0 if no augmenting path remains from `v`).
489    fn dfs(&mut self, v: usize, target: usize, limit: f64) -> f64 {
490        if v == target {
491            return limit;
492        }
493        let next_level = self.level[v].saturating_add(1);
494        while (self.iter[v] as usize) < self.net.arcs_out[v].len() {
495            let arc_idx = self.net.arcs_out[v][self.iter[v] as usize] as usize;
496            let w = self.net.head[arc_idx] as usize;
497            let cap_here = self.net.cap[arc_idx];
498            if cap_here > 0.0 && self.level[w] == next_level {
499                let send = limit.min(cap_here);
500                let pushed = self.dfs(w, target, send);
501                if pushed > 0.0 {
502                    self.net.cap[arc_idx] -= pushed;
503                    self.net.cap[arc_idx ^ 1] += pushed;
504                    return pushed;
505                }
506            }
507            self.iter[v] += 1;
508        }
509        0.0
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    fn assert_close(actual: f64, expected: f64, tol: f64) {
518        assert!(
519            (actual - expected).abs() < tol,
520            "actual = {actual}, expected = {expected}"
521        );
522    }
523
524    #[test]
525    fn rejects_out_of_range_source() {
526        let g = Graph::with_vertices(2);
527        let err = max_flow_value(&g, 5, 1, None).unwrap_err();
528        match err {
529            IgraphError::VertexOutOfRange { id, n } => {
530                assert_eq!(id, 5);
531                assert_eq!(n, 2);
532            }
533            _ => panic!("expected VertexOutOfRange"),
534        }
535    }
536
537    #[test]
538    fn rejects_out_of_range_target() {
539        let g = Graph::with_vertices(2);
540        let err = max_flow_value(&g, 0, 9, None).unwrap_err();
541        assert!(matches!(err, IgraphError::VertexOutOfRange { id: 9, n: 2 }));
542    }
543
544    #[test]
545    fn rejects_source_equals_target() {
546        let g = Graph::with_vertices(2);
547        let err = max_flow_value(&g, 0, 0, None).unwrap_err();
548        assert!(matches!(err, IgraphError::InvalidArgument(_)));
549    }
550
551    #[test]
552    fn rejects_wrong_capacity_length() {
553        let mut g = Graph::with_vertices(2);
554        g.add_edge(0, 1).unwrap();
555        let cap = vec![1.0, 2.0];
556        let err = max_flow_value(&g, 0, 1, Some(&cap)).unwrap_err();
557        assert!(matches!(err, IgraphError::InvalidArgument(_)));
558    }
559
560    #[test]
561    fn rejects_negative_capacity() {
562        let mut g = Graph::with_vertices(2);
563        g.add_edge(0, 1).unwrap();
564        let cap = vec![-1.0];
565        let err = max_flow_value(&g, 0, 1, Some(&cap)).unwrap_err();
566        assert!(matches!(err, IgraphError::InvalidArgument(_)));
567    }
568
569    #[test]
570    fn isolated_source_and_target() {
571        let g = Graph::with_vertices(2);
572        let f = max_flow_value(&g, 0, 1, None).unwrap();
573        assert_close(f, 0.0, 1e-12);
574    }
575
576    #[test]
577    fn single_edge_directed_unit() {
578        let mut g = Graph::new(2, true).unwrap();
579        g.add_edge(0, 1).unwrap();
580        let f = max_flow_value(&g, 0, 1, None).unwrap();
581        assert_close(f, 1.0, 1e-12);
582    }
583
584    #[test]
585    fn single_edge_directed_wrong_direction() {
586        let mut g = Graph::new(2, true).unwrap();
587        g.add_edge(0, 1).unwrap();
588        let f = max_flow_value(&g, 1, 0, None).unwrap();
589        assert_close(f, 0.0, 1e-12);
590    }
591
592    #[test]
593    fn single_edge_undirected_unit() {
594        let mut g = Graph::with_vertices(2);
595        g.add_edge(0, 1).unwrap();
596        let f = max_flow_value(&g, 0, 1, None).unwrap();
597        assert_close(f, 1.0, 1e-12);
598        let f_rev = max_flow_value(&g, 1, 0, None).unwrap();
599        assert_close(f_rev, 1.0, 1e-12);
600    }
601
602    #[test]
603    fn two_parallel_paths_directed() {
604        // 0 → 1 → 3,  0 → 2 → 3, all unit capacity.
605        let mut g = Graph::new(4, true).unwrap();
606        g.add_edge(0, 1).unwrap();
607        g.add_edge(1, 3).unwrap();
608        g.add_edge(0, 2).unwrap();
609        g.add_edge(2, 3).unwrap();
610        let f = max_flow_value(&g, 0, 3, None).unwrap();
611        assert_close(f, 2.0, 1e-12);
612    }
613
614    #[test]
615    fn bottleneck_directed() {
616        // 0 → 1 (cap 5), 1 → 2 (cap 2), 2 → 3 (cap 5) → bottleneck = 2.
617        let mut g = Graph::new(4, true).unwrap();
618        g.add_edge(0, 1).unwrap();
619        g.add_edge(1, 2).unwrap();
620        g.add_edge(2, 3).unwrap();
621        let cap = vec![5.0, 2.0, 5.0];
622        let f = max_flow_value(&g, 0, 3, Some(&cap)).unwrap();
623        assert_close(f, 2.0, 1e-12);
624    }
625
626    #[test]
627    fn classic_max_flow_textbook() {
628        // CLRS classic flow network: 6 vertices s = 0, t = 5.
629        // Arcs and capacities:
630        //   0→1:16  0→2:13  1→3:12  2→1:4  2→4:14  3→2:9  3→5:20  4→3:7  4→5:4
631        // Max flow = 23.
632        let mut g = Graph::new(6, true).unwrap();
633        g.add_edge(0, 1).unwrap();
634        g.add_edge(0, 2).unwrap();
635        g.add_edge(1, 3).unwrap();
636        g.add_edge(2, 1).unwrap();
637        g.add_edge(2, 4).unwrap();
638        g.add_edge(3, 2).unwrap();
639        g.add_edge(3, 5).unwrap();
640        g.add_edge(4, 3).unwrap();
641        g.add_edge(4, 5).unwrap();
642        let cap = vec![16.0, 13.0, 12.0, 4.0, 14.0, 9.0, 20.0, 7.0, 4.0];
643        let f = max_flow_value(&g, 0, 5, Some(&cap)).unwrap();
644        assert_close(f, 23.0, 1e-12);
645    }
646
647    #[test]
648    fn multigraph_parallel_edges() {
649        // Three parallel arcs 0→1 with capacities 1, 2, 4 → total 7.
650        let mut g = Graph::new(2, true).unwrap();
651        g.add_edge(0, 1).unwrap();
652        g.add_edge(0, 1).unwrap();
653        g.add_edge(0, 1).unwrap();
654        let cap = vec![1.0, 2.0, 4.0];
655        let f = max_flow_value(&g, 0, 1, Some(&cap)).unwrap();
656        assert_close(f, 7.0, 1e-12);
657    }
658
659    #[test]
660    fn self_loop_does_not_contribute() {
661        // A self-loop on the source can't add flow to the sink.
662        let mut g = Graph::new(2, true).unwrap();
663        g.add_edge(0, 0).unwrap();
664        g.add_edge(0, 1).unwrap();
665        let cap = vec![100.0, 3.0];
666        let f = max_flow_value(&g, 0, 1, Some(&cap)).unwrap();
667        assert_close(f, 3.0, 1e-12);
668    }
669
670    #[test]
671    fn undirected_two_paths_share_capacity() {
672        // Undirected edges 0-1, 1-2, all unit capacity. The two
673        // forward orientations of {0,1} share a single capacity unit
674        // (igraph C semantics: convert to two directed arcs of cap c
675        // each, but only one unit of net flow can move from 0 to 1 at
676        // a time before residuals cancel).
677        let mut g = Graph::with_vertices(3);
678        g.add_edge(0, 1).unwrap();
679        g.add_edge(1, 2).unwrap();
680        let f = max_flow_value(&g, 0, 2, None).unwrap();
681        assert_close(f, 1.0, 1e-12);
682    }
683
684    #[test]
685    fn weighted_fractional_flow() {
686        // Two parallel paths with fractional capacities → max flow = 0.75.
687        let mut g = Graph::new(4, true).unwrap();
688        g.add_edge(0, 1).unwrap();
689        g.add_edge(1, 3).unwrap();
690        g.add_edge(0, 2).unwrap();
691        g.add_edge(2, 3).unwrap();
692        let cap = vec![0.5, 0.5, 0.25, 0.25];
693        let f = max_flow_value(&g, 0, 3, Some(&cap)).unwrap();
694        assert_close(f, 0.75, 1e-12);
695    }
696
697    // ---- max_flow (full result) tests ----
698
699    fn validate_flow(graph: &Graph, result: &MaxFlow, capacity: Option<&[f64]>) {
700        let m = graph.ecount();
701        let n = graph.vcount();
702        let directed = graph.is_directed();
703
704        // flow vector has correct length
705        assert_eq!(result.flow.len(), m, "flow.len() must equal ecount()");
706
707        // capacity constraints: |flow[e]| <= capacity[e]
708        for e in 0..m {
709            let cap_e = capacity.map_or(1.0, |c| c[e]);
710            assert!(
711                result.flow[e].abs() <= cap_e + 1e-12,
712                "flow[{e}] = {} exceeds capacity {cap_e}",
713                result.flow[e]
714            );
715            if directed {
716                assert!(
717                    result.flow[e] >= -1e-12,
718                    "directed flow[{e}] = {} must be non-negative",
719                    result.flow[e]
720                );
721            }
722        }
723
724        // flow conservation: for every vertex != source, target,
725        // sum of incoming flow == sum of outgoing flow.
726        // We'll skip this for undirected (harder to define direction).
727        if directed {
728            for v in 0..n {
729                if v == result.partition[0]
730                    || !result.partition2.is_empty()
731                        && v == *result.partition2.last().unwrap_or(&u32::MAX)
732                {
733                    continue;
734                }
735                let mut net = 0.0_f64;
736                for e in 0..m {
737                    let (src, dst) = graph.edge(e as u32).unwrap();
738                    if dst == v {
739                        net += result.flow[e];
740                    }
741                    if src == v {
742                        net -= result.flow[e];
743                    }
744                }
745                assert!(
746                    net.abs() < 1e-9,
747                    "flow conservation violated at vertex {v}: net = {net}"
748                );
749            }
750        }
751
752        // partitions well-formed
753        assert_eq!(result.partition.len() + result.partition2.len(), n as usize);
754        assert!(result.partition.windows(2).all(|w| w[0] < w[1]));
755        assert!(result.partition2.windows(2).all(|w| w[0] < w[1]));
756
757        // cut capacity sum equals value
758        let cut_sum: f64 = result
759            .cut
760            .iter()
761            .map(|&e| capacity.map_or(1.0, |c| c[e as usize]))
762            .sum();
763        assert_close(cut_sum, result.value, 1e-9 * result.value.abs().max(1.0));
764
765        // cut edges cross the partition boundary
766        let mut in_source = vec![false; n as usize];
767        for &v in &result.partition {
768            in_source[v as usize] = true;
769        }
770        for &eid in &result.cut {
771            let (u, v) = graph.edge(eid).unwrap();
772            if directed {
773                assert!(
774                    in_source[u as usize] && !in_source[v as usize],
775                    "directed cut edge {eid} must go S→V\\S"
776                );
777            } else {
778                assert_ne!(
779                    in_source[u as usize], in_source[v as usize],
780                    "cut edge {eid} must cross partition"
781                );
782            }
783        }
784    }
785
786    #[test]
787    fn max_flow_two_parallel_paths() {
788        let mut g = Graph::new(4, true).unwrap();
789        g.add_edge(0, 1).unwrap();
790        g.add_edge(1, 3).unwrap();
791        g.add_edge(0, 2).unwrap();
792        g.add_edge(2, 3).unwrap();
793        let cap = vec![1.0, 1.0, 1.0, 1.0];
794        let r = max_flow(&g, 0, 3, Some(&cap)).unwrap();
795        assert_close(r.value, 2.0, 1e-12);
796        validate_flow(&g, &r, Some(&cap));
797    }
798
799    #[test]
800    fn max_flow_bottleneck() {
801        let mut g = Graph::new(4, true).unwrap();
802        g.add_edge(0, 1).unwrap();
803        g.add_edge(1, 2).unwrap();
804        g.add_edge(2, 3).unwrap();
805        let cap = vec![5.0, 2.0, 7.0];
806        let r = max_flow(&g, 0, 3, Some(&cap)).unwrap();
807        assert_close(r.value, 2.0, 1e-12);
808        assert_close(r.flow[0], 2.0, 1e-12);
809        assert_close(r.flow[1], 2.0, 1e-12);
810        assert_close(r.flow[2], 2.0, 1e-12);
811        validate_flow(&g, &r, Some(&cap));
812    }
813
814    #[test]
815    fn max_flow_classic_textbook() {
816        let mut g = Graph::new(6, true).unwrap();
817        g.add_edge(0, 1).unwrap();
818        g.add_edge(0, 2).unwrap();
819        g.add_edge(1, 3).unwrap();
820        g.add_edge(2, 1).unwrap();
821        g.add_edge(2, 4).unwrap();
822        g.add_edge(3, 2).unwrap();
823        g.add_edge(3, 5).unwrap();
824        g.add_edge(4, 3).unwrap();
825        g.add_edge(4, 5).unwrap();
826        let cap = vec![16.0, 13.0, 12.0, 4.0, 14.0, 9.0, 20.0, 7.0, 4.0];
827        let r = max_flow(&g, 0, 5, Some(&cap)).unwrap();
828        assert_close(r.value, 23.0, 1e-12);
829        validate_flow(&g, &r, Some(&cap));
830    }
831
832    #[test]
833    fn max_flow_isolated_endpoints() {
834        let g = Graph::with_vertices(4);
835        let r = max_flow(&g, 0, 3, None).unwrap();
836        assert_close(r.value, 0.0, 1e-12);
837        assert!(r.flow.iter().all(|&f| f.abs() < 1e-12));
838        assert!(r.cut.is_empty());
839    }
840
841    #[test]
842    fn max_flow_single_edge() {
843        let mut g = Graph::new(2, true).unwrap();
844        g.add_edge(0, 1).unwrap();
845        let r = max_flow(&g, 0, 1, None).unwrap();
846        assert_close(r.value, 1.0, 1e-12);
847        assert_close(r.flow[0], 1.0, 1e-12);
848        validate_flow(&g, &r, None);
849    }
850
851    #[test]
852    fn max_flow_undirected() {
853        let mut g = Graph::with_vertices(4);
854        g.add_edge(0, 1).unwrap();
855        g.add_edge(0, 2).unwrap();
856        g.add_edge(1, 3).unwrap();
857        g.add_edge(2, 3).unwrap();
858        let r = max_flow(&g, 0, 3, None).unwrap();
859        assert_close(r.value, 2.0, 1e-12);
860        assert_eq!(r.flow.len(), 4);
861        validate_flow(&g, &r, None);
862    }
863
864    #[test]
865    fn max_flow_value_matches_full() {
866        let mut g = Graph::new(5, true).unwrap();
867        for (s, t) in [(0u32, 1u32), (0, 2), (1, 3), (2, 3), (3, 4), (1, 4)] {
868            g.add_edge(s, t).unwrap();
869        }
870        let caps = [3.0, 5.0, 2.0, 4.0, 6.0, 1.0];
871        let scalar = max_flow_value(&g, 0, 4, Some(&caps)).unwrap();
872        let full = max_flow(&g, 0, 4, Some(&caps)).unwrap();
873        assert_close(scalar, full.value, 1e-12);
874        validate_flow(&g, &full, Some(&caps));
875    }
876}
877
878#[cfg(all(test, feature = "proptest-harness"))]
879mod proptest_tests {
880    use super::*;
881    use proptest::prelude::*;
882
883    /// Independent reference: plain Edmonds-Karp BFS-augmentation on
884    /// the same residual structure. O(V·E²) — perfectly fine for the
885    /// tiny proptest graphs but algorithmically distinct from Dinic's
886    /// blocking-flow strategy, so agreement on the scalar value
887    /// cross-validates both implementations.
888    fn edmonds_karp(graph: &Graph, source: u32, target: u32, cap: &[f64]) -> f64 {
889        let net = Network::build(graph, Some(cap)).expect("net builds");
890        let n = net.n;
891        let mut residual = net.cap.clone();
892        let mut total = 0.0_f64;
893        loop {
894            let mut parent_arc = vec![u32::MAX; n];
895            let mut visited = vec![false; n];
896            visited[source as usize] = true;
897            let mut queue = vec![source];
898            let mut head = 0_usize;
899            let mut found = false;
900            'bfs: while head < queue.len() {
901                let v = queue[head] as usize;
902                head += 1;
903                for &a in &net.arcs_out[v] {
904                    let a_us = a as usize;
905                    let w = net.head[a_us] as usize;
906                    if !visited[w] && residual[a_us] > 0.0 {
907                        visited[w] = true;
908                        parent_arc[w] = a;
909                        if w == target as usize {
910                            found = true;
911                            break 'bfs;
912                        }
913                        queue.push(w as u32);
914                    }
915                }
916            }
917            if !found {
918                break;
919            }
920            let mut bottleneck = f64::INFINITY;
921            let mut cur = target as usize;
922            while cur != source as usize {
923                let a = parent_arc[cur] as usize;
924                if residual[a] < bottleneck {
925                    bottleneck = residual[a];
926                }
927                cur = net.head[a ^ 1] as usize;
928            }
929            let mut cur = target as usize;
930            while cur != source as usize {
931                let a = parent_arc[cur] as usize;
932                residual[a] -= bottleneck;
933                residual[a ^ 1] += bottleneck;
934                cur = net.head[a ^ 1] as usize;
935            }
936            total += bottleneck;
937        }
938        total
939    }
940
941    proptest! {
942        #![proptest_config(ProptestConfig::with_cases(80))]
943
944        #[test]
945        fn matches_edmonds_karp_directed(
946            n in 2u32..8,
947            seed in any::<u64>(),
948        ) {
949            // Build a random directed graph + capacities deterministically from `seed`.
950            let mut rng_state = seed | 1;
951            let mut next = || {
952                rng_state ^= rng_state << 13;
953                rng_state ^= rng_state >> 7;
954                rng_state ^= rng_state << 17;
955                rng_state
956            };
957            let mut g = Graph::new(n, true).unwrap();
958            let edge_count = next() as u32 % (n * 3 + 1);
959            let mut caps = Vec::with_capacity(edge_count as usize);
960            for _ in 0..edge_count {
961                let u = (next() as u32) % n;
962                let v = (next() as u32) % n;
963                g.add_edge(u, v).unwrap();
964                let c = f64::from((next() as u32 % 16) + 1);
965                caps.push(c);
966            }
967            let source = 0;
968            let target = n - 1;
969            if source == target {
970                return Ok(());
971            }
972            let dinic = max_flow_value(&g, source, target, Some(&caps)).unwrap();
973            let ref_val = edmonds_karp(&g, source, target, &caps);
974            prop_assert!(
975                (dinic - ref_val).abs() < 1e-9,
976                "dinic={dinic} ref={ref_val}"
977            );
978        }
979
980        #[test]
981        fn matches_edmonds_karp_undirected(
982            n in 2u32..8,
983            seed in any::<u64>(),
984        ) {
985            let mut rng_state = seed | 1;
986            let mut next = || {
987                rng_state ^= rng_state << 13;
988                rng_state ^= rng_state >> 7;
989                rng_state ^= rng_state << 17;
990                rng_state
991            };
992            let mut g = Graph::with_vertices(n);
993            let edge_count = next() as u32 % (n * 3 + 1);
994            let mut caps = Vec::with_capacity(edge_count as usize);
995            for _ in 0..edge_count {
996                let u = (next() as u32) % n;
997                let v = (next() as u32) % n;
998                g.add_edge(u, v).unwrap();
999                let c = f64::from((next() as u32 % 16) + 1);
1000                caps.push(c);
1001            }
1002            let source = 0;
1003            let target = n - 1;
1004            if source == target {
1005                return Ok(());
1006            }
1007            let dinic = max_flow_value(&g, source, target, Some(&caps)).unwrap();
1008            let ref_val = edmonds_karp(&g, source, target, &caps);
1009            prop_assert!(
1010                (dinic - ref_val).abs() < 1e-9,
1011                "dinic={dinic} ref={ref_val}"
1012            );
1013        }
1014    }
1015}