Skip to main content

problemreductions/models/graph/
minimum_cost_circulation.rs

1//! Minimum-Cost Circulation problem implementation.
2//!
3//! Given a directed multigraph `G = (V, A)` with arc capacities `c(a)`
4//! and **signed** arc costs `a(a)`, find a circulation `g` that
5//! minimizes the total cost
6//!
7//! `sum_{a in A} a(a) * g(a)`,
8//!
9//! subject to
10//!
11//! 1. `0 <= g(a) <= c(a)` for every arc `a`, and
12//! 2. inflow equals outflow at **every** vertex `v in V` (there is no
13//!    distinguished source or sink — this is the defining feature of a
14//!    circulation).
15//!
16//! Negative costs are explicitly allowed: with finite capacities every
17//! circulation has bounded cost, and negative-cost cycles are exactly
18//! what the standard reduction from min-cost max-flow uses (a single
19//! sufficiently negative return arc from the sink to the source).
20//!
21//! # Integral-circulation restriction
22//!
23//! The mathematical formulation in issue #1030 uses continuous flows
24//! `g: A -> R_{>= 0}`, but the [`Problem`] trait requires a discrete
25//! configuration space via [`Problem::dims`]. Following the same
26//! precedent as [`MinimumEdgeCostFlow`](super::MinimumEdgeCostFlow) and
27//! the recently added [`MinimumCostMaximumFlow`](super::MinimumCostMaximumFlow),
28//! we therefore restrict to **integer** circulations: each variable
29//! `g(a)` ranges over `{0, 1, ..., c(a)}`. When capacities and costs are
30//! integral, the standard minimum-cost flow theory (see e.g. the MIT
31//! 6.854 notes, Ahuja-Magnanti-Orlin) guarantees that an integral
32//! optimum exists, so this restriction does not change the optimal value
33//! on integer instances.
34
35use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
36use crate::topology::DirectedGraph;
37use crate::traits::Problem;
38use serde::{Deserialize, Serialize};
39
40inventory::submit! {
41    ProblemSchemaEntry {
42        name: "MinimumCostCirculation",
43        display_name: "Minimum-Cost Circulation",
44        aliases: &["MCC"],
45        dimensions: &[],
46        module_path: module_path!(),
47        description: "Integral circulation on a directed multigraph minimizing total signed arc cost",
48        fields: &[
49            FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed multigraph G = (V, A); loops and parallel arcs allowed" },
50            FieldInfo { name: "capacities", type_name: "Vec<i64>", description: "Arc capacity c(a) in graph arc order (non-negative)" },
51            FieldInfo { name: "costs", type_name: "Vec<i64>", description: "Signed arc cost a(a) in graph arc order (negative values allowed)" },
52        ],
53    }
54}
55
56inventory::submit! {
57    ProblemSizeFieldEntry {
58        name: "MinimumCostCirculation",
59        fields: &["num_vertices", "num_arcs"],
60    }
61}
62
63/// Minimum-Cost Circulation problem.
64///
65/// # Variables
66///
67/// `|A|` variables: variable `a` ranges over `{0, ..., c(a)}`
68/// representing the integral circulation on arc `a`.
69///
70/// # Example
71///
72/// ```
73/// use problemreductions::models::graph::MinimumCostCirculation;
74/// use problemreductions::topology::DirectedGraph;
75/// use problemreductions::{Problem, Solver, BruteForce};
76///
77/// // Two competing cycles 0->1->0 and 0->2->0; the cheaper-per-unit
78/// // cycle 0->2->0 has lower capacity, but pushing both to capacity is
79/// // optimal.
80/// let graph = DirectedGraph::new(3, vec![
81///     (0, 1), (1, 0), (0, 2), (2, 0),
82/// ]);
83/// let problem = MinimumCostCirculation::new(
84///     graph,
85///     vec![2, 2, 1, 1],   // capacities
86///     vec![2, -3, 1, -4], // costs (signed)
87/// );
88/// let solver = BruteForce::new();
89/// let witness = solver.find_witness(&problem).unwrap();
90/// // Optimal cost = 2*2 + 2*(-3) + 1*1 + 1*(-4) = -5.
91/// assert_eq!(problem.total_cost(&witness), -5);
92/// ```
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct MinimumCostCirculation {
95    /// The directed multigraph G = (V, A).
96    graph: DirectedGraph,
97    /// Capacity c(a) for each arc.
98    capacities: Vec<i64>,
99    /// Signed cost a(a) for each arc.
100    costs: Vec<i64>,
101}
102
103impl MinimumCostCirculation {
104    /// Create a new Minimum-Cost Circulation problem.
105    ///
106    /// # Panics
107    ///
108    /// Panics if any of the following holds:
109    /// - `capacities.len() != graph.num_arcs()`
110    /// - `costs.len() != graph.num_arcs()`
111    /// - Any capacity is negative
112    ///
113    /// Note: costs are signed and **may be negative**.
114    pub fn new(graph: DirectedGraph, capacities: Vec<i64>, costs: Vec<i64>) -> Self {
115        let m = graph.num_arcs();
116        assert_eq!(
117            capacities.len(),
118            m,
119            "capacities length ({}) must match num_arcs ({m})",
120            capacities.len()
121        );
122        assert_eq!(
123            costs.len(),
124            m,
125            "costs length ({}) must match num_arcs ({m})",
126            costs.len()
127        );
128        for (i, &c) in capacities.iter().enumerate() {
129            assert!(c >= 0, "capacity[{i}] = {c} is negative");
130        }
131        Self {
132            graph,
133            capacities,
134            costs,
135        }
136    }
137
138    /// Get a reference to the underlying directed graph.
139    pub fn graph(&self) -> &DirectedGraph {
140        &self.graph
141    }
142
143    /// Get a reference to the arc capacities.
144    pub fn capacities(&self) -> &[i64] {
145        &self.capacities
146    }
147
148    /// Get a reference to the arc costs (signed).
149    pub fn costs(&self) -> &[i64] {
150        &self.costs
151    }
152
153    /// Get the number of vertices `|V|`.
154    pub fn num_vertices(&self) -> usize {
155        self.graph.num_vertices()
156    }
157
158    /// Get the number of arcs `|A|`.
159    pub fn num_arcs(&self) -> usize {
160        self.graph.num_arcs()
161    }
162
163    /// Check whether a circulation assignment is feasible.
164    ///
165    /// A circulation is feasible iff
166    /// 1. `config.len() == num_arcs`,
167    /// 2. each `0 <= g(a) <= c(a)`, and
168    /// 3. inflow equals outflow at **every** vertex (no exempt
169    ///    terminals — this is what distinguishes a circulation from a
170    ///    flow).
171    pub fn is_feasible(&self, config: &[usize]) -> bool {
172        let m = self.graph.num_arcs();
173        if config.len() != m {
174            return false;
175        }
176        // (1) Capacity constraints
177        for (flow, cap) in config.iter().zip(self.capacities.iter()) {
178            if (*flow as i64) > *cap {
179                return false;
180            }
181        }
182        // (2) Flow conservation at every vertex
183        let n = self.graph.num_vertices();
184        let mut balance = vec![0_i64; n];
185        for (a, &(u, v)) in self.graph.arcs().iter().enumerate() {
186            let flow = config[a] as i64;
187            balance[u] -= flow;
188            balance[v] += flow;
189        }
190        balance.iter().all(|&b| b == 0)
191    }
192
193    /// Compute the total cost `sum_a a(a) * g(a)` of a circulation.
194    pub fn total_cost(&self, config: &[usize]) -> i64 {
195        config
196            .iter()
197            .zip(self.costs.iter())
198            .map(|(&g, &c)| (g as i64) * c)
199            .sum()
200    }
201}
202
203impl Problem for MinimumCostCirculation {
204    const NAME: &'static str = "MinimumCostCirculation";
205    type Value = crate::types::Min<i64>;
206
207    fn dims(&self) -> Vec<usize> {
208        self.capacities.iter().map(|&c| (c as usize) + 1).collect()
209    }
210
211    fn evaluate(&self, config: &[usize]) -> crate::types::Min<i64> {
212        if !self.is_feasible(config) {
213            return crate::types::Min(None);
214        }
215        crate::types::Min(Some(self.total_cost(config)))
216    }
217
218    fn variant() -> Vec<(&'static str, &'static str)> {
219        crate::variant_params![]
220    }
221}
222
223crate::declare_variants! {
224    default MinimumCostCirculation => "(num_vertices + num_arcs)^6",
225}
226
227#[cfg(feature = "example-db")]
228pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
229    // Two competing cycles on V = {0, 1, 2}:
230    //   cycle A: 0->1->0  (cost per unit = 2 + (-3) = -1, capacity 2)
231    //   cycle B: 0->2->0  (cost per unit = 1 + (-4) = -3, capacity 1)
232    // Optimal: push both to capacity.
233    //   arc 0 (0->1) = 2, arc 1 (1->0) = 2, arc 2 (0->2) = 1, arc 3 (2->0) = 1
234    //   cost = 2*2 + 2*(-3) + 1*1 + 1*(-4) = 4 - 6 + 1 - 4 = -5
235    let problem = MinimumCostCirculation::new(
236        crate::topology::DirectedGraph::new(3, vec![(0, 1), (1, 0), (0, 2), (2, 0)]),
237        vec![2, 2, 1, 1],
238        vec![2, -3, 1, -4],
239    );
240    let optimal_config = vec![2_usize, 2, 1, 1];
241    let optimal_value = problem.evaluate(&optimal_config);
242    let scalar = match optimal_value {
243        crate::types::Min(Some(v)) => v,
244        crate::types::Min(None) => panic!("canonical example must be feasible"),
245    };
246    vec![crate::example_db::specs::ModelExampleSpec {
247        id: "minimum_cost_circulation",
248        instance: Box::new(problem),
249        optimal_config,
250        optimal_value: serde_json::json!(scalar),
251    }]
252}
253
254#[cfg(test)]
255#[path = "../../unit_tests/models/graph/minimum_cost_circulation.rs"]
256mod tests;