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>` 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>`)
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::{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>,
37    num_vertices: usize,
38}
39
40impl ReductionResult for ReductionMMCToILP {
41    type Source = MinMaxMulticenter<SimpleGraph, i64>;
42    type Target = ILP<i64>;
43
44    fn target_problem(&self) -> &ILP<i64> {
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) -> Vec<Option<i64>> {
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; 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 + len;
111            let should_update = match dist[v] {
112                None => true,
113                Some(current) => candidate < current,
114            };
115            if should_update {
116                dist[v] = Some(candidate);
117            }
118        }
119    }
120
121    dist
122}
123
124#[reduction(
125    transform = exact {
126        num_vars = "num_vertices + num_vertices^2 + 1",
127        num_constraints = "2 * num_vertices^2 + 3 * num_vertices + 2",
128    },
129    unavailable = {
130        num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform",
131    }
132)]
133impl ReduceTo<ILP<i64>> for MinMaxMulticenter<SimpleGraph, i64> {
134    type Result = ReductionMMCToILP;
135
136    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
137        let n = self.num_vertices();
138        let k = Self::exact_i64(self.k(), "encoding the number of centers")?;
139        let vertex_weights = self.vertex_weights();
140        let edge_lengths = self.edge_lengths();
141
142        // Precompute all-pairs weighted shortest-path distances.
143        let all_dist: Vec<Vec<Option<i64>>> = (0..n)
144            .map(|s| weighted_distances_mmc(self.graph(), edge_lengths, s, n))
145            .collect();
146
147        // Index helpers.
148        let x_var = |j: usize| j;
149        let y_var = |i: usize, j: usize| n + i * n + j;
150        let z_var = n + n * n;
151
152        let num_vars = n + n * n + 1;
153        let mut constraints = Vec::with_capacity(2 * n * n + 3 * n + 2);
154
155        // Cardinality constraint: Σ_j x_j = k
156        let center_terms: Vec<(usize, i64)> = (0..n).map(|j| (x_var(j), 1)).collect();
157        constraints.push(LinearConstraint::eq(center_terms, k));
158
159        // Assignment constraints: ∀i: Σ_j y_{i,j} = 1
160        for i in 0..n {
161            let terms: Vec<(usize, i64)> = (0..n).map(|j| (y_var(i, j), 1)).collect();
162            constraints.push(LinearConstraint::eq(terms, 1));
163        }
164
165        // Assignment link constraints:
166        // reachable pairs use y_{i,j} ≤ x_j, unreachable pairs force y_{i,j} = 0.
167        for (i, distances) in all_dist.iter().enumerate() {
168            for (j, distance) in distances.iter().enumerate() {
169                if distance.is_some() {
170                    constraints.push(LinearConstraint::le(
171                        vec![(y_var(i, j), 1), (x_var(j), -1)],
172                        0,
173                    ));
174                } else {
175                    constraints.push(LinearConstraint::eq(vec![(y_var(i, j), 1)], 0));
176                }
177            }
178        }
179
180        // Binary bounds for x_j and y_{i,j} (enforce binary within `ILP<i64>`)
181        for j in 0..n {
182            constraints.push(LinearConstraint::le(vec![(x_var(j), 1)], 1));
183        }
184        for i in 0..n {
185            for j in 0..n {
186                constraints.push(LinearConstraint::le(vec![(y_var(i, j), 1)], 1));
187            }
188        }
189
190        // Upper bound on z: the worst-case weighted distance over all vertex
191        // pairs.  Without this bound HiGHS sees z ∈ [0, 2^31) and can stall.
192        let weighted_distances: Vec<Vec<Option<i64>>> = all_dist
193            .iter()
194            .enumerate()
195            .map(|(i, row)| {
196                row.iter()
197                    .map(|distance| {
198                        distance
199                            .map(|distance| {
200                                vertex_weights[i].checked_mul(distance).ok_or_else(|| {
201                                    crate::rules::ReductionError::integer_overflow::<
202                                        MinMaxMulticenter<SimpleGraph, i64>,
203                                        ILP<i64>,
204                                    >(
205                                        "multiplying a vertex weight by a shortest-path distance"
206                                    )
207                                })
208                            })
209                            .transpose()
210                    })
211                    .collect::<Result<Vec<_>, _>>()
212            })
213            .collect::<Result<_, crate::rules::ReductionError>>()?;
214        let z_upper = weighted_distances
215            .iter()
216            .flatten()
217            .filter_map(|distance| *distance)
218            .fold(0_i64, i64::max);
219        constraints.push(LinearConstraint::le(vec![(z_var, 1)], z_upper));
220
221        // Minimax constraints: ∀i: Σ_j w_i · d(i,j) · y_{i,j} ≤ z
222        for (i, distances) in weighted_distances.iter().enumerate() {
223            let mut terms: Vec<(usize, i64)> = distances
224                .iter()
225                .enumerate()
226                .filter_map(|(j, distance)| distance.map(|distance| (y_var(i, j), distance)))
227                .collect();
228            terms.push((z_var, -1));
229            constraints.push(LinearConstraint::le(terms, 0));
230        }
231
232        // Objective: minimize z
233        let objective = vec![(z_var, 1)];
234
235        let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
236            .map_err(Self::target_construction)?;
237        Ok(ReductionMMCToILP {
238            target,
239            num_vertices: n,
240        })
241    }
242}
243
244#[cfg(feature = "example-db")]
245pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
246    vec![crate::example_db::specs::RuleExampleSpec {
247        id: "minmaxmulticenter_to_ilp",
248        build: || {
249            // 3-vertex path: 0 - 1 - 2, unit weights/lengths, K=1
250            // Optimal: place center at vertex 1; max distance = 1.
251            let source = MinMaxMulticenter::new(
252                SimpleGraph::new(3, vec![(0, 1), (1, 2)]),
253                vec![1i64; 3],
254                vec![1i64; 2],
255                1,
256            );
257            crate::example_db::specs::rule_example_via_ilp::<_, i64>(source)
258        },
259    }]
260}
261
262#[cfg(test)]
263#[path = "../unit_tests/rules/minmaxmulticenter_ilp.rs"]
264mod tests;