Skip to main content

problemreductions/rules/
minmaxmulticenter_ilp.rs

1//! Reduction from MinMaxMulticenter to ILP (Integer Linear Programming).
2//!
3//! The vertex p-center optimization problem is formulated as a mixed ILP
4//! using `ILP<i64, i64, Bounded>` to accommodate both binary and integer variables.
5//!
6//! Variable layout:
7//! - `x_j` for each vertex j (binary: 1 if vertex j is selected as a center), indices `0..n`
8//! - `y_{i,j}` for each ordered pair (i, j), index `n + i*n + j`
9//!   (binary: 1 if vertex i is assigned to center j)
10//! - `z` at index `n + n^2` (integer: the maximum weighted distance to minimize)
11//!
12//! Constraints:
13//! - Cardinality: Σ_j x_j = k (exactly k centers)
14//! - Assignment: ∀i: Σ_j y_{i,j} = 1 (each vertex assigned to exactly one center)
15//! - Assignment link: ∀i,j: if j is reachable from i then y_{i,j} ≤ x_j,
16//!   otherwise y_{i,j} = 0
17//! - Binary bounds: x_j ≤ 1, y_{i,j} ≤ 1 (enforce binary within `ILP<i64, i64, Bounded>`)
18//! - Minimax: ∀i: Σ_j w_i · d(i,j) · y_{i,j} ≤ z
19//!
20//! Objective: minimize z.
21//!
22//! Extraction: first n variables (x_j).
23//!
24//! Note: All-pairs shortest-path distances are computed using weighted shortest
25//! paths over `edge_lengths`. Unreachable assignment variables are forced to 0.
26
27use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP};
28use crate::models::graph::MinMaxMulticenter;
29use crate::reduction;
30use crate::rules::traits::{ReduceTo, ReductionResult};
31use crate::topology::{Graph, SimpleGraph};
32
33/// Result of reducing MinMaxMulticenter to ILP.
34#[derive(Debug, Clone)]
35pub struct ReductionMMCToILP {
36    target: ILP<i64, i64, Bounded>,
37    num_vertices: usize,
38}
39
40impl ReductionResult for ReductionMMCToILP {
41    type Source = MinMaxMulticenter<SimpleGraph, i64>;
42    type Target = ILP<i64, i64, Bounded>;
43
44    fn target_problem(&self) -> &ILP<i64, i64, Bounded> {
45        &self.target
46    }
47
48    fn extract_solution(
49        &self,
50        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
51    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
52        crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
53
54        Ok(target_solution[..self.num_vertices]
55            .iter()
56            .map(|&value| value == 1)
57            .collect())
58    }
59}
60
61/// Compute weighted shortest-path distances from `source` in `graph`.
62///
63/// Returns a vector of length `n`; unreachable vertices remain `None`.
64fn weighted_distances_mmc(
65    graph: &SimpleGraph,
66    edge_lengths: &[i64],
67    source: usize,
68    n: usize,
69) -> Result<Vec<Option<i64>>, crate::rules::ReductionError> {
70    let mut adj: Vec<Vec<(usize, i64)>> = vec![Vec::new(); n];
71    for (idx, &(u, v)) in graph.edges().iter().enumerate() {
72        let len = edge_lengths[idx];
73        adj[u].push((v, len));
74        adj[v].push((u, len));
75    }
76
77    let mut dist = vec![None::<i64>; n];
78    let mut visited = vec![false; n];
79    dist[source] = Some(0);
80
81    for _ in 0..n {
82        let mut next = None;
83        for vertex in 0..n {
84            if visited[vertex] {
85                continue;
86            }
87            let Some(dv) = dist[vertex] else {
88                continue;
89            };
90            match next {
91                None => next = Some(vertex),
92                Some(prev) => {
93                    if dv < dist[prev].expect("selected vertex must have a distance") {
94                        next = Some(vertex);
95                    }
96                }
97            }
98        }
99
100        let Some(u) = next else {
101            break;
102        };
103        visited[u] = true;
104        let du = dist[u].expect("selected vertex must have a distance");
105
106        for &(v, len) in &adj[u] {
107            if visited[v] {
108                continue;
109            }
110            let candidate = du.checked_add(len).ok_or_else(|| {
111                crate::rules::ReductionError::integer_overflow::<
112                    MinMaxMulticenter<SimpleGraph, i64>,
113                    ILP<i64, i64, Bounded>,
114                >("adding shortest-path lengths")
115            })?;
116            let should_update = match dist[v] {
117                None => true,
118                Some(current) => candidate < current,
119            };
120            if should_update {
121                dist[v] = Some(candidate);
122            }
123        }
124    }
125
126    Ok(dist)
127}
128
129#[reduction(transform = {
130    exact {
131        num_vars = "num_vertices + num_vertices^2 + 1",
132        num_constraints = "2 * num_vertices^2 + 3 * num_vertices + 2",
133    },
134    upper_bound {
135        max_constraint_magnitude_bits = "2 * max_numeric_magnitude_bits + num_vertices",
136        num_nonzeros = "(num_vertices + num_vertices^2 + 1) * (2 * num_vertices^2 + 3 * num_vertices + 2)",
137    },
138})]
139impl ReduceTo<ILP<i64, i64, Bounded>> for MinMaxMulticenter<SimpleGraph, i64> {
140    type Result = ReductionMMCToILP;
141
142    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
143        let n = self.num_vertices();
144        let k = Self::exact_i64(self.k(), "encoding the number of centers")?;
145        let vertex_weights = self.vertex_weights();
146        let edge_lengths = self.edge_lengths();
147
148        // Precompute all-pairs weighted shortest-path distances.
149        let all_dist: Vec<Vec<Option<i64>>> = (0..n)
150            .map(|s| weighted_distances_mmc(self.graph(), edge_lengths, s, n))
151            .collect::<Result<_, _>>()?;
152
153        // Index helpers.
154        let x_var = |j: usize| j;
155        let y_var = |i: usize, j: usize| n + i * n + j;
156        let z_var = n + n * n;
157
158        let num_vars = n + n * n + 1;
159        let mut constraints = Vec::with_capacity(2 * n * n + 3 * n + 2);
160
161        // Cardinality constraint: Σ_j x_j = k
162        let center_terms: Vec<(usize, i64)> = (0..n).map(|j| (x_var(j), 1)).collect();
163        constraints.push(LinearConstraint::eq(center_terms, k));
164
165        // Assignment constraints: ∀i: Σ_j y_{i,j} = 1
166        for i in 0..n {
167            let terms: Vec<(usize, i64)> = (0..n).map(|j| (y_var(i, j), 1)).collect();
168            constraints.push(LinearConstraint::eq(terms, 1));
169        }
170
171        // Assignment link constraints:
172        // reachable pairs use y_{i,j} ≤ x_j, unreachable pairs force y_{i,j} = 0.
173        for (i, distances) in all_dist.iter().enumerate() {
174            for (j, distance) in distances.iter().enumerate() {
175                if distance.is_some() {
176                    constraints.push(LinearConstraint::le(
177                        vec![(y_var(i, j), 1), (x_var(j), -1)],
178                        0,
179                    ));
180                } else {
181                    constraints.push(LinearConstraint::eq(vec![(y_var(i, j), 1)], 0));
182                }
183            }
184        }
185
186        // Binary bounds for x_j and y_{i,j} (enforce binary within `ILP<i64, i64, Bounded>`)
187        for j in 0..n {
188            constraints.push(LinearConstraint::le(vec![(x_var(j), 1)], 1));
189        }
190        for i in 0..n {
191            for j in 0..n {
192                constraints.push(LinearConstraint::le(vec![(y_var(i, j), 1)], 1));
193            }
194        }
195
196        // Upper bound on z: the worst-case weighted distance over all vertex
197        // pairs.  Without this bound HiGHS sees z ∈ [0, 2^31) and can stall.
198        let weighted_distances: Vec<Vec<Option<i64>>> = all_dist
199            .iter()
200            .enumerate()
201            .map(|(i, row)| {
202                row.iter()
203                    .map(|distance| {
204                        distance
205                            .map(|distance| {
206                                vertex_weights[i].checked_mul(distance).ok_or_else(|| {
207                                    crate::rules::ReductionError::integer_overflow::<
208                                        MinMaxMulticenter<SimpleGraph, i64>,
209                                        ILP<i64, i64, Bounded>,
210                                    >(
211                                        "multiplying a vertex weight by a shortest-path distance"
212                                    )
213                                })
214                            })
215                            .transpose()
216                    })
217                    .collect::<Result<Vec<_>, _>>()
218            })
219            .collect::<Result<_, crate::rules::ReductionError>>()?;
220        let z_upper = weighted_distances
221            .iter()
222            .flatten()
223            .filter_map(|distance| *distance)
224            .fold(0_i64, i64::max);
225        constraints.push(LinearConstraint::le(vec![(z_var, 1)], z_upper));
226
227        // Minimax constraints: ∀i: Σ_j w_i · d(i,j) · y_{i,j} ≤ z
228        for (i, distances) in weighted_distances.iter().enumerate() {
229            let mut terms: Vec<(usize, i64)> = distances
230                .iter()
231                .enumerate()
232                .filter_map(|(j, distance)| distance.map(|distance| (y_var(i, j), distance)))
233                .collect();
234            terms.push((z_var, -1));
235            constraints.push(LinearConstraint::le(terms, 0));
236        }
237
238        // Objective: minimize z
239        let objective = vec![(z_var, 1)];
240
241        let mut variables = vec![IntegerVariable::binary(); num_vars];
242        variables[z_var] =
243            IntegerVariable::new(Some(0), Some(z_upper)).map_err(Self::target_construction)?;
244
245        let target =
246            ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize)
247                .map_err(Self::target_construction)?;
248        Ok(ReductionMMCToILP {
249            target,
250            num_vertices: n,
251        })
252    }
253}
254
255#[cfg(feature = "example-db")]
256pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
257    vec![crate::example_db::specs::RuleExampleSpec {
258        id: "minmaxmulticenter_to_ilp",
259        build: || {
260            // 3-vertex path: 0 - 1 - 2, unit weights/lengths, K=1
261            // Optimal: place center at vertex 1; max distance = 1.
262            let source = MinMaxMulticenter::new(
263                SimpleGraph::new(3, vec![(0, 1), (1, 2)]),
264                vec![1i64; 3],
265                vec![1i64; 2],
266                1,
267            );
268            crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source)
269        },
270    }]
271}
272
273#[cfg(test)]
274#[path = "../unit_tests/rules/minmaxmulticenter_ilp.rs"]
275mod tests;