Skip to main content

problemreductions/models/graph/
minimum_cost_maximum_flow.rs

1//! Minimum-Cost Maximum-Flow problem implementation.
2//!
3//! Given a directed graph `G = (V, A)` with arc capacities `c(a)` and
4//! arc costs `cost(a)`, a source `s`, and a sink `t`, find a flow `f`
5//! that
6//!
7//! 1. first maximizes the flow value `|f| = sum_{a in delta^+(s)} f(a)
8//!    - sum_{a in delta^-(s)} f(a)`, and then
9//! 2. among all maximum-value flows, minimizes the total arc cost
10//!    `sum_a cost(a) * f(a)`.
11//!
12//! The objective is lexicographic: maximum-value first, ties broken by
13//! lower total cost.
14//!
15//! # Integral-flow restriction
16//!
17//! The mathematical CellRouter formulation in issue #1029 uses
18//! continuous flows `f: A -> R_{>= 0}`, but this model's registered reference
19//! solver uses a finite Cartesian space. Following the same precedent as
20//! [`MinimumEdgeCostFlow`](super::MinimumEdgeCostFlow) (see
21//! `src/models/graph/minimum_edge_cost_flow.rs`), we therefore restrict
22//! to **integer** flows: each variable `f(a)` ranges over
23//! `{0, 1, ..., c(a)}`. When capacities and costs are integral, the
24//! standard minimum-cost flow theory (see e.g. the MIT 6.854 notes,
25//! Ahuja-Magnanti-Orlin) guarantees that an integral optimum exists, so
26//! this restriction does not change the optimal value on integer
27//! instances.
28//!
29//! # Lexicographic encoding
30//!
31//! The lexicographic objective `(max |f|, min cost(f))` is encoded as a
32//! single scalar score
33//!
34//! `score = M * (max_possible_flow - |f|) + cost(f)`
35//!
36//! where `M = sum_e c(e) * cost(e) + 1` strictly exceeds any feasible
37//! cost. Minimizing this scalar therefore minimizes
38//! `max_possible_flow - |f|` first (i.e. maximizes `|f|`), then breaks
39//! ties by `cost(f)`. The optimum is always non-negative, and a smaller
40//! score is strictly better in the lex order.
41
42use crate::registry::{ConstructionError, FieldInfo, ProblemSchemaEntry};
43use crate::topology::DirectedGraph;
44use crate::traits::Problem;
45use serde::{Deserialize, Serialize};
46
47inventory::submit! {
48    ProblemSchemaEntry {
49        name: "MinimumCostMaximumFlow",
50        display_name: "Minimum-Cost Maximum-Flow",
51        aliases: &["MCMF"],
52        dimensions: &[],
53        category: crate::registry::ProblemCategory::Graph,
54        module_path: module_path!(),
55        description: "Integral flow that lexicographically maximizes value then minimizes total arc cost",
56        fields: &[
57            FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed graph G = (V, A)" },
58            FieldInfo { name: "source", type_name: "usize", description: "Source vertex s" },
59            FieldInfo { name: "sink", type_name: "usize", description: "Sink vertex t" },
60            FieldInfo { name: "capacities", type_name: "Vec<i64>", description: "Arc capacity c(a) in graph arc order (non-negative)" },
61            FieldInfo { name: "costs", type_name: "Vec<i64>", description: "Arc cost cost(a) in graph arc order (non-negative)" },
62        ],
63    }
64}
65
66/// Minimum-Cost Maximum-Flow problem.
67///
68/// # Variables
69///
70/// `|A|` variables: variable `a` ranges over `{0, ..., c(a)}`
71/// representing the integral flow on arc `a`.
72///
73/// # Example
74///
75/// ```
76/// use problemreductions::models::graph::MinimumCostMaximumFlow;
77/// use problemreductions::topology::DirectedGraph;
78/// use problemreductions::{Problem, BruteForce};
79///
80/// // Diamond network from the canonical example.
81/// let graph = DirectedGraph::new(4, vec![
82///     (0, 1), (0, 2), (1, 2), (1, 3), (2, 3),
83/// ]);
84/// let problem = MinimumCostMaximumFlow::new(
85///     graph,
86///     0, 3,
87///     vec![2, 1, 1, 1, 2], // capacities
88///     vec![1, 0, 0, 1, 2], // costs
89/// );
90/// let solver = BruteForce::new();
91/// let witness = solver.solve(&problem).unwrap().unwrap();
92/// // Optimal flow has value 3 and cost 7.
93/// assert_eq!(problem.flow_value(&witness).unwrap(), 3);
94/// assert_eq!(problem.total_cost(&witness).unwrap(), 7);
95/// ```
96#[derive(Debug, Clone, Serialize)]
97pub struct MinimumCostMaximumFlow {
98    /// The directed graph G = (V, A).
99    graph: DirectedGraph,
100    /// Source vertex s.
101    source: usize,
102    /// Sink vertex t.
103    sink: usize,
104    /// Capacity c(a) for each arc.
105    capacities: Vec<i64>,
106    /// Cost cost(a) for each arc.
107    costs: Vec<i64>,
108}
109
110#[derive(Deserialize)]
111struct MinimumCostMaximumFlowSerde {
112    graph: DirectedGraph,
113    source: usize,
114    sink: usize,
115    capacities: Vec<i64>,
116    costs: Vec<i64>,
117}
118
119impl TryFrom<MinimumCostMaximumFlowSerde> for MinimumCostMaximumFlow {
120    type Error = ConstructionError;
121
122    fn try_from(value: MinimumCostMaximumFlowSerde) -> Result<Self, Self::Error> {
123        Self::try_new(
124            value.graph,
125            value.source,
126            value.sink,
127            value.capacities,
128            value.costs,
129        )
130    }
131}
132
133impl<'de> Deserialize<'de> for MinimumCostMaximumFlow {
134    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135    where
136        D: serde::Deserializer<'de>,
137    {
138        MinimumCostMaximumFlowSerde::deserialize(deserializer)?
139            .try_into()
140            .map_err(serde::de::Error::custom)
141    }
142}
143
144impl MinimumCostMaximumFlow {
145    /// Create a new Minimum-Cost Maximum-Flow problem.
146    ///
147    /// # Panics
148    ///
149    /// Panics if any of the following holds:
150    /// - `capacities.len() != graph.num_arcs()`
151    /// - `costs.len() != graph.num_arcs()`
152    /// - `source >= graph.num_vertices()`
153    /// - `sink >= graph.num_vertices()`
154    /// - `source == sink`
155    /// - Any capacity is negative
156    /// - Any cost is negative
157    pub fn new(
158        graph: DirectedGraph,
159        source: usize,
160        sink: usize,
161        capacities: Vec<i64>,
162        costs: Vec<i64>,
163    ) -> Self {
164        Self::try_new(graph, source, sink, capacities, costs)
165            .unwrap_or_else(|error| panic!("{error}"))
166    }
167
168    fn try_new(
169        graph: DirectedGraph,
170        source: usize,
171        sink: usize,
172        capacities: Vec<i64>,
173        costs: Vec<i64>,
174    ) -> Result<Self, ConstructionError> {
175        let n = graph.num_vertices();
176        let m = graph.num_arcs();
177        if capacities.len() != m {
178            return Err(format!(
179                "capacities length ({}) must match num_arcs ({m})",
180                capacities.len()
181            )
182            .into());
183        }
184        if costs.len() != m {
185            return Err(format!("costs length ({}) must match num_arcs ({m})", costs.len()).into());
186        }
187        if source >= n {
188            return Err(format!("source ({source}) >= num_vertices ({n})").into());
189        }
190        if sink >= n {
191            return Err(format!("sink ({sink}) >= num_vertices ({n})").into());
192        }
193        if source == sink {
194            return Err("source and sink must be distinct".into());
195        }
196        if let Some((index, capacity)) = capacities
197            .iter()
198            .enumerate()
199            .find(|(_, capacity)| **capacity < 0)
200        {
201            return Err(format!("capacity[{index}] = {capacity} is negative").into());
202        }
203        if let Some((index, cost)) = costs.iter().enumerate().find(|(_, cost)| **cost < 0) {
204            return Err(format!("cost[{index}] = {cost} is negative").into());
205        }
206        Ok(Self {
207            graph,
208            source,
209            sink,
210            capacities,
211            costs,
212        })
213    }
214
215    /// Get a reference to the underlying directed graph.
216    pub fn graph(&self) -> &DirectedGraph {
217        &self.graph
218    }
219
220    /// Get the source vertex.
221    pub fn source(&self) -> usize {
222        self.source
223    }
224
225    /// Get the sink vertex.
226    pub fn sink(&self) -> usize {
227        self.sink
228    }
229
230    /// Get a reference to the arc capacities.
231    pub fn capacities(&self) -> &[i64] {
232        &self.capacities
233    }
234
235    /// Get a reference to the arc costs.
236    pub fn costs(&self) -> &[i64] {
237        &self.costs
238    }
239
240    /// Get the number of vertices `|V|`.
241    pub fn num_vertices(&self) -> usize {
242        self.graph.num_vertices()
243    }
244
245    /// Get the number of arcs `|A|`.
246    pub fn num_arcs(&self) -> usize {
247        self.graph.num_arcs()
248    }
249
250    /// Check whether a flow assignment is feasible.
251    ///
252    /// A flow is feasible iff
253    /// 1. `config.len() == num_arcs`,
254    /// 2. each `0 <= f(a) <= c(a)`, and
255    /// 3. flow is conserved at every non-terminal vertex.
256    pub fn is_feasible(&self, config: &[usize]) -> Result<bool, crate::traits::EvaluationError> {
257        let m = self.graph.num_arcs();
258        if config.len() != m {
259            return Ok(false);
260        }
261        // (1) Capacity constraints
262        for (flow, cap) in config.iter().zip(self.capacities.iter()) {
263            let flow = i64::try_from(*flow).map_err(|_| {
264                crate::traits::EvaluationError::IntegerOverflow(
265                    "converting maximum-flow configuration value".to_string(),
266                )
267            })?;
268            if flow > *cap {
269                return Ok(false);
270            }
271        }
272        // (2) Flow conservation at non-terminal vertices
273        let n = self.graph.num_vertices();
274        let mut balance = vec![0_i64; n];
275        for (a, &(u, v)) in self.graph.arcs().iter().enumerate() {
276            let flow = i64::try_from(config[a]).map_err(|_| {
277                crate::traits::EvaluationError::IntegerOverflow(
278                    "converting maximum-flow configuration value".to_string(),
279                )
280            })?;
281            balance[u] = balance[u].checked_sub(flow).ok_or_else(|| {
282                crate::traits::EvaluationError::IntegerOverflow(
283                    "computing maximum-flow vertex balance".to_string(),
284                )
285            })?;
286            balance[v] = balance[v].checked_add(flow).ok_or_else(|| {
287                crate::traits::EvaluationError::IntegerOverflow(
288                    "computing maximum-flow vertex balance".to_string(),
289                )
290            })?;
291        }
292        for (v, &bal) in balance.iter().enumerate() {
293            if v != self.source && v != self.sink && bal != 0 {
294                return Ok(false);
295            }
296        }
297        Ok(true)
298    }
299
300    /// Compute the flow value `|f|` = net outflow from the source for a
301    /// feasible configuration. Result is meaningless if `config` is not
302    /// feasible.
303    pub fn flow_value(&self, config: &[usize]) -> Result<i64, crate::traits::EvaluationError> {
304        let mut net_out: i64 = 0;
305        for (a, &(u, v)) in self.graph.arcs().iter().enumerate() {
306            let f = i64::try_from(config[a]).map_err(|_| {
307                crate::traits::EvaluationError::IntegerOverflow(
308                    "converting maximum-flow configuration value".to_string(),
309                )
310            })?;
311            if u == self.source {
312                net_out = net_out.checked_add(f).ok_or_else(|| {
313                    crate::traits::EvaluationError::IntegerOverflow(
314                        "computing maximum-flow value".to_string(),
315                    )
316                })?;
317            }
318            if v == self.source {
319                net_out = net_out.checked_sub(f).ok_or_else(|| {
320                    crate::traits::EvaluationError::IntegerOverflow(
321                        "computing maximum-flow value".to_string(),
322                    )
323                })?;
324            }
325        }
326        Ok(net_out)
327    }
328
329    /// Compute the total cost `sum_a cost(a) * f(a)` of a flow.
330    pub fn total_cost(&self, config: &[usize]) -> Result<i64, crate::traits::EvaluationError> {
331        let mut total = 0_i64;
332        for (&flow, &cost) in config.iter().zip(self.costs.iter()) {
333            let flow = i64::try_from(flow).map_err(|_| {
334                crate::traits::EvaluationError::IntegerOverflow(
335                    "converting maximum-flow configuration value".to_string(),
336                )
337            })?;
338            let term = flow.checked_mul(cost).ok_or_else(|| {
339                crate::traits::EvaluationError::IntegerOverflow(
340                    "multiplying maximum-flow arc cost".to_string(),
341                )
342            })?;
343            total = total.checked_add(term).ok_or_else(|| {
344                crate::traits::EvaluationError::IntegerOverflow(
345                    "summing maximum-flow costs".to_string(),
346                )
347            })?;
348        }
349        Ok(total)
350    }
351
352    /// Upper bound on the integral flow value: `sum_a c(a)` (a trivial
353    /// but valid bound, since `|f|` is bounded by the total capacity).
354    fn max_possible_flow(&self) -> Result<i64, crate::traits::EvaluationError> {
355        self.capacities.iter().try_fold(0_i64, |total, &capacity| {
356            total.checked_add(capacity).ok_or_else(|| {
357                crate::traits::EvaluationError::IntegerOverflow(
358                    "summing maximum-flow capacities".to_string(),
359                )
360            })
361        })
362    }
363
364    /// Strict upper bound on any feasible cost, used as the
365    /// lex-multiplier `M` so that the scalar `score = M * (B - |f|)
366    /// + cost(f)` orders by `(max |f|, min cost(f))`.
367    fn cost_multiplier(&self) -> Result<i64, crate::traits::EvaluationError> {
368        let mut total = 0_i64;
369        for (&capacity, &cost) in self.capacities.iter().zip(self.costs.iter()) {
370            let term = capacity.checked_mul(cost).ok_or_else(|| {
371                crate::traits::EvaluationError::IntegerOverflow(
372                    "multiplying maximum-flow capacity by cost".to_string(),
373                )
374            })?;
375            total = total.checked_add(term).ok_or_else(|| {
376                crate::traits::EvaluationError::IntegerOverflow(
377                    "summing maximum-flow cost bounds".to_string(),
378                )
379            })?;
380        }
381        total.checked_add(1).ok_or_else(|| {
382            crate::traits::EvaluationError::IntegerOverflow(
383                "forming maximum-flow cost multiplier".to_string(),
384            )
385        })
386    }
387}
388
389impl Problem for MinimumCostMaximumFlow {
390    const NAME: &'static str = "MinimumCostMaximumFlow";
391    type Solution = Vec<usize>;
392    type Value = crate::types::Min<i64>;
393
394    crate::problem_parameters![("num_arcs", num_arcs), ("num_vertices", num_vertices),];
395
396    fn evaluate(
397        &self,
398        config: &Self::Solution,
399    ) -> Result<crate::types::Min<i64>, crate::traits::EvaluationError> {
400        if config.len() != self.graph.num_arcs() {
401            return Err(crate::traits::EvaluationError::InvalidConfiguration(
402                "flow vector length does not match the graph arcs".into(),
403            ));
404        }
405        Ok({
406            if !self.is_feasible(config)? {
407                return Ok(crate::types::Min(None));
408            }
409            let m = self.cost_multiplier()?;
410            let value = self.flow_value(config)?;
411            let cost = self.total_cost(config)?;
412            let bound = self.max_possible_flow()?;
413            // score = M * (max_possible_flow - |f|) + cost(f)
414            let remaining = bound.checked_sub(value).ok_or_else(|| {
415                crate::traits::EvaluationError::IntegerOverflow(
416                    "computing maximum-flow objective gap".to_string(),
417                )
418            })?;
419            let penalty = m.checked_mul(remaining).ok_or_else(|| {
420                crate::traits::EvaluationError::IntegerOverflow(
421                    "multiplying maximum-flow objective penalty".to_string(),
422                )
423            })?;
424            let score = penalty.checked_add(cost).ok_or_else(|| {
425                crate::traits::EvaluationError::IntegerOverflow(
426                    "summing maximum-flow objective".to_string(),
427                )
428            })?;
429            crate::types::Min(Some(score))
430        })
431    }
432
433    fn variant() -> Vec<(&'static str, &'static str)> {
434        crate::variant_params![]
435    }
436}
437
438impl crate::solvers::BruteForceProblem for MinimumCostMaximumFlow {
439    fn dimensions(&self) -> Vec<usize> {
440        self.capacities.iter().map(|&c| (c as usize) + 1).collect()
441    }
442}
443
444crate::declare_variants! {
445    default MinimumCostMaximumFlow => "(num_vertices + num_arcs)^6",
446}
447
448crate::register_brute_force! {
449    MinimumCostMaximumFlow,
450}
451
452#[cfg(feature = "example-db")]
453pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
454    let problem = MinimumCostMaximumFlow::new(
455        crate::topology::DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]),
456        0,
457        3,
458        vec![2, 1, 1, 1, 2],
459        vec![1, 0, 0, 1, 2],
460    );
461    // Optimal flow has value 3, routed as:
462    //   - 1 unit on 0->1->3        via arcs 0,3 (cost 1 + 1 = 2)
463    //   - 1 unit on 0->1->2->3     via arcs 0,2,4 (cost 1 + 0 + 2 = 3)
464    //   - 1 unit on 0->2->3        via arcs 1,4 (cost 0 + 2 = 2)
465    // Arc flows sum to f = [2, 1, 1, 1, 2]: value = 3,
466    // cost = 2*1 + 1*0 + 1*0 + 1*1 + 2*2 = 7.
467    let optimal_config = vec![2, 1, 1, 1, 2];
468    let optimal_value = problem
469        .evaluate(&optimal_config)
470        .expect("canonical example evaluation must succeed");
471    let scalar = match optimal_value {
472        crate::types::Min(Some(v)) => v,
473        crate::types::Min(None) => panic!("canonical example must be feasible"),
474    };
475    vec![crate::example_db::specs::ModelExampleSpec {
476        id: "minimum_cost_maximum_flow",
477        instance: Box::new(problem),
478        optimal_config: serde_json::to_value(optimal_config)
479            .expect("solution serialization must succeed"),
480        optimal_value: serde_json::json!(scalar),
481    }]
482}
483
484#[cfg(test)]
485#[path = "../../unit_tests/models/graph/minimum_cost_maximum_flow.rs"]
486mod tests;