Skip to content

Commit 291cf56

Browse files
zazabapclaudeGiggleLiu
authored
Fix #399: Add MinSumMulticenter model (#635)
* Add plan for #399: MinSumMulticenter model * Implement MinSumMulticenter (p-median) model (#399) 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 <noreply@anthropic.com> * chore: remove plan file after implementation * Address Copilot review comments for MinSumMulticenter - 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * Add MinimumSumMulticenter paper section Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix trait consistency, switch to Dijkstra for MinimumSumMulticenter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent 40b2623 commit 291cf56

12 files changed

Lines changed: 666 additions & 4 deletions

File tree

docs/paper/reductions.typ

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"SubsetSum": [Subset Sum],
5959
"MinimumFeedbackArcSet": [Minimum Feedback Arc Set],
6060
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
61+
"MinimumSumMulticenter": [Minimum Sum Multicenter],
6162
"SubgraphIsomorphism": [Subgraph Isomorphism],
6263
"SubsetSum": [Subset Sum],
6364
)
@@ -575,6 +576,16 @@ caption: [A directed graph with FVS $S = {v_0}$ (blue, $w(S) = 1$). Removing $v_
575576
) <fig:fvs-example>
576577
]
577578

579+
#problem-def("MinimumSumMulticenter")[
580+
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$.
581+
][
582+
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.
583+
584+
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).
585+
586+
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.
587+
]
588+
578589
== Set Problems
579590

580591
#problem-def("MaximumSetPacking")[

docs/src/reductions/problem_schemas.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,32 @@
355355
}
356356
]
357357
},
358+
{
359+
"name": "MinimumSumMulticenter",
360+
"description": "Find K centers minimizing total weighted distance (p-median problem)",
361+
"fields": [
362+
{
363+
"name": "graph",
364+
"type_name": "G",
365+
"description": "The underlying graph G=(V,E)"
366+
},
367+
{
368+
"name": "vertex_weights",
369+
"type_name": "Vec<W>",
370+
"description": "Vertex weights w: V -> R"
371+
},
372+
{
373+
"name": "edge_lengths",
374+
"type_name": "Vec<W>",
375+
"description": "Edge lengths l: E -> R"
376+
},
377+
{
378+
"name": "k",
379+
"type_name": "usize",
380+
"description": "Number of centers to place"
381+
}
382+
]
383+
},
358384
{
359385
"name": "MinimumVertexCover",
360386
"description": "Find minimum weight vertex cover in a graph",
@@ -382,6 +408,17 @@
382408
}
383409
]
384410
},
411+
{
412+
"name": "PartitionIntoTriangles",
413+
"description": "Partition vertices into triangles (K3 subgraphs)",
414+
"fields": [
415+
{
416+
"name": "graph",
417+
"type_name": "G",
418+
"description": "The underlying graph G=(V,E) with |V| divisible by 3"
419+
}
420+
]
421+
},
385422
{
386423
"name": "QUBO",
387424
"description": "Minimize quadratic unconstrained binary objective",

problemreductions-cli/src/commands/create.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
9393
"QUBO" => "--matrix \"1,0.5;0.5,2\"",
9494
"SpinGlass" => "--graph 0-1,1-2 --couplings 1,1",
9595
"KColoring" => "--graph 0-1,1-2,2-0 --k 3",
96+
"MinimumSumMulticenter" => "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2",
9697
"PartitionIntoTriangles" => "--graph 0-1,1-2,0-2",
9798
"Factoring" => "--target 15 --m 4 --n 4",
9899
"MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"",
@@ -564,6 +565,32 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
564565
)
565566
}
566567

568+
// MinimumSumMulticenter (p-median)
569+
"MinimumSumMulticenter" => {
570+
let (graph, n) = parse_graph(args).map_err(|e| {
571+
anyhow::anyhow!(
572+
"{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"
573+
)
574+
})?;
575+
let vertex_weights = parse_vertex_weights(args, n)?;
576+
let edge_lengths = parse_edge_weights(args, graph.num_edges())?;
577+
let k = args.k.ok_or_else(|| {
578+
anyhow::anyhow!(
579+
"MinimumSumMulticenter requires --k (number of centers)\n\n\
580+
Usage: pred create MinimumSumMulticenter --graph 0-1,1-2,2-3 --k 2"
581+
)
582+
})?;
583+
(
584+
ser(MinimumSumMulticenter::new(
585+
graph,
586+
vertex_weights,
587+
edge_lengths,
588+
k,
589+
))?,
590+
resolved_variant.clone(),
591+
)
592+
}
593+
567594
// SubgraphIsomorphism
568595
"SubgraphIsomorphism" => {
569596
let (host_graph, _) = parse_graph(args).map_err(|e| {

problemreductions-cli/src/dispatch.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ pub fn load_problem(
210210
"MaximumClique" => deser_opt::<MaximumClique<SimpleGraph, i32>>(data),
211211
"MaximumMatching" => deser_opt::<MaximumMatching<SimpleGraph, i32>>(data),
212212
"MinimumDominatingSet" => deser_opt::<MinimumDominatingSet<SimpleGraph, i32>>(data),
213+
"MinimumSumMulticenter" => deser_opt::<MinimumSumMulticenter<SimpleGraph, i32>>(data),
213214
"GraphPartitioning" => deser_opt::<GraphPartitioning<SimpleGraph>>(data),
214215
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
215216
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
@@ -275,6 +276,7 @@ pub fn serialize_any_problem(
275276
"MaximumClique" => try_ser::<MaximumClique<SimpleGraph, i32>>(any),
276277
"MaximumMatching" => try_ser::<MaximumMatching<SimpleGraph, i32>>(any),
277278
"MinimumDominatingSet" => try_ser::<MinimumDominatingSet<SimpleGraph, i32>>(any),
279+
"MinimumSumMulticenter" => try_ser::<MinimumSumMulticenter<SimpleGraph, i32>>(any),
278280
"GraphPartitioning" => try_ser::<GraphPartitioning<SimpleGraph>>(any),
279281
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
280282
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),

problemreductions-cli/src/mcp/tools.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use crate::util;
22
use problemreductions::models::algebraic::QUBO;
33
use problemreductions::models::formula::{CNFClause, Satisfiability};
44
use problemreductions::models::graph::{
5-
MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet,
6-
MinimumVertexCover, SpinGlass, TravelingSalesman,
5+
MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumSumMulticenter,
6+
MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman,
77
};
88
use problemreductions::models::misc::Factoring;
99
use problemreductions::registry::collect_schemas;
@@ -514,6 +514,25 @@ impl McpServer {
514514
(ser(Factoring::new(bits_m, bits_n, target))?, variant)
515515
}
516516

517+
// MinimumSumMulticenter (p-median)
518+
"MinimumSumMulticenter" => {
519+
let (graph, n) = parse_graph_from_params(params)?;
520+
let vertex_weights = parse_vertex_weights_from_params(params, n)?;
521+
let edge_lengths = parse_edge_lengths_from_params(params, graph.num_edges())?;
522+
let k = params
523+
.get("k")
524+
.and_then(|v| v.as_u64())
525+
.map(|v| v as usize)
526+
.ok_or_else(|| {
527+
anyhow::anyhow!("MinimumSumMulticenter requires 'k' (number of centers)")
528+
})?;
529+
let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]);
530+
(
531+
ser(MinimumSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?,
532+
variant,
533+
)
534+
}
535+
517536
_ => anyhow::bail!("{}", unknown_problem_error(&canonical)),
518537
};
519538

@@ -634,10 +653,34 @@ impl McpServer {
634653
util::validate_k_param(resolved_variant, k_flag, Some(3), "KColoring")?;
635654
util::ser_kcoloring(graph, k)?
636655
}
656+
"MinimumSumMulticenter" => {
657+
let edge_prob = params
658+
.get("edge_prob")
659+
.and_then(|v| v.as_f64())
660+
.unwrap_or(0.5);
661+
if !(0.0..=1.0).contains(&edge_prob) {
662+
anyhow::bail!("edge_prob must be between 0.0 and 1.0");
663+
}
664+
let graph = util::create_random_graph(num_vertices, edge_prob, seed);
665+
let num_edges = graph.num_edges();
666+
let vertex_weights = vec![1i32; num_vertices];
667+
let edge_lengths = vec![1i32; num_edges];
668+
let k = params
669+
.get("k")
670+
.and_then(|v| v.as_u64())
671+
.map(|v| v as usize)
672+
.unwrap_or(1.max(num_vertices / 3));
673+
let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]);
674+
(
675+
ser(MinimumSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?,
676+
variant,
677+
)
678+
}
637679
_ => anyhow::bail!(
638680
"Random generation is not supported for {}. \
639681
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
640-
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman)",
682+
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, \
683+
TravelingSalesman, MinimumSumMulticenter)",
641684
canonical
642685
),
643686
};
@@ -1294,6 +1337,30 @@ fn parse_edge_weights_from_params(
12941337
}
12951338
}
12961339

1340+
/// Parse `edge_lengths` field from JSON params as edge lengths (i32), defaulting to all 1s.
1341+
fn parse_edge_lengths_from_params(
1342+
params: &serde_json::Value,
1343+
num_edges: usize,
1344+
) -> anyhow::Result<Vec<i32>> {
1345+
match params.get("edge_lengths").and_then(|v| v.as_str()) {
1346+
Some(w) => {
1347+
let lengths: Vec<i32> = w
1348+
.split(',')
1349+
.map(|s| s.trim().parse::<i32>())
1350+
.collect::<std::result::Result<Vec<_>, _>>()?;
1351+
if lengths.len() != num_edges {
1352+
anyhow::bail!(
1353+
"Expected {} edge lengths but got {}",
1354+
num_edges,
1355+
lengths.len()
1356+
);
1357+
}
1358+
Ok(lengths)
1359+
}
1360+
None => Ok(vec![1i32; num_edges]),
1361+
}
1362+
}
1363+
12971364
/// Parse `clauses` field from JSON params as semicolon-separated clauses.
12981365
fn parse_clauses_from_params(params: &serde_json::Value) -> anyhow::Result<Vec<CNFClause>> {
12991366
let clauses_str = params

problemreductions-cli/src/problem_name.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub const ALIASES: &[(&str, &str)] = &[
2525
("MaxMatching", "MaximumMatching"),
2626
("FVS", "MinimumFeedbackVertexSet"),
2727
("FAS", "MinimumFeedbackArcSet"),
28+
("pmedian", "MinimumSumMulticenter"),
2829
];
2930

3031
/// Resolve a short alias to the canonical problem name.
@@ -63,6 +64,7 @@ pub fn resolve_alias(input: &str) -> String {
6364
"lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(),
6465
"fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(),
6566
"fas" | "minimumfeedbackarcset" => "MinimumFeedbackArcSet".to_string(),
67+
"minimumsummulticenter" | "pmedian" => "MinimumSumMulticenter".to_string(),
6668
"subsetsum" => "SubsetSum".to_string(),
6769
_ => input.to_string(), // pass-through for exact names
6870
}

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ pub mod prelude {
4545
};
4646
pub use crate::models::graph::{
4747
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
48-
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumVertexCover,
48+
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet,
49+
MinimumSumMulticenter, MinimumVertexCover,
4950
PartitionIntoTriangles, RuralPostman, TravelingSalesman,
5051
};
5152
pub use crate::models::misc::{

0 commit comments

Comments
 (0)