Skip to main content

problemreductions/models/graph/
minimum_edge_cost_flow.rs

1//! Minimum Edge-Cost Flow problem implementation.
2//!
3//! Given a directed graph G = (V, A) with arc capacities c(a) and prices p(a),
4//! a source s, a sink t, and a flow requirement R, find an integral flow of
5//! value at least R that minimizes the total edge cost — the sum of prices of
6//! arcs carrying nonzero flow.
7//!
8//! This is NP-hard: it generalizes Minimum-Weight Satisfiability via reduction
9//! from Minimum Edge-Cost Flow on DAGs (Amaldi et al., 2011).
10
11use crate::registry::{FieldInfo, ProblemSchemaEntry};
12use crate::topology::DirectedGraph;
13use crate::traits::Problem;
14use serde::{Deserialize, Serialize};
15
16inventory::submit! {
17    ProblemSchemaEntry {
18        name: "MinimumEdgeCostFlow",
19        display_name: "Minimum Edge-Cost Flow",
20        aliases: &["MECF"],
21        dimensions: &[],
22        category: crate::registry::ProblemCategory::Graph,
23        module_path: module_path!(),
24        description: "Integral flow minimizing the number of arcs with nonzero flow (weighted by price)",
25        fields: &[
26            FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed graph G = (V, A)" },
27            FieldInfo { name: "prices", type_name: "Vec<i64>", description: "Price p(a) for each arc" },
28            FieldInfo { name: "capacities", type_name: "Vec<i64>", description: "Capacity c(a) for each arc" },
29            FieldInfo { name: "source", type_name: "usize", description: "Source vertex s" },
30            FieldInfo { name: "sink", type_name: "usize", description: "Sink vertex t" },
31            FieldInfo { name: "required_flow", type_name: "i64", description: "Flow requirement R" },
32        ],
33    }
34}
35
36/// Minimum Edge-Cost Flow problem.
37///
38/// Given a directed graph G = (V, A) with arc capacities c(a) and prices p(a),
39/// source s, sink t, and flow requirement R, find an integral flow f: A -> Z_0^+
40/// of value at least R minimizing the total edge cost sum_{a: f(a)>0} p(a).
41///
42/// # Variables
43///
44/// |A| variables: variable a ranges over {0, ..., c(a)} representing the flow
45/// on arc a.
46///
47/// # Example
48///
49/// ```
50/// use problemreductions::models::graph::MinimumEdgeCostFlow;
51/// use problemreductions::topology::DirectedGraph;
52/// use problemreductions::{Problem, BruteForce};
53///
54/// // 5-vertex network: s=0, t=4, R=3
55/// let graph = DirectedGraph::new(5, vec![
56///     (0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4),
57/// ]);
58/// let problem = MinimumEdgeCostFlow::new(
59///     graph,
60///     vec![3, 1, 2, 0, 0, 0], // prices
61///     vec![2, 2, 2, 2, 2, 2], // capacities
62///     0, 4, 3,
63/// );
64/// let solver = BruteForce::new();
65/// let witness = solver.solve(&problem).unwrap().unwrap();
66/// assert_eq!(problem.evaluate(&witness).unwrap(), problemreductions::types::Min(Some(3)));
67/// ```
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(try_from = "MinimumEdgeCostFlowData")]
70pub struct MinimumEdgeCostFlow {
71    /// The directed graph G = (V, A).
72    graph: DirectedGraph,
73    /// Price p(a) for each arc.
74    prices: Vec<i64>,
75    /// Capacity c(a) for each arc.
76    capacities: Vec<i64>,
77    /// Source vertex s.
78    source: usize,
79    /// Sink vertex t.
80    sink: usize,
81    /// Flow requirement R.
82    required_flow: i64,
83}
84
85#[derive(Deserialize)]
86struct MinimumEdgeCostFlowData {
87    graph: DirectedGraph,
88    prices: Vec<i64>,
89    capacities: Vec<i64>,
90    source: usize,
91    sink: usize,
92    required_flow: i64,
93}
94
95impl TryFrom<MinimumEdgeCostFlowData> for MinimumEdgeCostFlow {
96    type Error = crate::registry::ConstructionError;
97    fn try_from(data: MinimumEdgeCostFlowData) -> Result<Self, Self::Error> {
98        Self::try_new(
99            data.graph,
100            data.prices,
101            data.capacities,
102            data.source,
103            data.sink,
104            data.required_flow,
105        )
106    }
107}
108
109impl MinimumEdgeCostFlow {
110    /// Create a new Minimum Edge-Cost Flow problem.
111    ///
112    /// # Arguments
113    ///
114    /// * `graph` - Directed graph G = (V, A)
115    /// * `prices` - Price p(a) for each arc (one per arc)
116    /// * `capacities` - Capacity c(a) for each arc (one per arc, all non-negative)
117    /// * `source` - Source vertex index
118    /// * `sink` - Sink vertex index
119    /// * `required_flow` - Minimum flow requirement R
120    ///
121    /// # Panics
122    ///
123    /// Panics if:
124    /// - `prices.len() != graph.num_arcs()`
125    /// - `capacities.len() != graph.num_arcs()`
126    /// - `source >= graph.num_vertices()`
127    /// - `sink >= graph.num_vertices()`
128    /// - `source == sink`
129    /// - Any capacity is negative
130    pub fn new(
131        graph: DirectedGraph,
132        prices: Vec<i64>,
133        capacities: Vec<i64>,
134        source: usize,
135        sink: usize,
136        required_flow: i64,
137    ) -> Self {
138        Self::try_new(graph, prices, capacities, source, sink, required_flow)
139            .unwrap_or_else(|error| panic!("{error}"))
140    }
141
142    fn try_new(
143        graph: DirectedGraph,
144        prices: Vec<i64>,
145        capacities: Vec<i64>,
146        source: usize,
147        sink: usize,
148        required_flow: i64,
149    ) -> Result<Self, crate::registry::ConstructionError> {
150        let n = graph.num_vertices();
151        let m = graph.num_arcs();
152        if prices.len() != m {
153            return Err(
154                format!("prices length ({}) must match num_arcs ({m})", prices.len()).into(),
155            );
156        }
157        if capacities.len() != m {
158            return Err(format!(
159                "capacities length ({}) must match num_arcs ({m})",
160                capacities.len()
161            )
162            .into());
163        }
164        if source >= n {
165            return Err(format!("source ({source}) >= num_vertices ({n})").into());
166        }
167        if sink >= n {
168            return Err(format!("sink ({sink}) >= num_vertices ({n})").into());
169        }
170        if source == sink {
171            return Err("source and sink must be distinct".into());
172        }
173        for (i, &c) in capacities.iter().enumerate() {
174            if c < 0 {
175                return Err(format!("capacity[{i}] = {c} is negative").into());
176            }
177        }
178        Ok(Self {
179            graph,
180            prices,
181            capacities,
182            source,
183            sink,
184            required_flow,
185        })
186    }
187
188    /// Get a reference to the underlying directed graph.
189    pub fn graph(&self) -> &DirectedGraph {
190        &self.graph
191    }
192
193    /// Get a reference to the prices.
194    pub fn prices(&self) -> &[i64] {
195        &self.prices
196    }
197
198    /// Get a reference to the capacities.
199    pub fn capacities(&self) -> &[i64] {
200        &self.capacities
201    }
202
203    /// Get the source vertex.
204    pub fn source(&self) -> usize {
205        self.source
206    }
207
208    /// Get the sink vertex.
209    pub fn sink(&self) -> usize {
210        self.sink
211    }
212
213    /// Get the flow requirement.
214    pub fn required_flow(&self) -> i64 {
215        self.required_flow
216    }
217
218    /// Get the number of vertices.
219    pub fn num_vertices(&self) -> usize {
220        self.graph.num_vertices()
221    }
222
223    /// Get the number of edges (arcs).
224    pub fn num_edges(&self) -> usize {
225        self.graph.num_arcs()
226    }
227
228    /// Get the maximum capacity across all arcs (0 if empty).
229    pub fn max_capacity(&self) -> i64 {
230        self.capacities.iter().copied().max().unwrap_or(0)
231    }
232
233    /// Get a reference to the edges (arcs).
234    pub fn edges(&self) -> Vec<(usize, usize)> {
235        self.graph.arcs()
236    }
237
238    /// Check whether a flow assignment is feasible.
239    ///
240    /// A flow is feasible if:
241    /// 1. Each arc's flow does not exceed its capacity
242    /// 2. Flow is conserved at every non-terminal vertex
243    /// 3. Net flow into the sink is at least the required flow
244    pub fn is_feasible(&self, config: &[usize]) -> Result<bool, crate::traits::EvaluationError> {
245        let m = self.graph.num_arcs();
246        if config.len() != m {
247            return Ok(false);
248        }
249        let arcs = self.graph.arcs();
250
251        // (1) Capacity constraints
252        for (flow, cap) in config.iter().zip(self.capacities.iter()) {
253            let flow = i64::try_from(*flow).map_err(|_| {
254                crate::traits::EvaluationError::IntegerOverflow(
255                    "converting edge-cost flow configuration value".to_string(),
256                )
257            })?;
258            if flow > *cap {
259                return Ok(false);
260            }
261        }
262
263        // (2) Flow conservation at non-terminal vertices
264        let n = self.graph.num_vertices();
265        let mut balance = vec![0_i64; n];
266        for (a, &(u, v)) in arcs.iter().enumerate() {
267            let flow = i64::try_from(config[a]).map_err(|_| {
268                crate::traits::EvaluationError::IntegerOverflow(
269                    "converting edge-cost flow configuration value".to_string(),
270                )
271            })?;
272            balance[u] = balance[u].checked_sub(flow).ok_or_else(|| {
273                crate::traits::EvaluationError::IntegerOverflow(
274                    "computing edge-cost flow balance".to_string(),
275                )
276            })?;
277            balance[v] = balance[v].checked_add(flow).ok_or_else(|| {
278                crate::traits::EvaluationError::IntegerOverflow(
279                    "computing edge-cost flow balance".to_string(),
280                )
281            })?;
282        }
283
284        for (v, &bal) in balance.iter().enumerate() {
285            if v != self.source && v != self.sink && bal != 0 {
286                return Ok(false);
287            }
288        }
289
290        // (3) Flow requirement: net flow into sink >= R
291        if balance[self.sink] < self.required_flow {
292            return Ok(false);
293        }
294
295        Ok(true)
296    }
297
298    /// Compute the edge cost for a feasible flow: sum of prices of arcs with
299    /// nonzero flow.
300    pub fn edge_cost(&self, config: &[usize]) -> Result<i64, crate::traits::EvaluationError> {
301        config
302            .iter()
303            .enumerate()
304            .filter(|(_, &flow)| flow > 0)
305            .try_fold(0_i64, |total, (arc, _)| {
306                total.checked_add(self.prices[arc]).ok_or_else(|| {
307                    crate::traits::EvaluationError::IntegerOverflow(
308                        "summing selected edge prices".to_string(),
309                    )
310                })
311            })
312    }
313}
314
315impl Problem for MinimumEdgeCostFlow {
316    const NAME: &'static str = "MinimumEdgeCostFlow";
317    type Solution = Vec<usize>;
318    type Value = crate::types::Min<i64>;
319
320    crate::problem_parameters![
321        ("max_capacity", max_capacity),
322        ("num_edges", num_edges),
323        ("num_vertices", num_vertices),
324    ];
325
326    fn evaluate(
327        &self,
328        config: &Self::Solution,
329    ) -> Result<crate::types::Min<i64>, crate::traits::EvaluationError> {
330        if config.len() != self.graph.num_arcs() {
331            return Err(crate::traits::EvaluationError::InvalidConfiguration(
332                "flow vector length does not match the graph arcs".into(),
333            ));
334        }
335        Ok({
336            if self.is_feasible(config)? {
337                crate::types::Min(Some(self.edge_cost(config)?))
338            } else {
339                crate::types::Min(None)
340            }
341        })
342    }
343
344    fn variant() -> Vec<(&'static str, &'static str)> {
345        crate::variant_params![]
346    }
347}
348
349impl crate::solvers::BruteForceProblem for MinimumEdgeCostFlow {
350    fn dimensions(&self) -> Vec<usize> {
351        self.capacities.iter().map(|&c| (c as usize) + 1).collect()
352    }
353}
354
355crate::declare_variants! {
356    default MinimumEdgeCostFlow => "(max_capacity + 1)^num_edges",
357}
358
359crate::register_brute_force! {
360    MinimumEdgeCostFlow,
361}
362
363#[cfg(feature = "example-db")]
364pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
365    vec![crate::example_db::specs::ModelExampleSpec {
366        id: "minimum_edge_cost_flow",
367        instance: Box::new(MinimumEdgeCostFlow::new(
368            crate::topology::DirectedGraph::new(
369                5,
370                vec![(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)],
371            ),
372            vec![3, 1, 2, 0, 0, 0], // prices
373            vec![2, 2, 2, 2, 2, 2], // capacities
374            0,
375            4,
376            3,
377        )),
378        // Optimal: route 1 unit via v2 and 2 units via v3 → cost = 1 + 2 = 3
379        // config = [0, 1, 2, 0, 1, 2]
380        optimal_config: serde_json::json!(vec![0, 1, 2, 0, 1, 2]),
381        optimal_value: serde_json::json!(3),
382    }]
383}
384
385#[cfg(test)]
386#[path = "../../unit_tests/models/graph/minimum_edge_cost_flow.rs"]
387mod tests;