Skip to main content

problemreductions/models/graph/
minimum_sum_multicenter.rs

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