Skip to main content

oxicuda_graphalg/max_flow/
parametric_maxflow.rs

1//! Parametric maximum flow (Gallo-Grigoriadis-Tarjan 1989 / Hochbaum 2008).
2//!
3//! Solves a *family* of `s`-`t` maximum-flow problems in which some arc
4//! capacities vary monotonically with a scalar parameter `lambda`. Each arc
5//! capacity is modelled as an affine function `cap(lambda) = base + slope * lambda`
6//! (clamped at `0`). The Gallo-Grigoriadis-Tarjan (GGT) *monotone* structure is
7//! enforced:
8//!
9//! * arcs leaving the source (`from == source`) have **non-decreasing** capacity
10//!   (`slope >= 0`),
11//! * arcs entering the sink (`to == sink`) have **non-increasing** capacity
12//!   (`slope <= 0`),
13//! * every other arc has a **constant** capacity (`slope == 0`).
14//!
15//! Under this structure the minimal source side `S(lambda)` of the minimum cut is
16//! a *non-decreasing* (nested) function of `lambda`: for `l1 <= l2` we have
17//! `S(l1) ⊆ S(l2)`. This module exposes
18//!
19//! * [`ParametricMaxFlow::solve_at`] — a single `(value, source-set)` solve at one
20//!   `lambda`, reusing the crate's Edmonds-Karp / residual-reachability min-cut,
21//! * [`ParametricMaxFlow::solve_grid`] — the whole family over a `lambda` grid,
22//! * [`ParametricMaxFlow::find_breakpoints`] — the GGT divide-and-conquer
23//!   "slicing" that locates every `lambda` at which the minimal min-cut changes,
24//!   exploiting the nested-cut structure (one max-flow solve per slice).
25//!
26//! The per-`lambda` solves are exact single-parameter max-flows, so they agree
27//! with an independent solve at every `lambda`.
28
29use crate::error::{GraphalgError, GraphalgResult};
30use crate::max_flow::edmonds_karp::FlowNetwork;
31use crate::max_flow::min_cut::min_cut_from_max_flow;
32
33/// Numerical tolerance for capacity / slack comparisons.
34const EPS: f64 = 1.0e-9;
35
36/// An arc whose capacity is affine in the parameter: `cap(lambda) = base + slope * lambda`.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct ParametricArc {
39    /// Tail endpoint.
40    pub from: usize,
41    /// Head endpoint.
42    pub to: usize,
43    /// Constant part of the capacity.
44    pub base: f64,
45    /// Per-unit-`lambda` part of the capacity.
46    pub slope: f64,
47}
48
49/// Result of one single-`lambda` parametric solve.
50#[derive(Debug, Clone, PartialEq)]
51pub struct ParametricSolution {
52    /// The parameter value.
53    pub lambda: f64,
54    /// Maximum flow value (= minimum cut capacity) at this `lambda`.
55    pub flow_value: f64,
56    /// Minimal source side of the minimum cut (residual-reachable from the
57    /// source), sorted ascending. Nested/monotone in `lambda`.
58    pub source_set: Vec<usize>,
59}
60
61/// A `lambda` value at which the optimal minimal min-cut source side changes.
62#[derive(Debug, Clone, PartialEq)]
63pub struct ParametricBreakpoint {
64    /// Parameter value of the transition.
65    pub lambda: f64,
66    /// Minimal source side that is optimal immediately *above* this breakpoint
67    /// (until the next breakpoint), sorted ascending.
68    pub source_set: Vec<usize>,
69}
70
71/// A parametric `s`-`t` flow network with affine, GGT-monotone arc capacities.
72#[derive(Debug, Clone)]
73pub struct ParametricMaxFlow {
74    n: usize,
75    source: usize,
76    sink: usize,
77    arcs: Vec<ParametricArc>,
78}
79
80impl ParametricMaxFlow {
81    /// Create an empty parametric network on `n` nodes with the given source/sink.
82    pub fn new(n: usize, source: usize, sink: usize) -> GraphalgResult<Self> {
83        if n == 0 {
84            return Err(GraphalgError::EmptyInput);
85        }
86        if source >= n || sink >= n {
87            return Err(GraphalgError::SourceOutOfRange {
88                node: source.max(sink),
89                n,
90            });
91        }
92        if source == sink {
93            return Err(GraphalgError::InvalidParameter(
94                "source and sink must differ".to_string(),
95            ));
96        }
97        Ok(Self {
98            n,
99            source,
100            sink,
101            arcs: Vec::new(),
102        })
103    }
104
105    /// Number of nodes.
106    pub fn num_nodes(&self) -> usize {
107        self.n
108    }
109
110    /// Source node.
111    pub fn source(&self) -> usize {
112        self.source
113    }
114
115    /// Sink node.
116    pub fn sink(&self) -> usize {
117        self.sink
118    }
119
120    /// Arcs of the network.
121    pub fn arcs(&self) -> &[ParametricArc] {
122        &self.arcs
123    }
124
125    /// Add a parametric arc `from -> to` with capacity `base + slope * lambda`.
126    ///
127    /// Enforces the GGT monotone structure (see module docs) and rejects
128    /// out-of-range endpoints and non-finite coefficients.
129    pub fn add_arc(&mut self, from: usize, to: usize, base: f64, slope: f64) -> GraphalgResult<()> {
130        if from >= self.n || to >= self.n {
131            return Err(GraphalgError::IndexOutOfBounds {
132                index: from.max(to),
133                len: self.n,
134            });
135        }
136        if from == to {
137            return Err(GraphalgError::InvalidParameter(format!(
138                "self-loop arc at node {from} is not allowed"
139            )));
140        }
141        if !base.is_finite() || !slope.is_finite() {
142            return Err(GraphalgError::InvalidEdgeWeight(format!(
143                "arc ({from},{to}) has non-finite coefficients base={base} slope={slope}"
144            )));
145        }
146        let is_source_arc = from == self.source;
147        let is_sink_arc = to == self.sink;
148        if is_source_arc && slope < -EPS {
149            return Err(GraphalgError::InvalidConfiguration(format!(
150                "source arc ({from},{to}) must be non-decreasing in lambda (slope {slope} < 0)"
151            )));
152        }
153        if is_sink_arc && slope > EPS {
154            return Err(GraphalgError::InvalidConfiguration(format!(
155                "sink arc ({from},{to}) must be non-increasing in lambda (slope {slope} > 0)"
156            )));
157        }
158        if !is_source_arc && !is_sink_arc && slope.abs() > EPS {
159            return Err(GraphalgError::InvalidConfiguration(format!(
160                "interior arc ({from},{to}) must have constant capacity (slope {slope} != 0)"
161            )));
162        }
163        self.arcs.push(ParametricArc {
164            from,
165            to,
166            base,
167            slope,
168        });
169        Ok(())
170    }
171
172    /// Build a parametric network from parallel coefficient slices.
173    ///
174    /// All slices must share the same length, otherwise a [`GraphalgError::DimensionMismatch`]
175    /// is returned. Each tuple position `i` describes arc `from[i] -> to[i]` with
176    /// capacity `base[i] + slope[i] * lambda`.
177    pub fn from_linear_capacities(
178        n: usize,
179        source: usize,
180        sink: usize,
181        from: &[usize],
182        to: &[usize],
183        base: &[f64],
184        slope: &[f64],
185    ) -> GraphalgResult<Self> {
186        let m = from.len();
187        if to.len() != m {
188            return Err(GraphalgError::DimensionMismatch { a: m, b: to.len() });
189        }
190        if base.len() != m {
191            return Err(GraphalgError::DimensionMismatch {
192                a: m,
193                b: base.len(),
194            });
195        }
196        if slope.len() != m {
197            return Err(GraphalgError::DimensionMismatch {
198                a: m,
199                b: slope.len(),
200            });
201        }
202        let mut net = Self::new(n, source, sink)?;
203        for i in 0..m {
204            net.add_arc(from[i], to[i], base[i], slope[i])?;
205        }
206        Ok(net)
207    }
208
209    /// Capacity of an arc at a given `lambda`, clamped to be non-negative.
210    fn arc_capacity(arc: &ParametricArc, lambda: f64) -> f64 {
211        (arc.base + arc.slope * lambda).max(0.0)
212    }
213
214    /// Materialise a concrete [`FlowNetwork`] at the given `lambda`.
215    fn build_network(&self, lambda: f64) -> GraphalgResult<FlowNetwork> {
216        if !lambda.is_finite() {
217            return Err(GraphalgError::InvalidParameter(format!(
218                "lambda must be finite, got {lambda}"
219            )));
220        }
221        let mut net = FlowNetwork::new(self.n);
222        for arc in &self.arcs {
223            let cap = Self::arc_capacity(arc, lambda);
224            if cap > 0.0 {
225                net.add_edge(arc.from, arc.to, cap)?;
226            }
227        }
228        Ok(net)
229    }
230
231    /// Solve the maximum flow at a single `lambda`, returning the flow value and
232    /// the minimal source side of the minimum cut (sorted ascending).
233    pub fn solve_at(&self, lambda: f64) -> GraphalgResult<ParametricSolution> {
234        let net = self.build_network(lambda)?;
235        let cut = min_cut_from_max_flow(&net, self.source, self.sink)?;
236        let mut source_set = cut.source_side;
237        source_set.sort_unstable();
238        Ok(ParametricSolution {
239            lambda,
240            flow_value: cut.value,
241            source_set,
242        })
243    }
244
245    /// Solve the whole family over the provided `lambda` grid.
246    ///
247    /// Each entry is an exact independent single-parameter max-flow solve. When
248    /// the grid is non-decreasing the returned `source_set`s are nested.
249    pub fn solve_grid(&self, lambdas: &[f64]) -> GraphalgResult<Vec<ParametricSolution>> {
250        let mut out = Vec::with_capacity(lambdas.len());
251        for &lambda in lambdas {
252            out.push(self.solve_at(lambda)?);
253        }
254        Ok(out)
255    }
256
257    /// Affine cut-capacity line `(intercept, slope)` of a given source side
258    /// `S`: the sum over arcs crossing from `S` to its complement of
259    /// `(base, slope)`. Uses the raw (un-clamped) coefficients, valid wherever
260    /// the capacities stay non-negative.
261    fn cut_line(&self, in_source: &[bool]) -> (f64, f64) {
262        let mut intercept = 0.0;
263        let mut slope = 0.0;
264        for arc in &self.arcs {
265            if in_source[arc.from] && !in_source[arc.to] {
266                intercept += arc.base;
267                slope += arc.slope;
268            }
269        }
270        (intercept, slope)
271    }
272
273    /// Build a membership mask from a sorted source-set.
274    fn membership(&self, source_set: &[usize]) -> Vec<bool> {
275        let mut mask = vec![false; self.n];
276        for &v in source_set {
277            mask[v] = true;
278        }
279        mask
280    }
281
282    /// Locate every `lambda` in `[lambda_min, lambda_max]` at which the minimal
283    /// minimum-cut source side changes, via Gallo-Grigoriadis-Tarjan slicing.
284    ///
285    /// Returns the breakpoints sorted by ascending `lambda`. Each breakpoint
286    /// carries the source side optimal immediately above it. Assumes the affine
287    /// capacities stay non-negative across the interval (the standard GGT
288    /// regime); otherwise the breakpoint geometry uses the un-clamped lines.
289    pub fn find_breakpoints(
290        &self,
291        lambda_min: f64,
292        lambda_max: f64,
293    ) -> GraphalgResult<Vec<ParametricBreakpoint>> {
294        if !lambda_min.is_finite() || !lambda_max.is_finite() {
295            return Err(GraphalgError::InvalidParameter(
296                "lambda bounds must be finite".to_string(),
297            ));
298        }
299        if lambda_min > lambda_max {
300            return Err(GraphalgError::InvalidParameter(format!(
301                "lambda_min ({lambda_min}) must not exceed lambda_max ({lambda_max})"
302            )));
303        }
304        let lo = self.slice_end(lambda_min)?;
305        let hi = self.slice_end(lambda_max)?;
306        let mut out = Vec::new();
307        let budget = 8 * self.n + 16;
308        self.slice(&lo, &hi, budget, &mut out)?;
309        out.sort_by(|a, b| {
310            a.lambda
311                .partial_cmp(&b.lambda)
312                .unwrap_or(std::cmp::Ordering::Equal)
313        });
314        Ok(out)
315    }
316
317    /// Solve at `lambda` and package the source side together with its cut line.
318    fn slice_end(&self, lambda: f64) -> GraphalgResult<SliceEnd> {
319        let sol = self.solve_at(lambda)?;
320        let mask = self.membership(&sol.source_set);
321        let line = self.cut_line(&mask);
322        Ok(SliceEnd {
323            lambda,
324            flow_value: sol.flow_value,
325            source_set: sol.source_set,
326            line,
327        })
328    }
329
330    fn slice(
331        &self,
332        lo: &SliceEnd,
333        hi: &SliceEnd,
334        budget: usize,
335        out: &mut Vec<ParametricBreakpoint>,
336    ) -> GraphalgResult<()> {
337        if budget == 0 {
338            return Err(GraphalgError::NotConverged {
339                iter: 8 * self.n + 16,
340            });
341        }
342        if lo.source_set == hi.source_set {
343            return Ok(());
344        }
345        let (a_lo, b_lo) = lo.line;
346        let (a_hi, b_hi) = hi.line;
347        let denom = b_lo - b_hi;
348        if denom.abs() <= EPS {
349            // Parallel cut lines with differing sets: record the transition at the
350            // upper endpoint of the slice; the upper set dominates above it.
351            out.push(ParametricBreakpoint {
352                lambda: hi.lambda,
353                source_set: hi.source_set.clone(),
354            });
355            return Ok(());
356        }
357        let lam_star = (a_hi - a_lo) / denom;
358        if lam_star <= lo.lambda + EPS || lam_star >= hi.lambda - EPS {
359            // Intersection falls outside the open interval: the two cuts already
360            // describe the cut structure here; record the transition at lam_star
361            // clamped into the interval.
362            let clamped = lam_star.clamp(lo.lambda, hi.lambda);
363            out.push(ParametricBreakpoint {
364                lambda: clamped,
365                source_set: hi.source_set.clone(),
366            });
367            return Ok(());
368        }
369        let mid = self.slice_end(lam_star)?;
370        let y_int = a_lo + b_lo * lam_star;
371        if mid.flow_value >= y_int - EPS {
372            // No cut beats the intersection of the two bounding lines: lam_star is
373            // a genuine breakpoint between lo and hi source sides.
374            out.push(ParametricBreakpoint {
375                lambda: lam_star,
376                source_set: hi.source_set.clone(),
377            });
378            Ok(())
379        } else {
380            self.slice(lo, &mid, budget - 1, out)?;
381            self.slice(&mid, hi, budget - 1, out)?;
382            Ok(())
383        }
384    }
385}
386
387/// One endpoint of a GGT slice: a solved `lambda` with its source side and cut line.
388#[derive(Debug, Clone)]
389struct SliceEnd {
390    lambda: f64,
391    flow_value: f64,
392    source_set: Vec<usize>,
393    line: (f64, f64),
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    fn approx(a: f64, b: f64) -> bool {
401        (a - b).abs() < 1e-7
402    }
403
404    /// Independent single max-flow at a given lambda (oracle), via Dinic.
405    fn independent_flow(net: &ParametricMaxFlow, lambda: f64) -> f64 {
406        let fnet = net.build_network(lambda).expect("build");
407        crate::max_flow::dinic::dinic_max_flow(&fnet, net.source(), net.sink()).expect("dinic")
408    }
409
410    /// Build the canonical nested example: s=0, a=1, b=2, t=3.
411    /// s->a, s->b capacities = lambda (source arcs, slope 1); a->t, b->t = 1.
412    fn nested_net() -> ParametricMaxFlow {
413        let mut net = ParametricMaxFlow::new(4, 0, 3).expect("new");
414        net.add_arc(0, 1, 0.0, 1.0).expect("s->a");
415        net.add_arc(0, 2, 0.0, 1.0).expect("s->b");
416        net.add_arc(1, 3, 1.0, 0.0).expect("a->t");
417        net.add_arc(2, 3, 1.0, 0.0).expect("b->t");
418        net
419    }
420
421    #[test]
422    fn grid_matches_independent_solves() {
423        let net = nested_net();
424        for k in 0..=20 {
425            let lambda = k as f64 * 0.2;
426            let sol = net.solve_at(lambda).expect("solve");
427            let indep = independent_flow(&net, lambda);
428            assert!(
429                approx(sol.flow_value, indep),
430                "lambda={lambda} parametric={} independent={indep}",
431                sol.flow_value
432            );
433        }
434    }
435
436    #[test]
437    fn source_sets_are_nested() {
438        let net = nested_net();
439        let lambdas: Vec<f64> = (0..=20).map(|k| k as f64 * 0.25).collect();
440        let sols = net.solve_grid(&lambdas).expect("grid");
441        for w in sols.windows(2) {
442            let prev: std::collections::BTreeSet<usize> = w[0].source_set.iter().copied().collect();
443            let next: std::collections::BTreeSet<usize> = w[1].source_set.iter().copied().collect();
444            assert!(
445                prev.is_subset(&next),
446                "not nested at lambda {} -> {}: {:?} vs {:?}",
447                w[0].lambda,
448                w[1].lambda,
449                w[0].source_set,
450                w[1].source_set
451            );
452        }
453        // sanity: low lambda -> just the source; high lambda -> source side grows.
454        assert_eq!(sols.first().expect("first").source_set, vec![0]);
455        assert!(sols.last().expect("last").source_set.len() >= 3);
456    }
457
458    #[test]
459    fn constant_capacities_give_constant_flow() {
460        let mut net = ParametricMaxFlow::new(4, 0, 3).expect("new");
461        net.add_arc(0, 1, 3.0, 0.0).expect("e");
462        net.add_arc(0, 2, 2.0, 0.0).expect("e");
463        net.add_arc(1, 3, 3.0, 0.0).expect("e");
464        net.add_arc(2, 3, 2.0, 0.0).expect("e");
465        let lambdas: Vec<f64> = (-5..=5).map(|k| k as f64).collect();
466        let sols = net.solve_grid(&lambdas).expect("grid");
467        for s in &sols {
468            assert!(
469                approx(s.flow_value, 5.0),
470                "flow {} != 5 at {}",
471                s.flow_value,
472                s.lambda
473            );
474        }
475        // identical source sets across all lambda
476        let first = &sols[0].source_set;
477        for s in &sols {
478            assert_eq!(&s.source_set, first);
479        }
480    }
481
482    #[test]
483    fn analytic_two_node_min_formula() {
484        // s=0 -> a=1 with cap = lambda; a=1 -> t=2 with cap = C.
485        // max flow = min(max(0, lambda), C).
486        let other = 2.5;
487        let mut net = ParametricMaxFlow::new(3, 0, 2).expect("new");
488        net.add_arc(0, 1, 0.0, 1.0).expect("s->a");
489        net.add_arc(1, 2, other, 0.0).expect("a->t");
490        for k in 0..=12 {
491            let lambda = k as f64 * 0.5;
492            let sol = net.solve_at(lambda).expect("solve");
493            let expected = lambda.max(0.0).min(other);
494            assert!(
495                approx(sol.flow_value, expected),
496                "lambda={lambda}: got {} expected {expected}",
497                sol.flow_value
498            );
499        }
500    }
501
502    #[test]
503    fn breakpoint_detected_for_nested_example() {
504        let net = nested_net();
505        // flow = min(2 lambda, 2): breakpoint at lambda = 1 where the cut changes
506        // from the source side {0} to {0,1,2}.
507        let bps = net.find_breakpoints(0.0, 3.0).expect("breakpoints");
508        assert!(!bps.is_empty(), "expected at least one breakpoint");
509        let near_one = bps.iter().any(|b| (b.lambda - 1.0).abs() < 1e-4);
510        assert!(near_one, "expected a breakpoint near lambda=1, got {bps:?}");
511    }
512
513    #[test]
514    fn rejects_non_monotone_arcs() {
515        let mut net = ParametricMaxFlow::new(4, 0, 3).expect("new");
516        // source arc with negative slope -> error
517        assert!(net.add_arc(0, 1, 1.0, -1.0).is_err());
518        // sink arc with positive slope -> error
519        assert!(net.add_arc(1, 3, 1.0, 1.0).is_err());
520        // interior arc with non-zero slope -> error
521        assert!(net.add_arc(1, 2, 1.0, 0.5).is_err());
522    }
523
524    #[test]
525    fn rejects_bad_construction() {
526        assert!(ParametricMaxFlow::new(0, 0, 0).is_err());
527        assert!(ParametricMaxFlow::new(3, 0, 0).is_err());
528        assert!(ParametricMaxFlow::new(3, 5, 1).is_err());
529        // mismatched capacity dimensions
530        let from = [0usize, 1];
531        let to = [1usize, 2];
532        let base = [1.0];
533        let slope = [0.0, 0.0];
534        assert!(
535            ParametricMaxFlow::from_linear_capacities(3, 0, 2, &from, &to, &base, &slope).is_err()
536        );
537    }
538
539    #[test]
540    fn from_linear_capacities_builds() {
541        let from = [0usize, 0, 1, 2];
542        let to = [1usize, 2, 3, 3];
543        let base = [0.0, 0.0, 1.0, 1.0];
544        let slope = [1.0, 1.0, 0.0, 0.0];
545        let net = ParametricMaxFlow::from_linear_capacities(4, 0, 3, &from, &to, &base, &slope)
546            .expect("build");
547        assert_eq!(net.arcs().len(), 4);
548        let sol = net.solve_at(0.5).expect("solve");
549        assert!(approx(sol.flow_value, 1.0));
550    }
551}