Skip to main content

problemreductions/models/graph/
min_max_multicenter.rs

1//! Min-Max Multicenter (vertex p-center) problem implementation.
2//!
3//! The vertex p-center problem asks for K centers on vertices of a graph that
4//! minimize the maximum weighted distance from any vertex to its nearest center.
5
6use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use crate::types::{Min, One, WeightElement};
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "MinMaxMulticenter",
16        display_name: "Min-Max Multicenter",
17        aliases: &["pCenter"],
18        dimensions: &[
19            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
20            VariantDimension::new("weight", "i64", &["i64", "One"]),
21        ],
22        category: crate::registry::ProblemCategory::Graph,
23        module_path: module_path!(),
24        description: "Find K centers minimizing the maximum weighted distance from any vertex to its nearest center (vertex p-center)",
25        fields: MinMaxMulticenterI64CreateSpec::FIELDS,
26    }
27}
28
29/// The Min-Max Multicenter (vertex p-center) problem.
30///
31/// Given a graph G = (V, E) with vertex weights w(v) and edge lengths l(e),
32/// and a number K of centers to place, find a subset P of K vertices (centers)
33/// that minimizes max_{v in V} w(v) * d(v, P),
34/// where d(v, P) is the shortest-path distance from v to the nearest center.
35///
36/// # Type Parameters
37///
38/// * `G` - The graph type (e.g., `SimpleGraph`)
39/// * `W` - The weight/length type (e.g., `i64`)
40///
41/// # Example
42///
43/// ```
44/// use problemreductions::models::graph::MinMaxMulticenter;
45/// use problemreductions::topology::SimpleGraph;
46/// use problemreductions::{Problem, BruteForce};
47///
48/// // Hexagonal-like graph: 6 vertices, 7 edges, unit weights/lengths, K=2
49/// let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (1, 4)]);
50/// let problem = MinMaxMulticenter::new(graph, vec![1i64; 6], vec![1i64; 7], 2);
51///
52/// let solver = BruteForce::new();
53/// let solution = solver.solve(&problem).unwrap();
54/// assert!(solution.is_some());
55/// ```
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct MinMaxMulticenter<G, W: WeightElement> {
58    /// The underlying graph.
59    graph: G,
60    /// Non-negative weight for each vertex.
61    vertex_weights: Vec<W>,
62    /// Non-negative length for each edge (in edge index order).
63    edge_lengths: Vec<W>,
64    /// Number of centers to place.
65    k: usize,
66}
67
68macro_rules! min_max_multicenter_create_spec {
69    ($name:ident, $weight:ty, $one:expr $(, $weights:ident, $edge_weights:ident)?) => {
70        #[derive(Debug, Deserialize, crate::CreateSpec)]
71        struct $name {
72            #[create(codec = "edge-list")]
73            graph: Vec<(usize, usize)>,
74            num_vertices: Option<usize>,
75            $(
76            #[create(codec = "comma-separated")]
77            $weights: Option<Vec<$weight>>,
78            )?
79            $(
80            #[create(codec = "comma-separated")]
81            $edge_weights: Option<Vec<$weight>>,
82            )?
83            k: usize,
84        }
85
86        impl TryFrom<$name> for MinMaxMulticenter<SimpleGraph, $weight> {
87            type Error = crate::registry::ConstructionError;
88
89            fn try_from(spec: $name) -> Result<Self, Self::Error> {
90                let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?;
91                let vertex_weights = { $(if let Some(value) = spec.$weights { value } else)? { vec![$one; graph.num_vertices()] } };
92                if vertex_weights.len() != graph.num_vertices() {
93                    return Err(format!(
94                        "weights has length {}, expected {}",
95                        vertex_weights.len(),
96                        graph.num_vertices()
97                    )
98                    .into());
99                }
100                let edge_lengths = { $(if let Some(value) = spec.$edge_weights { value } else)? { vec![$one; graph.num_edges()] } };
101                if edge_lengths.len() != graph.num_edges() {
102                    return Err(format!(
103                        "edge_weights has length {}, expected {}",
104                        edge_lengths.len(),
105                        graph.num_edges()
106                    )
107                    .into());
108                }
109                let zero = <$weight as WeightElement>::Sum::zero();
110                if vertex_weights
111                    .iter()
112                    .any(|weight| weight.to_sum() < zero.clone())
113                {
114                    return Err("weights must be non-negative".to_string().into());
115                }
116                if edge_lengths
117                    .iter()
118                    .any(|weight| weight.to_sum() < zero.clone())
119                {
120                    return Err("edge_weights must be non-negative".to_string().into());
121                }
122                if spec.k == 0 || spec.k > graph.num_vertices() {
123                    return Err(format!("k must be between 1 and {}", graph.num_vertices()).into());
124                }
125                Ok(Self::new(graph, vertex_weights, edge_lengths, spec.k))
126            }
127        }
128    };
129}
130
131min_max_multicenter_create_spec!(
132    MinMaxMulticenterI64CreateSpec,
133    i64,
134    1,
135    weights,
136    edge_weights
137);
138min_max_multicenter_create_spec!(MinMaxMulticenterOneCreateSpec, One, One);
139
140fn simple_graph_from_create(
141    edges: Vec<(usize, usize)>,
142    num_vertices: Option<usize>,
143) -> Result<SimpleGraph, crate::registry::ConstructionError> {
144    if edges.is_empty() && num_vertices.is_none() {
145        return Err("num_vertices is required for an empty graph"
146            .to_string()
147            .into());
148    }
149    for (index, &(u, v)) in edges.iter().enumerate() {
150        if u == v {
151            return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
152        }
153    }
154    let inferred = edges
155        .iter()
156        .flat_map(|&(u, v)| [u, v])
157        .max()
158        .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
159        .transpose()?
160        .unwrap_or(0);
161    let num_vertices = num_vertices.unwrap_or(inferred);
162    if num_vertices < inferred {
163        return Err(format!("num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}").into());
164    }
165    Ok(SimpleGraph::new(num_vertices, edges))
166}
167
168impl<G: Graph, W: WeightElement> MinMaxMulticenter<G, W> {
169    /// Create a MinMaxMulticenter problem.
170    ///
171    /// # Panics
172    /// - If `vertex_weights.len() != graph.num_vertices()`
173    /// - If `edge_lengths.len() != graph.num_edges()`
174    /// - If any vertex weight or edge length is negative
175    /// - If `k == 0` or `k > graph.num_vertices()`
176    pub fn new(graph: G, vertex_weights: Vec<W>, edge_lengths: Vec<W>, k: usize) -> Self {
177        assert_eq!(
178            vertex_weights.len(),
179            graph.num_vertices(),
180            "vertex_weights length must match num_vertices"
181        );
182        assert_eq!(
183            edge_lengths.len(),
184            graph.num_edges(),
185            "edge_lengths length must match num_edges"
186        );
187        let zero = W::Sum::zero();
188        assert!(
189            vertex_weights
190                .iter()
191                .all(|weight| weight.to_sum() >= zero.clone()),
192            "vertex_weights must be non-negative"
193        );
194        assert!(
195            edge_lengths
196                .iter()
197                .all(|length| length.to_sum() >= zero.clone()),
198            "edge_lengths must be non-negative"
199        );
200        assert!(k > 0, "k must be positive");
201        assert!(k <= graph.num_vertices(), "k must not exceed num_vertices");
202        Self {
203            graph,
204            vertex_weights,
205            edge_lengths,
206            k,
207        }
208    }
209
210    /// Get a reference to the underlying graph.
211    pub fn graph(&self) -> &G {
212        &self.graph
213    }
214
215    /// Get a reference to the vertex weights.
216    pub fn vertex_weights(&self) -> &[W] {
217        &self.vertex_weights
218    }
219
220    /// Get a reference to the edge lengths.
221    pub fn edge_lengths(&self) -> &[W] {
222        &self.edge_lengths
223    }
224
225    /// Get the number of centers K.
226    pub fn k(&self) -> usize {
227        self.k
228    }
229
230    /// Get the number of vertices in the underlying graph.
231    pub fn num_vertices(&self) -> usize {
232        self.graph().num_vertices()
233    }
234
235    /// Get the number of edges in the underlying graph.
236    pub fn num_edges(&self) -> usize {
237        self.graph().num_edges()
238    }
239
240    /// Get the number of centers K.
241    pub fn num_centers(&self) -> usize {
242        self.k
243    }
244
245    /// Compute shortest distances from each vertex to the nearest center.
246    ///
247    /// Uses multi-source Dijkstra with linear scan: initializes all centers
248    /// at distance 0 and greedily relaxes edges by increasing distance.
249    /// Correct because all edge lengths are non-negative.
250    ///
251    /// Returns `None` if any vertex is unreachable from all centers.
252    fn shortest_distances(&self, config: &[bool]) -> Option<Vec<W::Sum>> {
253        let n = self.graph.num_vertices();
254        if config.len() != n {
255            return None;
256        }
257        let edges = self.graph.edges();
258
259        let mut adj: Vec<Vec<(usize, W::Sum)>> = vec![Vec::new(); n];
260        for (idx, &(u, v)) in edges.iter().enumerate() {
261            let len = self.edge_lengths[idx].to_sum();
262            adj[u].push((v, len.clone()));
263            adj[v].push((u, len));
264        }
265
266        // Multi-source Dijkstra with linear scan (works with PartialOrd)
267        let mut dist: Vec<Option<W::Sum>> = vec![None; n];
268        let mut visited = vec![false; n];
269
270        // Initialize centers
271        for (v, &selected) in config.iter().enumerate() {
272            if selected {
273                dist[v] = Some(W::Sum::zero());
274            }
275        }
276
277        for _ in 0..n {
278            // Find unvisited vertex with smallest distance
279            let mut u = None;
280            for v in 0..n {
281                if visited[v] {
282                    continue;
283                }
284                if let Some(ref dv) = dist[v] {
285                    match u {
286                        None => u = Some(v),
287                        Some(prev) => {
288                            if *dv < dist[prev].clone().unwrap() {
289                                u = Some(v);
290                            }
291                        }
292                    }
293                }
294            }
295            let u = match u {
296                Some(v) => v,
297                None => break, // remaining vertices are unreachable
298            };
299            visited[u] = true;
300
301            let du = dist[u].clone().unwrap();
302            for &(next, ref len) in &adj[u] {
303                if visited[next] {
304                    continue;
305                }
306                let new_dist = du.clone() + len.clone();
307                let update = match &dist[next] {
308                    None => true,
309                    Some(d) => new_dist < *d,
310                };
311                if update {
312                    dist[next] = Some(new_dist);
313                }
314            }
315        }
316
317        dist.into_iter().collect()
318    }
319}
320
321impl<G, W> Problem for MinMaxMulticenter<G, W>
322where
323    G: Graph + crate::variant::VariantParam,
324    W: WeightElement + crate::variant::VariantParam,
325{
326    const NAME: &'static str = "MinMaxMulticenter";
327    type Solution = Vec<bool>;
328    type Value = Min<W::Sum>;
329
330    crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),];
331
332    fn variant() -> Vec<(&'static str, &'static str)> {
333        crate::variant_params![G, W]
334    }
335
336    fn evaluate(
337        &self,
338        config: &Self::Solution,
339    ) -> Result<Min<W::Sum>, crate::traits::EvaluationError> {
340        Ok({
341            if config.len() != self.graph.num_vertices() {
342                return Err(crate::traits::EvaluationError::InvalidConfiguration(
343                    "center-selection length does not match the graph vertices".into(),
344                ));
345            }
346
347            // Check exactly K centers are selected
348            let num_selected = config.iter().filter(|&&selected| selected).count();
349            if num_selected != self.k {
350                return Ok(Min(None));
351            }
352
353            // Compute shortest distances to nearest center
354            let distances = match self.shortest_distances(config) {
355                Some(d) => d,
356                None => {
357                    return Ok(Min(None));
358                }
359            };
360
361            // Compute max weighted distance: max_{v} w(v) * d(v)
362            let mut max_wd = W::Sum::zero();
363            for (v, dist) in distances.iter().enumerate() {
364                let wd = W::checked_mul_sum(
365                    self.vertex_weights[v].to_sum(),
366                    dist.clone(),
367                    "multiplying min-max multicenter vertex weight by distance",
368                )?;
369                if wd > max_wd {
370                    max_wd = wd;
371                }
372            }
373
374            Min(Some(max_wd))
375        })
376    }
377}
378
379impl<G, W> crate::solvers::BruteForceProblem for MinMaxMulticenter<G, W>
380where
381    G: Graph + crate::variant::VariantParam,
382    W: WeightElement + crate::variant::VariantParam,
383{
384    fn dimensions(&self) -> Vec<usize> {
385        vec![2; self.graph.num_vertices()]
386    }
387}
388
389crate::declare_variants! {
390    default MinMaxMulticenter<SimpleGraph, i64> => "1.4969^num_vertices" create MinMaxMulticenterI64CreateSpec,
391    MinMaxMulticenter<SimpleGraph, One> => "1.4969^num_vertices" create MinMaxMulticenterOneCreateSpec,
392}
393
394crate::register_brute_force! {
395    MinMaxMulticenter<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
396    MinMaxMulticenter<SimpleGraph, One> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
397}
398
399#[cfg(feature = "example-db")]
400pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
401    vec![crate::example_db::specs::ModelExampleSpec {
402        id: "min_max_multicenter_simplegraph",
403        instance: Box::new(MinMaxMulticenter::new(
404            SimpleGraph::new(
405                6,
406                vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (1, 4)],
407            ),
408            vec![1i64; 6],
409            vec![1i64; 7],
410            2,
411        )),
412        optimal_config: serde_json::json!(vec![false, true, false, false, true, false]),
413        optimal_value: serde_json::json!(1),
414    }]
415}
416
417#[cfg(test)]
418#[path = "../../unit_tests/models/graph/min_max_multicenter.rs"]
419mod tests;