From 594ad31d859f0087bb74769c684b052208a36289 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 11:55:35 +0000 Subject: [PATCH 1/7] Add plan for #399: MinSumMulticenter model --- docs/plans/2026-03-13-minsummulticenter.md | 99 ++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/plans/2026-03-13-minsummulticenter.md diff --git a/docs/plans/2026-03-13-minsummulticenter.md b/docs/plans/2026-03-13-minsummulticenter.md new file mode 100644 index 000000000..12114450a --- /dev/null +++ b/docs/plans/2026-03-13-minsummulticenter.md @@ -0,0 +1,99 @@ +# Plan: Add MinSumMulticenter Model (#399) + +## Overview + +Add the MinSumMulticenter (p-median) problem model — a facility location optimization problem that minimizes total weighted distance from vertices to K selected centers. + +**Design decision:** Implement as an **optimization problem** (Metric = SolutionSize, Direction::Minimize), consistent with how MinimumDominatingSet, MinimumVertexCover, etc. are implemented despite GJ defining them as decision problems. The bound B is not stored; brute force finds optimal solutions directly. + +## Step 1: Create model file `src/models/graph/min_sum_multicenter.rs` + +### Struct +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinSumMulticenter { + graph: G, + vertex_weights: Vec, + edge_lengths: Vec, + k: usize, +} +``` + +### Constructor & Accessors +- `new(graph, vertex_weights, edge_lengths, k)` — assert vertex_weights.len() == num_vertices, edge_lengths.len() == num_edges, k <= num_vertices, k > 0 +- `graph()`, `vertex_weights()`, `edge_lengths()`, `k()` getters +- `num_vertices()`, `num_edges()`, `num_centers()` size getters (in WeightElement-bounded impl block) + +### Shortest Path Computation +Implement a private method `shortest_distances_to_centers(&self, config: &[usize]) -> Option>`: +- Multi-source BFS-like approach using a priority mechanism +- Build adjacency list with edge lengths from `graph.edges()` and `edge_lengths` +- Use a simple Dijkstra-like algorithm from all selected centers simultaneously +- Returns None if any vertex is unreachable (disconnected graph) +- Returns Some(distances) otherwise + +For the evaluate function: +1. Count selected centers; if != k, return Invalid +2. Compute shortest distances from each vertex to nearest center +3. If any vertex unreachable, return Invalid +4. Compute Σ vertex_weights[v].to_sum() * distances[v] for all v +5. Return SolutionSize::Valid(total) + +### Problem Trait +```rust +const NAME: &'static str = "MinSumMulticenter"; +type Metric = SolutionSize; +fn dims(&self) -> Vec { vec![2; num_vertices] } +fn variant() -> ... { variant_params![G, W] } +``` + +### OptimizationProblem +- Direction::Minimize +- type Value = W::Sum + +### declare_variants! +```rust +MinSumMulticenter => "2^num_vertices", +``` +Note: No specialized exact algorithm improves on brute-force C(n,K) enumeration for general p-median. + +### Schema Registration +Register with `inventory::submit!` including fields: graph, vertex_weights, edge_lengths, k. + +## Step 2: Register module + +- `src/models/graph/mod.rs` — add `pub(crate) mod min_sum_multicenter;` and `pub use min_sum_multicenter::MinSumMulticenter;` +- `src/models/mod.rs` — add `MinSumMulticenter` to graph re-exports +- `src/lib.rs` — add to prelude + +## Step 3: Create unit tests `src/unit_tests/models/graph/min_sum_multicenter.rs` + +Link via `#[path]` in model file. Tests: +- `test_min_sum_multicenter_creation` — basic construction, getters +- `test_min_sum_multicenter_evaluate` — manual evaluation of known configs +- `test_min_sum_multicenter_invalid_k` — wrong number of centers returns Invalid +- `test_min_sum_multicenter_solver` — BruteForce finds optimal for small instance +- `test_min_sum_multicenter_disconnected` — unreachable vertex returns Invalid +- `test_min_sum_multicenter_direction` — Direction::Minimize +- `test_min_sum_multicenter_size_getters` — num_vertices, num_edges, num_centers + +Use the example from the issue: 7 vertices, 8 edges, unit weights/lengths, K=2. Optimal centers at {2, 5} with total cost 6. + +## Step 4: Register in CLI `problemreductions-cli/src/commands/create.rs` + +Add custom handler for "MinSumMulticenter" that accepts: +- `--graph` / `--edges` (graph topology) +- `--vertex-weights` (vertex weights) +- `--edge-lengths` (edge lengths) +- `--k` (number of centers) + +Add to `example_for()` and the main match dispatch. + +## Step 5: Add unit test mod.rs registration + +- `src/unit_tests/models/graph/mod.rs` — add `mod min_sum_multicenter;` + +## Step 6: Verify + +- `make fmt && make clippy && make test` +- Verify the example instance gives expected results From 69805722753bee6462b6f0052911bc0365581386 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 12:08:42 +0000 Subject: [PATCH 2/7] Implement MinSumMulticenter (p-median) model (#399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the MinSumMulticenter problem — a facility location optimization problem that minimizes total weighted distance from vertices to K selected centers. Includes graph + vertex weights + edge lengths input, Bellman-Ford shortest path computation, CLI support, and comprehensive unit tests (19 tests including solver, serialization, edge cases). Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/commands/create.rs | 27 ++ problemreductions-cli/src/dispatch.rs | 2 + problemreductions-cli/src/problem_name.rs | 2 + src/lib.rs | 3 +- src/models/graph/min_sum_multicenter.rs | 243 ++++++++++++++++++ src/models/graph/mod.rs | 3 + src/models/mod.rs | 4 +- .../models/graph/min_sum_multicenter.rs | 241 +++++++++++++++++ 8 files changed, 522 insertions(+), 3 deletions(-) create mode 100644 src/models/graph/min_sum_multicenter.rs create mode 100644 src/unit_tests/models/graph/min_sum_multicenter.rs diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 2b4cb04b3..ed89c91e5 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -89,6 +89,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "QUBO" => "--matrix \"1,0.5;0.5,2\"", "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", + "MinSumMulticenter" => "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2", "Factoring" => "--target 15 --m 4 --n 4", "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", _ => "", @@ -502,6 +503,32 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // MinSumMulticenter (p-median) + "MinSumMulticenter" => { + let (graph, n) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create MinSumMulticenter --graph 0-1,1-2,2-3 [--weights 1,1,1,1] [--edge-weights 1,1,1] --k 2" + ) + })?; + let vertex_weights = parse_vertex_weights(args, n)?; + let edge_lengths = parse_edge_weights(args, graph.num_edges())?; + let k = args.k.ok_or_else(|| { + anyhow::anyhow!( + "MinSumMulticenter requires --k (number of centers)\n\n\ + Usage: pred create MinSumMulticenter --graph 0-1,1-2,2-3 --k 2" + ) + })?; + ( + ser(MinSumMulticenter::new( + graph, + vertex_weights, + edge_lengths, + k, + ))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackVertexSet "MinimumFeedbackVertexSet" => { let arcs_str = args.arcs.as_ref().ok_or_else(|| { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index e162efc24..389382443 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -210,6 +210,7 @@ pub fn load_problem( "MaximumClique" => deser_opt::>(data), "MaximumMatching" => deser_opt::>(data), "MinimumDominatingSet" => deser_opt::>(data), + "MinSumMulticenter" => deser_opt::>(data), "GraphPartitioning" => deser_opt::>(data), "MaxCut" => deser_opt::>(data), "MaximalIS" => deser_opt::>(data), @@ -271,6 +272,7 @@ pub fn serialize_any_problem( "MaximumClique" => try_ser::>(any), "MaximumMatching" => try_ser::>(any), "MinimumDominatingSet" => try_ser::>(any), + "MinSumMulticenter" => try_ser::>(any), "GraphPartitioning" => try_ser::>(any), "MaxCut" => try_ser::>(any), "MaximalIS" => try_ser::>(any), diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index a595f61b6..0769915b0 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -23,6 +23,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("LCS", "LongestCommonSubsequence"), ("MaxMatching", "MaximumMatching"), ("FVS", "MinimumFeedbackVertexSet"), + ("pmedian", "MinSumMulticenter"), ]; /// Resolve a short alias to the canonical problem name. @@ -57,6 +58,7 @@ pub fn resolve_alias(input: &str) -> String { "knapsack" => "Knapsack".to_string(), "lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), + "minsummulticenter" | "pmedian" => "MinSumMulticenter".to_string(), "subsetsum" => "SubsetSum".to_string(), _ => input.to_string(), // pass-through for exact names } diff --git a/src/lib.rs b/src/lib.rs index bdcbf5f32..0d25349c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,8 @@ pub mod prelude { pub use crate::models::graph::{BicliqueCover, GraphPartitioning, SpinGlass}; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman, + MinSumMulticenter, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, + TravelingSalesman, }; pub use crate::models::misc::{ BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum, diff --git a/src/models/graph/min_sum_multicenter.rs b/src/models/graph/min_sum_multicenter.rs new file mode 100644 index 000000000..7c0d2efc2 --- /dev/null +++ b/src/models/graph/min_sum_multicenter.rs @@ -0,0 +1,243 @@ +//! Min-Sum Multicenter (p-median) problem implementation. +//! +//! The p-median problem asks for K facility locations (centers) on a graph +//! that minimize the total weighted distance from all vertices to their nearest center. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize, WeightElement}; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "MinSumMulticenter", + module_path: module_path!(), + description: "Find K centers minimizing total weighted distance (p-median problem)", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, + FieldInfo { name: "vertex_weights", type_name: "Vec", description: "Vertex weights w: V -> R" }, + FieldInfo { name: "edge_lengths", type_name: "Vec", description: "Edge lengths l: E -> R" }, + FieldInfo { name: "k", type_name: "usize", description: "Number of centers to place" }, + ], + } +} + +/// The Min-Sum Multicenter (p-median) problem. +/// +/// Given a graph G = (V, E) with vertex weights w(v) and edge lengths l(e), +/// find a subset P ⊆ V of K vertices (centers) that minimizes the total +/// weighted distance Σ_{v ∈ V} w(v) · d(v, P), where d(v, P) is the +/// shortest-path distance from v to the nearest center in P. +/// +/// # Type Parameters +/// +/// * `G` - The graph type (e.g., `SimpleGraph`) +/// * `W` - The weight/length type (e.g., `i32`, `One`) +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::graph::MinSumMulticenter; +/// use problemreductions::topology::SimpleGraph; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // Path graph: 0-1-2, unit weights and lengths, K=1 +/// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); +/// let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); +/// +/// let solver = BruteForce::new(); +/// let solution = solver.find_best(&problem).unwrap(); +/// // Center at vertex 1 gives total distance 0+1+1 = 2 (optimal) +/// assert_eq!(solution, vec![0, 1, 0]); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinSumMulticenter { + /// The underlying graph. + graph: G, + /// Non-negative weight for each vertex. + vertex_weights: Vec, + /// Non-negative length for each edge (in edge index order). + edge_lengths: Vec, + /// Number of centers to place. + k: usize, +} + +impl MinSumMulticenter { + /// Create a MinSumMulticenter problem. + /// + /// # Panics + /// - If `vertex_weights.len() != graph.num_vertices()` + /// - If `edge_lengths.len() != graph.num_edges()` + /// - If `k == 0` or `k > graph.num_vertices()` + pub fn new(graph: G, vertex_weights: Vec, edge_lengths: Vec, k: usize) -> Self { + assert_eq!( + vertex_weights.len(), + graph.num_vertices(), + "vertex_weights length must match num_vertices" + ); + assert_eq!( + edge_lengths.len(), + graph.num_edges(), + "edge_lengths length must match num_edges" + ); + assert!(k > 0, "k must be positive"); + assert!(k <= graph.num_vertices(), "k must not exceed num_vertices"); + Self { + graph, + vertex_weights, + edge_lengths, + k, + } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get a reference to the vertex weights. + pub fn vertex_weights(&self) -> &[W] { + &self.vertex_weights + } + + /// Get a reference to the edge lengths. + pub fn edge_lengths(&self) -> &[W] { + &self.edge_lengths + } + + /// Get the number of centers K. + pub fn k(&self) -> usize { + self.k + } +} + +impl MinSumMulticenter { + /// Get the number of vertices in the underlying graph. + pub fn num_vertices(&self) -> usize { + self.graph().num_vertices() + } + + /// Get the number of edges in the underlying graph. + pub fn num_edges(&self) -> usize { + self.graph().num_edges() + } + + /// Get the number of centers K. + pub fn num_centers(&self) -> usize { + self.k + } + + /// Compute shortest distances from each vertex to the nearest center. + /// + /// Uses multi-source Dijkstra: initializes all centers at distance 0 + /// and relaxes edges using edge lengths. + /// + /// Returns `None` if any vertex is unreachable from all centers. + fn shortest_distances(&self, config: &[usize]) -> Option> { + let n = self.graph.num_vertices(); + let edges = self.graph.edges(); + + // Build adjacency list: for each vertex, list of (neighbor, edge_length) + let mut adj: Vec> = vec![Vec::new(); n]; + for (idx, &(u, v)) in edges.iter().enumerate() { + let len = self.edge_lengths[idx].to_sum(); + adj[u].push((v, len.clone())); + adj[v].push((u, len)); + } + + // Multi-source Bellman-Ford: initialize all centers at distance 0 + let mut dist: Vec> = vec![None; n]; + for (v, &selected) in config.iter().enumerate() { + if selected == 1 { + dist[v] = Some(W::Sum::zero()); + } + } + + // Repeated relaxation until convergence (Bellman-Ford style) + let mut changed = true; + while changed { + changed = false; + for v in 0..n { + let dv = match &dist[v] { + Some(d) => d.clone(), + None => continue, + }; + for &(u, ref len) in &adj[v] { + let new_dist = dv.clone() + len.clone(); + let update = match &dist[u] { + None => true, + Some(du) => new_dist < *du, + }; + if update { + dist[u] = Some(new_dist); + changed = true; + } + } + } + } + + // Collect distances, returning None if any vertex is unreachable + dist.into_iter().collect() + } +} + +impl Problem for MinSumMulticenter +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "MinSumMulticenter"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G, W] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_vertices()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + // Check exactly K centers are selected + let num_selected: usize = config.iter().sum(); + if num_selected != self.k { + return SolutionSize::Invalid; + } + + // Compute shortest distances to nearest center + let distances = match self.shortest_distances(config) { + Some(d) => d, + None => return SolutionSize::Invalid, + }; + + // Compute total weighted distance: Σ w(v) * d(v) + let mut total = W::Sum::zero(); + for (v, dist) in distances.iter().enumerate() { + total += self.vertex_weights[v].to_sum() * dist.clone(); + } + + SolutionSize::Valid(total) + } +} + +impl OptimizationProblem for MinSumMulticenter +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + type Value = W::Sum; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +crate::declare_variants! { + MinSumMulticenter => "2^num_vertices", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/min_sum_multicenter.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 42f46a155..eb84a6b05 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -14,6 +14,7 @@ //! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle) //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`BicliqueCover`]: Biclique cover on bipartite graphs +//! - [`MinSumMulticenter`]: Min-sum multicenter (p-median) pub(crate) mod biclique_cover; pub(crate) mod graph_partitioning; @@ -23,6 +24,7 @@ pub(crate) mod maximal_is; pub(crate) mod maximum_clique; pub(crate) mod maximum_independent_set; pub(crate) mod maximum_matching; +pub(crate) mod min_sum_multicenter; pub(crate) mod minimum_dominating_set; pub(crate) mod minimum_feedback_vertex_set; pub(crate) mod minimum_vertex_cover; @@ -37,6 +39,7 @@ pub use maximal_is::MaximalIS; pub use maximum_clique::MaximumClique; pub use maximum_independent_set::MaximumIndependentSet; pub use maximum_matching::MaximumMatching; +pub use min_sum_multicenter::MinSumMulticenter; pub use minimum_dominating_set::MinimumDominatingSet; pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet; pub use minimum_vertex_cover::MinimumVertexCover; diff --git a/src/models/mod.rs b/src/models/mod.rs index 6c8ac38a7..c500d74da 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -13,8 +13,8 @@ pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO}; pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use graph::{ BicliqueCover, GraphPartitioning, KColoring, MaxCut, MaximalIS, MaximumClique, - MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, - MinimumVertexCover, SpinGlass, TravelingSalesman, + MaximumIndependentSet, MaximumMatching, MinSumMulticenter, MinimumDominatingSet, + MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; pub use misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/graph/min_sum_multicenter.rs b/src/unit_tests/models/graph/min_sum_multicenter.rs new file mode 100644 index 000000000..eb42c34bc --- /dev/null +++ b/src/unit_tests/models/graph/min_sum_multicenter.rs @@ -0,0 +1,241 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::SimpleGraph; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::Direction; + +#[test] +fn test_min_sum_multicenter_creation() { + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 4], vec![1i32; 3], 2); + assert_eq!(problem.graph().num_vertices(), 4); + assert_eq!(problem.graph().num_edges(), 3); + assert_eq!(problem.k(), 2); + assert_eq!(problem.vertex_weights(), &[1, 1, 1, 1]); + assert_eq!(problem.edge_lengths(), &[1, 1, 1]); +} + +#[test] +fn test_min_sum_multicenter_size_getters() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 5], vec![1i32; 4], 2); + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_edges(), 4); + assert_eq!(problem.num_centers(), 2); +} + +#[test] +fn test_min_sum_multicenter_direction() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); + assert_eq!(problem.direction(), Direction::Minimize); +} + +#[test] +fn test_min_sum_multicenter_evaluate_path() { + // Path: 0-1-2, unit weights and lengths, K=1 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); + + // Center at vertex 1: distances = [1, 0, 1], total = 2 + let result = problem.evaluate(&[0, 1, 0]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 2); + + // Center at vertex 0: distances = [0, 1, 2], total = 3 + let result = problem.evaluate(&[1, 0, 0]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 3); +} + +#[test] +fn test_min_sum_multicenter_wrong_k() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 2); + + // Only 1 center selected when K=2 + let result = problem.evaluate(&[0, 1, 0]); + assert!(!result.is_valid()); + + // 3 centers selected when K=2 + let result = problem.evaluate(&[1, 1, 1]); + assert!(!result.is_valid()); + + // No centers selected + let result = problem.evaluate(&[0, 0, 0]); + assert!(!result.is_valid()); +} + +#[test] +fn test_min_sum_multicenter_weighted() { + // Path: 0-1-2, vertex weights = [3, 1, 2], edge lengths = [1, 1], K=1 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinSumMulticenter::new(graph, vec![3i32, 1, 2], vec![1i32; 2], 1); + + // Center at 0: distances = [0, 1, 2], total = 3*0 + 1*1 + 2*2 = 5 + assert_eq!(problem.evaluate(&[1, 0, 0]).unwrap(), 5); + + // Center at 1: distances = [1, 0, 1], total = 3*1 + 1*0 + 2*1 = 5 + assert_eq!(problem.evaluate(&[0, 1, 0]).unwrap(), 5); + + // Center at 2: distances = [2, 1, 0], total = 3*2 + 1*1 + 2*0 = 7 + assert_eq!(problem.evaluate(&[0, 0, 1]).unwrap(), 7); +} + +#[test] +fn test_min_sum_multicenter_weighted_edges() { + // Triangle: 0-1 (len 1), 1-2 (len 3), 0-2 (len 2), K=1 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1, 3, 2], 1); + + // Center at 0: d(0)=0, d(1)=1, d(2)=2, total=3 + assert_eq!(problem.evaluate(&[1, 0, 0]).unwrap(), 3); + + // Center at 1: d(1)=0, d(0)=1, d(2)=min(3, 1+2)=3, total=4 + assert_eq!(problem.evaluate(&[0, 1, 0]).unwrap(), 4); +} + +#[test] +fn test_min_sum_multicenter_two_centers() { + // Path: 0-1-2-3-4, unit weights and lengths, K=2 + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 5], vec![1i32; 4], 2); + + // Centers at {1, 3}: d = [1, 0, 1, 0, 1], total = 3 + assert_eq!(problem.evaluate(&[0, 1, 0, 1, 0]).unwrap(), 3); + + // Centers at {0, 4}: d = [0, 1, 2, 1, 0], total = 4 + assert_eq!(problem.evaluate(&[1, 0, 0, 0, 1]).unwrap(), 4); +} + +#[test] +fn test_min_sum_multicenter_solver() { + // Issue example: 7 vertices, 8 edges, unit weights, K=2 + let graph = SimpleGraph::new( + 7, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (0, 6), + (2, 5), + ], + ); + let problem = MinSumMulticenter::new(graph, vec![1i32; 7], vec![1i32; 8], 2); + + let solver = BruteForce::new(); + let best = solver.find_best(&problem).unwrap(); + let best_cost = problem.evaluate(&best).unwrap(); + + // Optimal cost should be 6 (centers at {2, 5}) + assert_eq!(best_cost, 6); +} + +#[test] +fn test_min_sum_multicenter_disconnected() { + // Two disconnected components: 0-1 and 2-3, K=1 + let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 4], vec![1i32; 2], 1); + + // Center at 0: vertex 2 and 3 are unreachable + let result = problem.evaluate(&[1, 0, 0, 0]); + assert!(!result.is_valid()); + + // With K=2, centers at {0, 2}: all reachable + let graph2 = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); + let problem2 = MinSumMulticenter::new(graph2, vec![1i32; 4], vec![1i32; 2], 2); + let result2 = problem2.evaluate(&[1, 0, 1, 0]); + assert!(result2.is_valid()); + assert_eq!(result2.unwrap(), 2); // d = [0, 1, 0, 1] +} + +#[test] +fn test_min_sum_multicenter_single_vertex() { + let graph = SimpleGraph::new(1, vec![]); + let problem = MinSumMulticenter::new(graph, vec![5i32], vec![], 1); + let result = problem.evaluate(&[1]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 0); // Only vertex is the center, distance = 0 +} + +#[test] +fn test_min_sum_multicenter_all_centers() { + // K = num_vertices: all vertices are centers, total distance = 0 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 3); + let result = problem.evaluate(&[1, 1, 1]); + assert!(result.is_valid()); + assert_eq!(result.unwrap(), 0); +} + +#[test] +#[should_panic(expected = "vertex_weights length must match num_vertices")] +fn test_min_sum_multicenter_wrong_vertex_weights_len() { + let graph = SimpleGraph::new(3, vec![(0, 1)]); + MinSumMulticenter::new(graph, vec![1i32; 2], vec![1i32; 1], 1); +} + +#[test] +#[should_panic(expected = "edge_lengths length must match num_edges")] +fn test_min_sum_multicenter_wrong_edge_lengths_len() { + let graph = SimpleGraph::new(3, vec![(0, 1)]); + MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); +} + +#[test] +#[should_panic(expected = "k must be positive")] +fn test_min_sum_multicenter_k_zero() { + let graph = SimpleGraph::new(3, vec![(0, 1)]); + MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 1], 0); +} + +#[test] +#[should_panic(expected = "k must not exceed num_vertices")] +fn test_min_sum_multicenter_k_too_large() { + let graph = SimpleGraph::new(3, vec![(0, 1)]); + MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 1], 4); +} + +#[test] +fn test_min_sum_multicenter_dims() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 5], vec![1i32; 4], 2); + assert_eq!(problem.dims(), vec![2; 5]); +} + +#[test] +fn test_min_sum_multicenter_find_all_best() { + // Path: 0-1-2, unit weights, K=1. Center at 1 is optimal (cost 2) + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); + + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + assert_eq!(solutions.len(), 1); + assert_eq!(solutions[0], vec![0, 1, 0]); +} + +#[test] +fn test_min_sum_multicenter_serialization() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); + + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: MinSumMulticenter = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.graph().num_vertices(), 3); + assert_eq!(deserialized.graph().num_edges(), 2); + assert_eq!(deserialized.vertex_weights(), &[1, 1, 1]); + assert_eq!(deserialized.edge_lengths(), &[1, 1]); + assert_eq!(deserialized.k(), 1); + + // Verify evaluation produces same results + let config = vec![0, 1, 0]; + assert_eq!( + problem.evaluate(&config).unwrap(), + deserialized.evaluate(&config).unwrap() + ); +} From 2beb79abbd0aadabbaff037213274374f6e5d6c1 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 12:08:48 +0000 Subject: [PATCH 3/7] chore: remove plan file after implementation --- docs/plans/2026-03-13-minsummulticenter.md | 99 ---------------------- 1 file changed, 99 deletions(-) delete mode 100644 docs/plans/2026-03-13-minsummulticenter.md diff --git a/docs/plans/2026-03-13-minsummulticenter.md b/docs/plans/2026-03-13-minsummulticenter.md deleted file mode 100644 index 12114450a..000000000 --- a/docs/plans/2026-03-13-minsummulticenter.md +++ /dev/null @@ -1,99 +0,0 @@ -# Plan: Add MinSumMulticenter Model (#399) - -## Overview - -Add the MinSumMulticenter (p-median) problem model — a facility location optimization problem that minimizes total weighted distance from vertices to K selected centers. - -**Design decision:** Implement as an **optimization problem** (Metric = SolutionSize, Direction::Minimize), consistent with how MinimumDominatingSet, MinimumVertexCover, etc. are implemented despite GJ defining them as decision problems. The bound B is not stored; brute force finds optimal solutions directly. - -## Step 1: Create model file `src/models/graph/min_sum_multicenter.rs` - -### Struct -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MinSumMulticenter { - graph: G, - vertex_weights: Vec, - edge_lengths: Vec, - k: usize, -} -``` - -### Constructor & Accessors -- `new(graph, vertex_weights, edge_lengths, k)` — assert vertex_weights.len() == num_vertices, edge_lengths.len() == num_edges, k <= num_vertices, k > 0 -- `graph()`, `vertex_weights()`, `edge_lengths()`, `k()` getters -- `num_vertices()`, `num_edges()`, `num_centers()` size getters (in WeightElement-bounded impl block) - -### Shortest Path Computation -Implement a private method `shortest_distances_to_centers(&self, config: &[usize]) -> Option>`: -- Multi-source BFS-like approach using a priority mechanism -- Build adjacency list with edge lengths from `graph.edges()` and `edge_lengths` -- Use a simple Dijkstra-like algorithm from all selected centers simultaneously -- Returns None if any vertex is unreachable (disconnected graph) -- Returns Some(distances) otherwise - -For the evaluate function: -1. Count selected centers; if != k, return Invalid -2. Compute shortest distances from each vertex to nearest center -3. If any vertex unreachable, return Invalid -4. Compute Σ vertex_weights[v].to_sum() * distances[v] for all v -5. Return SolutionSize::Valid(total) - -### Problem Trait -```rust -const NAME: &'static str = "MinSumMulticenter"; -type Metric = SolutionSize; -fn dims(&self) -> Vec { vec![2; num_vertices] } -fn variant() -> ... { variant_params![G, W] } -``` - -### OptimizationProblem -- Direction::Minimize -- type Value = W::Sum - -### declare_variants! -```rust -MinSumMulticenter => "2^num_vertices", -``` -Note: No specialized exact algorithm improves on brute-force C(n,K) enumeration for general p-median. - -### Schema Registration -Register with `inventory::submit!` including fields: graph, vertex_weights, edge_lengths, k. - -## Step 2: Register module - -- `src/models/graph/mod.rs` — add `pub(crate) mod min_sum_multicenter;` and `pub use min_sum_multicenter::MinSumMulticenter;` -- `src/models/mod.rs` — add `MinSumMulticenter` to graph re-exports -- `src/lib.rs` — add to prelude - -## Step 3: Create unit tests `src/unit_tests/models/graph/min_sum_multicenter.rs` - -Link via `#[path]` in model file. Tests: -- `test_min_sum_multicenter_creation` — basic construction, getters -- `test_min_sum_multicenter_evaluate` — manual evaluation of known configs -- `test_min_sum_multicenter_invalid_k` — wrong number of centers returns Invalid -- `test_min_sum_multicenter_solver` — BruteForce finds optimal for small instance -- `test_min_sum_multicenter_disconnected` — unreachable vertex returns Invalid -- `test_min_sum_multicenter_direction` — Direction::Minimize -- `test_min_sum_multicenter_size_getters` — num_vertices, num_edges, num_centers - -Use the example from the issue: 7 vertices, 8 edges, unit weights/lengths, K=2. Optimal centers at {2, 5} with total cost 6. - -## Step 4: Register in CLI `problemreductions-cli/src/commands/create.rs` - -Add custom handler for "MinSumMulticenter" that accepts: -- `--graph` / `--edges` (graph topology) -- `--vertex-weights` (vertex weights) -- `--edge-lengths` (edge lengths) -- `--k` (number of centers) - -Add to `example_for()` and the main match dispatch. - -## Step 5: Add unit test mod.rs registration - -- `src/unit_tests/models/graph/mod.rs` — add `mod min_sum_multicenter;` - -## Step 6: Verify - -- `make fmt && make clippy && make test` -- Verify the example instance gives expected results From 62ea2238672904bfc7f4ebaa2955832a2c71664e Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 15:45:48 +0000 Subject: [PATCH 4/7] Address Copilot review comments for MinSumMulticenter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix docstring: "multi-source Dijkstra" → "multi-source Bellman-Ford" - Add iteration cap (n-1) to prevent non-termination with negative edges - Fix potential borrow issue: clone du instead of dereferencing - Add MCP tools support (create_problem_inner + create_random_inner) - Regenerate problem_schemas.json to include MinSumMulticenter Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/problem_schemas.json | 37 ++++++++++++ problemreductions-cli/src/mcp/tools.rs | 73 +++++++++++++++++++++++- src/models/graph/min_sum_multicenter.rs | 17 +++--- 3 files changed, 117 insertions(+), 10 deletions(-) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 793fcc1f1..3a51f7747 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -302,6 +302,32 @@ } ] }, + { + "name": "MinSumMulticenter", + "description": "Find K centers minimizing total weighted distance (p-median problem)", + "fields": [ + { + "name": "graph", + "type_name": "G", + "description": "The underlying graph G=(V,E)" + }, + { + "name": "vertex_weights", + "type_name": "Vec", + "description": "Vertex weights w: V -> R" + }, + { + "name": "edge_lengths", + "type_name": "Vec", + "description": "Edge lengths l: E -> R" + }, + { + "name": "k", + "type_name": "usize", + "description": "Number of centers to place" + } + ] + }, { "name": "MinimumDominatingSet", "description": "Find minimum weight dominating set in a graph", @@ -382,6 +408,17 @@ } ] }, + { + "name": "PartitionIntoTriangles", + "description": "Partition vertices into triangles (K3 subgraphs)", + "fields": [ + { + "name": "graph", + "type_name": "G", + "description": "The underlying graph G=(V,E) with |V| divisible by 3" + } + ] + }, { "name": "QUBO", "description": "Minimize quadratic unconstrained binary objective", diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 9f478a150..1a6428041 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -2,8 +2,8 @@ use crate::util; use problemreductions::models::algebraic::QUBO; use problemreductions::models::formula::{CNFClause, Satisfiability}; use problemreductions::models::graph::{ - MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, - MinimumVertexCover, SpinGlass, TravelingSalesman, + MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinSumMulticenter, + MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; use problemreductions::models::misc::Factoring; use problemreductions::registry::collect_schemas; @@ -514,6 +514,25 @@ impl McpServer { (ser(Factoring::new(bits_m, bits_n, target))?, variant) } + // MinSumMulticenter (p-median) + "MinSumMulticenter" => { + let (graph, n) = parse_graph_from_params(params)?; + let vertex_weights = parse_vertex_weights_from_params(params, n)?; + let edge_lengths = parse_edge_lengths_from_params(params, graph.num_edges())?; + let k = params + .get("k") + .and_then(|v| v.as_u64()) + .map(|v| v as usize) + .ok_or_else(|| { + anyhow::anyhow!("MinSumMulticenter requires 'k' (number of centers)") + })?; + let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); + ( + ser(MinSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?, + variant, + ) + } + _ => anyhow::bail!("{}", unknown_problem_error(&canonical)), }; @@ -634,10 +653,34 @@ impl McpServer { util::validate_k_param(resolved_variant, k_flag, Some(3), "KColoring")?; util::ser_kcoloring(graph, k)? } + "MinSumMulticenter" => { + let edge_prob = params + .get("edge_prob") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5); + if !(0.0..=1.0).contains(&edge_prob) { + anyhow::bail!("edge_prob must be between 0.0 and 1.0"); + } + let graph = util::create_random_graph(num_vertices, edge_prob, seed); + let num_edges = graph.num_edges(); + let vertex_weights = vec![1i32; num_vertices]; + let edge_lengths = vec![1i32; num_edges]; + let k = params + .get("k") + .and_then(|v| v.as_u64()) + .map(|v| v as usize) + .unwrap_or(1.max(num_vertices / 3)); + let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); + ( + ser(MinSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?, + variant, + ) + } _ => anyhow::bail!( "Random generation is not supported for {}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ - MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman)", + MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, \ + TravelingSalesman, MinSumMulticenter)", canonical ), }; @@ -1294,6 +1337,30 @@ fn parse_edge_weights_from_params( } } +/// Parse `edge_lengths` field from JSON params as edge lengths (i32), defaulting to all 1s. +fn parse_edge_lengths_from_params( + params: &serde_json::Value, + num_edges: usize, +) -> anyhow::Result> { + match params.get("edge_lengths").and_then(|v| v.as_str()) { + Some(w) => { + let lengths: Vec = w + .split(',') + .map(|s| s.trim().parse::()) + .collect::, _>>()?; + if lengths.len() != num_edges { + anyhow::bail!( + "Expected {} edge lengths but got {}", + num_edges, + lengths.len() + ); + } + Ok(lengths) + } + None => Ok(vec![1i32; num_edges]), + } +} + /// Parse `clauses` field from JSON params as semicolon-separated clauses. fn parse_clauses_from_params(params: &serde_json::Value) -> anyhow::Result> { let clauses_str = params diff --git a/src/models/graph/min_sum_multicenter.rs b/src/models/graph/min_sum_multicenter.rs index 7c0d2efc2..61fc7b4ee 100644 --- a/src/models/graph/min_sum_multicenter.rs +++ b/src/models/graph/min_sum_multicenter.rs @@ -131,8 +131,9 @@ impl MinSumMulticenter { /// Compute shortest distances from each vertex to the nearest center. /// - /// Uses multi-source Dijkstra: initializes all centers at distance 0 - /// and relaxes edges using edge lengths. + /// Uses multi-source Bellman-Ford: initializes all centers at distance 0 + /// and relaxes edges using edge lengths. Terminates after at most `n - 1` + /// iterations (the standard Bellman-Ford bound). /// /// Returns `None` if any vertex is unreachable from all centers. fn shortest_distances(&self, config: &[usize]) -> Option> { @@ -155,10 +156,9 @@ impl MinSumMulticenter { } } - // Repeated relaxation until convergence (Bellman-Ford style) - let mut changed = true; - while changed { - changed = false; + // Bellman-Ford relaxation with iteration cap (at most n-1 iterations) + for _ in 0..n.saturating_sub(1) { + let mut changed = false; for v in 0..n { let dv = match &dist[v] { Some(d) => d.clone(), @@ -168,7 +168,7 @@ impl MinSumMulticenter { let new_dist = dv.clone() + len.clone(); let update = match &dist[u] { None => true, - Some(du) => new_dist < *du, + Some(du) => new_dist < du.clone(), }; if update { dist[u] = Some(new_dist); @@ -176,6 +176,9 @@ impl MinSumMulticenter { } } } + if !changed { + break; + } } // Collect distances, returning None if any vertex is unreachable From 9b72d41bc4df38d8840c4f897800d4f5ebf1d5fe Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 15:47:48 +0000 Subject: [PATCH 5/7] Rename MinSumMulticenter to MinimumSumMulticenter Per issue #399 and human review comment, the naming convention requires the full "Minimum" prefix (matching MinimumVertexCover, MinimumDominatingSet, etc.). Rename struct, files, module paths, CLI aliases, and regenerate schemas. Co-Authored-By: Claude Opus 4.6 --- docs/src/reductions/problem_schemas.json | 52 +++++++++---------- problemreductions-cli/src/commands/create.rs | 14 ++--- problemreductions-cli/src/dispatch.rs | 4 +- problemreductions-cli/src/mcp/tools.rs | 16 +++--- problemreductions-cli/src/problem_name.rs | 4 +- src/lib.rs | 2 +- ...ticenter.rs => minimum_sum_multicenter.rs} | 24 ++++----- src/models/graph/mod.rs | 6 +-- src/models/mod.rs | 2 +- ...ticenter.rs => minimum_sum_multicenter.rs} | 42 +++++++-------- 10 files changed, 83 insertions(+), 83 deletions(-) rename src/models/graph/{min_sum_multicenter.rs => minimum_sum_multicenter.rs} (91%) rename src/unit_tests/models/graph/{min_sum_multicenter.rs => minimum_sum_multicenter.rs} (79%) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 3a51f7747..6358e5906 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -302,32 +302,6 @@ } ] }, - { - "name": "MinSumMulticenter", - "description": "Find K centers minimizing total weighted distance (p-median problem)", - "fields": [ - { - "name": "graph", - "type_name": "G", - "description": "The underlying graph G=(V,E)" - }, - { - "name": "vertex_weights", - "type_name": "Vec", - "description": "Vertex weights w: V -> R" - }, - { - "name": "edge_lengths", - "type_name": "Vec", - "description": "Edge lengths l: E -> R" - }, - { - "name": "k", - "type_name": "usize", - "description": "Number of centers to place" - } - ] - }, { "name": "MinimumDominatingSet", "description": "Find minimum weight dominating set in a graph", @@ -381,6 +355,32 @@ } ] }, + { + "name": "MinimumSumMulticenter", + "description": "Find K centers minimizing total weighted distance (p-median problem)", + "fields": [ + { + "name": "graph", + "type_name": "G", + "description": "The underlying graph G=(V,E)" + }, + { + "name": "vertex_weights", + "type_name": "Vec", + "description": "Vertex weights w: V -> R" + }, + { + "name": "edge_lengths", + "type_name": "Vec", + "description": "Edge lengths l: E -> R" + }, + { + "name": "k", + "type_name": "usize", + "description": "Number of centers to place" + } + ] + }, { "name": "MinimumVertexCover", "description": "Find minimum weight vertex cover in a graph", diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index f6c8a43dd..e8558a91e 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -92,7 +92,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "QUBO" => "--matrix \"1,0.5;0.5,2\"", "SpinGlass" => "--graph 0-1,1-2 --couplings 1,1", "KColoring" => "--graph 0-1,1-2,2-0 --k 3", - "MinSumMulticenter" => "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2", + "MinimumSumMulticenter" => "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2", "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", "Factoring" => "--target 15 --m 4 --n 4", "RuralPostman" => { @@ -543,23 +543,23 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } - // MinSumMulticenter (p-median) - "MinSumMulticenter" => { + // MinimumSumMulticenter (p-median) + "MinimumSumMulticenter" => { let (graph, n) = parse_graph(args).map_err(|e| { anyhow::anyhow!( - "{e}\n\nUsage: pred create MinSumMulticenter --graph 0-1,1-2,2-3 [--weights 1,1,1,1] [--edge-weights 1,1,1] --k 2" + "{e}\n\nUsage: pred create MinimumSumMulticenter --graph 0-1,1-2,2-3 [--weights 1,1,1,1] [--edge-weights 1,1,1] --k 2" ) })?; let vertex_weights = parse_vertex_weights(args, n)?; let edge_lengths = parse_edge_weights(args, graph.num_edges())?; let k = args.k.ok_or_else(|| { anyhow::anyhow!( - "MinSumMulticenter requires --k (number of centers)\n\n\ - Usage: pred create MinSumMulticenter --graph 0-1,1-2,2-3 --k 2" + "MinimumSumMulticenter requires --k (number of centers)\n\n\ + Usage: pred create MinimumSumMulticenter --graph 0-1,1-2,2-3 --k 2" ) })?; ( - ser(MinSumMulticenter::new( + ser(MinimumSumMulticenter::new( graph, vertex_weights, edge_lengths, diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 713f76a3c..9fedf77ef 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -210,7 +210,7 @@ pub fn load_problem( "MaximumClique" => deser_opt::>(data), "MaximumMatching" => deser_opt::>(data), "MinimumDominatingSet" => deser_opt::>(data), - "MinSumMulticenter" => deser_opt::>(data), + "MinimumSumMulticenter" => deser_opt::>(data), "GraphPartitioning" => deser_opt::>(data), "MaxCut" => deser_opt::>(data), "MaximalIS" => deser_opt::>(data), @@ -275,7 +275,7 @@ pub fn serialize_any_problem( "MaximumClique" => try_ser::>(any), "MaximumMatching" => try_ser::>(any), "MinimumDominatingSet" => try_ser::>(any), - "MinSumMulticenter" => try_ser::>(any), + "MinimumSumMulticenter" => try_ser::>(any), "GraphPartitioning" => try_ser::>(any), "MaxCut" => try_ser::>(any), "MaximalIS" => try_ser::>(any), diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 1a6428041..2933149fb 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -2,7 +2,7 @@ use crate::util; use problemreductions::models::algebraic::QUBO; use problemreductions::models::formula::{CNFClause, Satisfiability}; use problemreductions::models::graph::{ - MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinSumMulticenter, + MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumSumMulticenter, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; use problemreductions::models::misc::Factoring; @@ -514,8 +514,8 @@ impl McpServer { (ser(Factoring::new(bits_m, bits_n, target))?, variant) } - // MinSumMulticenter (p-median) - "MinSumMulticenter" => { + // MinimumSumMulticenter (p-median) + "MinimumSumMulticenter" => { let (graph, n) = parse_graph_from_params(params)?; let vertex_weights = parse_vertex_weights_from_params(params, n)?; let edge_lengths = parse_edge_lengths_from_params(params, graph.num_edges())?; @@ -524,11 +524,11 @@ impl McpServer { .and_then(|v| v.as_u64()) .map(|v| v as usize) .ok_or_else(|| { - anyhow::anyhow!("MinSumMulticenter requires 'k' (number of centers)") + anyhow::anyhow!("MinimumSumMulticenter requires 'k' (number of centers)") })?; let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); ( - ser(MinSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?, + ser(MinimumSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?, variant, ) } @@ -653,7 +653,7 @@ impl McpServer { util::validate_k_param(resolved_variant, k_flag, Some(3), "KColoring")?; util::ser_kcoloring(graph, k)? } - "MinSumMulticenter" => { + "MinimumSumMulticenter" => { let edge_prob = params .get("edge_prob") .and_then(|v| v.as_f64()) @@ -672,7 +672,7 @@ impl McpServer { .unwrap_or(1.max(num_vertices / 3)); let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); ( - ser(MinSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?, + ser(MinimumSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?, variant, ) } @@ -680,7 +680,7 @@ impl McpServer { "Random generation is not supported for {}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, \ - TravelingSalesman, MinSumMulticenter)", + TravelingSalesman, MinimumSumMulticenter)", canonical ), }; diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 4e2b4f421..bd91d12db 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -24,7 +24,7 @@ pub const ALIASES: &[(&str, &str)] = &[ ("LCS", "LongestCommonSubsequence"), ("MaxMatching", "MaximumMatching"), ("FVS", "MinimumFeedbackVertexSet"), - ("pmedian", "MinSumMulticenter"), + ("pmedian", "MinimumSumMulticenter"), ]; /// Resolve a short alias to the canonical problem name. @@ -62,7 +62,7 @@ pub fn resolve_alias(input: &str) -> String { "partitionintotriangles" => "PartitionIntoTriangles".to_string(), "lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), - "minsummulticenter" | "pmedian" => "MinSumMulticenter".to_string(), + "minimumsummulticenter" | "pmedian" => "MinimumSumMulticenter".to_string(), "subsetsum" => "SubsetSum".to_string(), _ => input.to_string(), // pass-through for exact names } diff --git a/src/lib.rs b/src/lib.rs index 92298feb0..40ee3940b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,7 +45,7 @@ pub mod prelude { }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinSumMulticenter, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, + MinimumSumMulticenter, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, PartitionIntoTriangles, RuralPostman, TravelingSalesman, }; pub use crate::models::misc::{ diff --git a/src/models/graph/min_sum_multicenter.rs b/src/models/graph/minimum_sum_multicenter.rs similarity index 91% rename from src/models/graph/min_sum_multicenter.rs rename to src/models/graph/minimum_sum_multicenter.rs index 61fc7b4ee..6781b468d 100644 --- a/src/models/graph/min_sum_multicenter.rs +++ b/src/models/graph/minimum_sum_multicenter.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; inventory::submit! { ProblemSchemaEntry { - name: "MinSumMulticenter", + name: "MinimumSumMulticenter", module_path: module_path!(), description: "Find K centers minimizing total weighted distance (p-median problem)", fields: &[ @@ -39,13 +39,13 @@ inventory::submit! { /// # Example /// /// ``` -/// use problemreductions::models::graph::MinSumMulticenter; +/// use problemreductions::models::graph::MinimumSumMulticenter; /// use problemreductions::topology::SimpleGraph; /// use problemreductions::{Problem, Solver, BruteForce}; /// /// // Path graph: 0-1-2, unit weights and lengths, K=1 /// let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); -/// let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); +/// let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); /// /// let solver = BruteForce::new(); /// let solution = solver.find_best(&problem).unwrap(); @@ -53,7 +53,7 @@ inventory::submit! { /// assert_eq!(solution, vec![0, 1, 0]); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MinSumMulticenter { +pub struct MinimumSumMulticenter { /// The underlying graph. graph: G, /// Non-negative weight for each vertex. @@ -64,8 +64,8 @@ pub struct MinSumMulticenter { k: usize, } -impl MinSumMulticenter { - /// Create a MinSumMulticenter problem. +impl MinimumSumMulticenter { + /// Create a MinimumSumMulticenter problem. /// /// # Panics /// - If `vertex_weights.len() != graph.num_vertices()` @@ -113,7 +113,7 @@ impl MinSumMulticenter { } } -impl MinSumMulticenter { +impl MinimumSumMulticenter { /// Get the number of vertices in the underlying graph. pub fn num_vertices(&self) -> usize { self.graph().num_vertices() @@ -186,12 +186,12 @@ impl MinSumMulticenter { } } -impl Problem for MinSumMulticenter +impl Problem for MinimumSumMulticenter where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - const NAME: &'static str = "MinSumMulticenter"; + const NAME: &'static str = "MinimumSumMulticenter"; type Metric = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { @@ -225,7 +225,7 @@ where } } -impl OptimizationProblem for MinSumMulticenter +impl OptimizationProblem for MinimumSumMulticenter where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, @@ -238,9 +238,9 @@ where } crate::declare_variants! { - MinSumMulticenter => "2^num_vertices", + MinimumSumMulticenter => "2^num_vertices", } #[cfg(test)] -#[path = "../../unit_tests/models/graph/min_sum_multicenter.rs"] +#[path = "../../unit_tests/models/graph/minimum_sum_multicenter.rs"] mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 135e66932..24201a581 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -15,7 +15,7 @@ //! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle) //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`BicliqueCover`]: Biclique cover on bipartite graphs -//! - [`MinSumMulticenter`]: Min-sum multicenter (p-median) +//! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median) //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) @@ -27,7 +27,7 @@ pub(crate) mod maximal_is; pub(crate) mod maximum_clique; pub(crate) mod maximum_independent_set; pub(crate) mod maximum_matching; -pub(crate) mod min_sum_multicenter; +pub(crate) mod minimum_sum_multicenter; pub(crate) mod minimum_dominating_set; pub(crate) mod minimum_feedback_vertex_set; pub(crate) mod minimum_vertex_cover; @@ -45,7 +45,7 @@ pub use maximal_is::MaximalIS; pub use maximum_clique::MaximumClique; pub use maximum_independent_set::MaximumIndependentSet; pub use maximum_matching::MaximumMatching; -pub use min_sum_multicenter::MinSumMulticenter; +pub use minimum_sum_multicenter::MinimumSumMulticenter; pub use minimum_dominating_set::MinimumDominatingSet; pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet; pub use minimum_vertex_cover::MinimumVertexCover; diff --git a/src/models/mod.rs b/src/models/mod.rs index 125ff38b6..6cb4a9a78 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -13,7 +13,7 @@ pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO}; pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use graph::{ BicliqueCover, GraphPartitioning, KColoring, MaxCut, MaximalIS, MaximumClique, - MaximumIndependentSet, MaximumMatching, MinSumMulticenter, MinimumDominatingSet, + MaximumIndependentSet, MaximumMatching, MinimumSumMulticenter, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism, TravelingSalesman, }; diff --git a/src/unit_tests/models/graph/min_sum_multicenter.rs b/src/unit_tests/models/graph/minimum_sum_multicenter.rs similarity index 79% rename from src/unit_tests/models/graph/min_sum_multicenter.rs rename to src/unit_tests/models/graph/minimum_sum_multicenter.rs index eb42c34bc..c92b5348f 100644 --- a/src/unit_tests/models/graph/min_sum_multicenter.rs +++ b/src/unit_tests/models/graph/minimum_sum_multicenter.rs @@ -7,7 +7,7 @@ use crate::types::Direction; #[test] fn test_min_sum_multicenter_creation() { let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 4], vec![1i32; 3], 2); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 4], vec![1i32; 3], 2); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); assert_eq!(problem.k(), 2); @@ -18,7 +18,7 @@ fn test_min_sum_multicenter_creation() { #[test] fn test_min_sum_multicenter_size_getters() { let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 5], vec![1i32; 4], 2); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 5], vec![1i32; 4], 2); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 4); assert_eq!(problem.num_centers(), 2); @@ -27,7 +27,7 @@ fn test_min_sum_multicenter_size_getters() { #[test] fn test_min_sum_multicenter_direction() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); assert_eq!(problem.direction(), Direction::Minimize); } @@ -35,7 +35,7 @@ fn test_min_sum_multicenter_direction() { fn test_min_sum_multicenter_evaluate_path() { // Path: 0-1-2, unit weights and lengths, K=1 let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); // Center at vertex 1: distances = [1, 0, 1], total = 2 let result = problem.evaluate(&[0, 1, 0]); @@ -51,7 +51,7 @@ fn test_min_sum_multicenter_evaluate_path() { #[test] fn test_min_sum_multicenter_wrong_k() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 2); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 2); // Only 1 center selected when K=2 let result = problem.evaluate(&[0, 1, 0]); @@ -70,7 +70,7 @@ fn test_min_sum_multicenter_wrong_k() { fn test_min_sum_multicenter_weighted() { // Path: 0-1-2, vertex weights = [3, 1, 2], edge lengths = [1, 1], K=1 let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinSumMulticenter::new(graph, vec![3i32, 1, 2], vec![1i32; 2], 1); + let problem = MinimumSumMulticenter::new(graph, vec![3i32, 1, 2], vec![1i32; 2], 1); // Center at 0: distances = [0, 1, 2], total = 3*0 + 1*1 + 2*2 = 5 assert_eq!(problem.evaluate(&[1, 0, 0]).unwrap(), 5); @@ -86,7 +86,7 @@ fn test_min_sum_multicenter_weighted() { fn test_min_sum_multicenter_weighted_edges() { // Triangle: 0-1 (len 1), 1-2 (len 3), 0-2 (len 2), K=1 let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1, 3, 2], 1); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1, 3, 2], 1); // Center at 0: d(0)=0, d(1)=1, d(2)=2, total=3 assert_eq!(problem.evaluate(&[1, 0, 0]).unwrap(), 3); @@ -99,7 +99,7 @@ fn test_min_sum_multicenter_weighted_edges() { fn test_min_sum_multicenter_two_centers() { // Path: 0-1-2-3-4, unit weights and lengths, K=2 let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 5], vec![1i32; 4], 2); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 5], vec![1i32; 4], 2); // Centers at {1, 3}: d = [1, 0, 1, 0, 1], total = 3 assert_eq!(problem.evaluate(&[0, 1, 0, 1, 0]).unwrap(), 3); @@ -124,7 +124,7 @@ fn test_min_sum_multicenter_solver() { (2, 5), ], ); - let problem = MinSumMulticenter::new(graph, vec![1i32; 7], vec![1i32; 8], 2); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 7], vec![1i32; 8], 2); let solver = BruteForce::new(); let best = solver.find_best(&problem).unwrap(); @@ -138,7 +138,7 @@ fn test_min_sum_multicenter_solver() { fn test_min_sum_multicenter_disconnected() { // Two disconnected components: 0-1 and 2-3, K=1 let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 4], vec![1i32; 2], 1); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 4], vec![1i32; 2], 1); // Center at 0: vertex 2 and 3 are unreachable let result = problem.evaluate(&[1, 0, 0, 0]); @@ -146,7 +146,7 @@ fn test_min_sum_multicenter_disconnected() { // With K=2, centers at {0, 2}: all reachable let graph2 = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); - let problem2 = MinSumMulticenter::new(graph2, vec![1i32; 4], vec![1i32; 2], 2); + let problem2 = MinimumSumMulticenter::new(graph2, vec![1i32; 4], vec![1i32; 2], 2); let result2 = problem2.evaluate(&[1, 0, 1, 0]); assert!(result2.is_valid()); assert_eq!(result2.unwrap(), 2); // d = [0, 1, 0, 1] @@ -155,7 +155,7 @@ fn test_min_sum_multicenter_disconnected() { #[test] fn test_min_sum_multicenter_single_vertex() { let graph = SimpleGraph::new(1, vec![]); - let problem = MinSumMulticenter::new(graph, vec![5i32], vec![], 1); + let problem = MinimumSumMulticenter::new(graph, vec![5i32], vec![], 1); let result = problem.evaluate(&[1]); assert!(result.is_valid()); assert_eq!(result.unwrap(), 0); // Only vertex is the center, distance = 0 @@ -165,7 +165,7 @@ fn test_min_sum_multicenter_single_vertex() { fn test_min_sum_multicenter_all_centers() { // K = num_vertices: all vertices are centers, total distance = 0 let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 3); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 3); let result = problem.evaluate(&[1, 1, 1]); assert!(result.is_valid()); assert_eq!(result.unwrap(), 0); @@ -175,34 +175,34 @@ fn test_min_sum_multicenter_all_centers() { #[should_panic(expected = "vertex_weights length must match num_vertices")] fn test_min_sum_multicenter_wrong_vertex_weights_len() { let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinSumMulticenter::new(graph, vec![1i32; 2], vec![1i32; 1], 1); + MinimumSumMulticenter::new(graph, vec![1i32; 2], vec![1i32; 1], 1); } #[test] #[should_panic(expected = "edge_lengths length must match num_edges")] fn test_min_sum_multicenter_wrong_edge_lengths_len() { let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); + MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); } #[test] #[should_panic(expected = "k must be positive")] fn test_min_sum_multicenter_k_zero() { let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 1], 0); + MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 1], 0); } #[test] #[should_panic(expected = "k must not exceed num_vertices")] fn test_min_sum_multicenter_k_too_large() { let graph = SimpleGraph::new(3, vec![(0, 1)]); - MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 1], 4); + MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 1], 4); } #[test] fn test_min_sum_multicenter_dims() { let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 5], vec![1i32; 4], 2); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 5], vec![1i32; 4], 2); assert_eq!(problem.dims(), vec![2; 5]); } @@ -210,7 +210,7 @@ fn test_min_sum_multicenter_dims() { fn test_min_sum_multicenter_find_all_best() { // Path: 0-1-2, unit weights, K=1. Center at 1 is optimal (cost 2) let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); let solver = BruteForce::new(); let solutions = solver.find_all_best(&problem); @@ -221,10 +221,10 @@ fn test_min_sum_multicenter_find_all_best() { #[test] fn test_min_sum_multicenter_serialization() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); + let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); let json = serde_json::to_string(&problem).unwrap(); - let deserialized: MinSumMulticenter = serde_json::from_str(&json).unwrap(); + let deserialized: MinimumSumMulticenter = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.graph().num_vertices(), 3); assert_eq!(deserialized.graph().num_edges(), 2); From 0f551c03be58e08c8f1886575479c6bf93e135fd Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 14 Mar 2026 00:14:10 +0800 Subject: [PATCH 6/7] Add MinimumSumMulticenter paper section Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 2006b57b1..c34812b5b 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -58,6 +58,7 @@ "SubsetSum": [Subset Sum], "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "MinimumSumMulticenter": [Minimum Sum Multicenter], "SubgraphIsomorphism": [Subgraph Isomorphism], "SubsetSum": [Subset Sum], ) @@ -575,6 +576,16 @@ caption: [A directed graph with FVS $S = {v_0}$ (blue, $w(S) = 1$). Removing $v_ ) ] +#problem-def("MinimumSumMulticenter")[ + Given a graph $G = (V, E)$ with vertex weights $w: V -> ZZ_(>= 0)$, edge lengths $l: E -> ZZ_(>= 0)$, and a positive integer $K <= |V|$, find a set $P subset.eq V$ of $K$ vertices (centers) that minimizes the total weighted distance $sum_(v in V) w(v) dot d(v, P)$, where $d(v, P) = min_(p in P) d(v, p)$ is the shortest-path distance from $v$ to the nearest center in $P$. +][ +Also known as the _p-median problem_. This is a classical NP-complete facility location problem from Garey & Johnson (A2 ND51). The goal is to optimally place $K$ service centers (e.g., warehouses, hospitals) to minimize total service cost. NP-completeness was established by Kariv and Hakimi (1979) via transformation from Dominating Set. The problem remains NP-complete even with unit weights and unit edge lengths, but is solvable in polynomial time for fixed $K$ or when $G$ is a tree. + +The best known exact algorithm runs in $O^*(2^n)$ time by brute-force enumeration of all $binom(n, K)$ vertex subsets. Constant-factor approximation algorithms exist: Charikar et al. (1999) gave the first constant-factor result, and the best known ratio is $(2 + epsilon)$ by Cohen-Addad et al. (STOC 2022). + +Variables: $n = |V|$ binary variables, one per vertex. $x_v = 1$ if vertex $v$ is selected as a center. A configuration is valid when exactly $K$ centers are selected and all vertices are reachable from at least one center. +] + == Set Problems #problem-def("MaximumSetPacking")[ From 72a3e60a26677dbe87ff428f7da0f50dcd00a015 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 14 Mar 2026 00:49:53 +0800 Subject: [PATCH 7/7] Fix trait consistency, switch to Dijkstra for MinimumSumMulticenter Co-Authored-By: Claude Opus 4.6 --- src/models/graph/minimum_sum_multicenter.rs | 64 +++++++++++++-------- src/unit_tests/trait_consistency.rs | 8 +++ 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/models/graph/minimum_sum_multicenter.rs b/src/models/graph/minimum_sum_multicenter.rs index 6781b468d..bacbed941 100644 --- a/src/models/graph/minimum_sum_multicenter.rs +++ b/src/models/graph/minimum_sum_multicenter.rs @@ -131,16 +131,15 @@ impl MinimumSumMulticenter { /// Compute shortest distances from each vertex to the nearest center. /// - /// Uses multi-source Bellman-Ford: initializes all centers at distance 0 - /// and relaxes edges using edge lengths. Terminates after at most `n - 1` - /// iterations (the standard Bellman-Ford bound). + /// Uses multi-source Dijkstra with linear scan: initializes all centers + /// at distance 0 and greedily relaxes edges by increasing distance. + /// Correct because all edge lengths are non-negative. /// /// Returns `None` if any vertex is unreachable from all centers. fn shortest_distances(&self, config: &[usize]) -> Option> { let n = self.graph.num_vertices(); let edges = self.graph.edges(); - // Build adjacency list: for each vertex, list of (neighbor, edge_length) let mut adj: Vec> = vec![Vec::new(); n]; for (idx, &(u, v)) in edges.iter().enumerate() { let len = self.edge_lengths[idx].to_sum(); @@ -148,40 +147,57 @@ impl MinimumSumMulticenter { adj[v].push((u, len)); } - // Multi-source Bellman-Ford: initialize all centers at distance 0 + // Multi-source Dijkstra with linear scan (works with PartialOrd) let mut dist: Vec> = vec![None; n]; + let mut visited = vec![false; n]; + + // Initialize centers for (v, &selected) in config.iter().enumerate() { if selected == 1 { dist[v] = Some(W::Sum::zero()); } } - // Bellman-Ford relaxation with iteration cap (at most n-1 iterations) - for _ in 0..n.saturating_sub(1) { - let mut changed = false; + for _ in 0..n { + // Find unvisited vertex with smallest distance + let mut u = None; for v in 0..n { - let dv = match &dist[v] { - Some(d) => d.clone(), - None => continue, - }; - for &(u, ref len) in &adj[v] { - let new_dist = dv.clone() + len.clone(); - let update = match &dist[u] { - None => true, - Some(du) => new_dist < du.clone(), - }; - if update { - dist[u] = Some(new_dist); - changed = true; + if visited[v] { + continue; + } + if let Some(ref dv) = dist[v] { + match u { + None => u = Some(v), + Some(prev) => { + if *dv < dist[prev].clone().unwrap() { + u = Some(v); + } + } } } } - if !changed { - break; + let u = match u { + Some(v) => v, + None => break, // remaining vertices are unreachable + }; + visited[u] = true; + + let du = dist[u].clone().unwrap(); + for &(next, ref len) in &adj[u] { + if visited[next] { + continue; + } + let new_dist = du.clone() + len.clone(); + let update = match &dist[next] { + None => true, + Some(d) => new_dist < *d, + }; + if update { + dist[next] = Some(new_dist); + } } } - // Collect distances, returning None if any vertex is unreachable dist.into_iter().collect() } } diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index a90872ca8..cdbdba7a1 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -90,6 +90,10 @@ fn test_all_problems_implement_trait_correctly() { ), "MinimumFeedbackArcSet", ); + check_problem_trait( + &MinimumSumMulticenter::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3], vec![1i32; 2], 1), + "MinimumSumMulticenter", + ); } #[test] @@ -139,6 +143,10 @@ fn test_direction() { .direction(), Direction::Minimize ); + assert_eq!( + MinimumSumMulticenter::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3], vec![1i32; 2], 1).direction(), + Direction::Minimize + ); // Maximization problems assert_eq!(