Skip to content
Merged
11 changes: 11 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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],
)
Expand Down Expand Up @@ -575,6 +576,16 @@ caption: [A directed graph with FVS $S = {v_0}$ (blue, $w(S) = 1$). Removing $v_
) <fig:fvs-example>
]

#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")[
Expand Down
37 changes: 37 additions & 0 deletions docs/src/reductions/problem_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,32 @@
}
]
},
{
"name": "MinimumSumMulticenter",
"description": "Find K centers minimizing total weighted distance (p-median problem)",
"fields": [
Comment on lines +359 to +361
{
"name": "graph",
"type_name": "G",
"description": "The underlying graph G=(V,E)"
},
{
"name": "vertex_weights",
"type_name": "Vec<W>",
"description": "Vertex weights w: V -> R"
},
{
"name": "edge_lengths",
"type_name": "Vec<W>",
"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",
Expand Down Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,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",
"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",
"MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"",
Expand Down Expand Up @@ -564,6 +565,32 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// MinimumSumMulticenter (p-median)
"MinimumSumMulticenter" => {
let (graph, n) = parse_graph(args).map_err(|e| {
anyhow::anyhow!(
"{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!(
"MinimumSumMulticenter requires --k (number of centers)\n\n\
Usage: pred create MinimumSumMulticenter --graph 0-1,1-2,2-3 --k 2"
)
})?;
(
ser(MinimumSumMulticenter::new(
graph,
vertex_weights,
edge_lengths,
k,
))?,
resolved_variant.clone(),
)
}

// SubgraphIsomorphism
"SubgraphIsomorphism" => {
let (host_graph, _) = parse_graph(args).map_err(|e| {
Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ pub fn load_problem(
"MaximumClique" => deser_opt::<MaximumClique<SimpleGraph, i32>>(data),
"MaximumMatching" => deser_opt::<MaximumMatching<SimpleGraph, i32>>(data),
"MinimumDominatingSet" => deser_opt::<MinimumDominatingSet<SimpleGraph, i32>>(data),
"MinimumSumMulticenter" => deser_opt::<MinimumSumMulticenter<SimpleGraph, i32>>(data),
"GraphPartitioning" => deser_opt::<GraphPartitioning<SimpleGraph>>(data),
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
Expand Down Expand Up @@ -275,6 +276,7 @@ pub fn serialize_any_problem(
"MaximumClique" => try_ser::<MaximumClique<SimpleGraph, i32>>(any),
"MaximumMatching" => try_ser::<MaximumMatching<SimpleGraph, i32>>(any),
"MinimumDominatingSet" => try_ser::<MinimumDominatingSet<SimpleGraph, i32>>(any),
"MinimumSumMulticenter" => try_ser::<MinimumSumMulticenter<SimpleGraph, i32>>(any),
"GraphPartitioning" => try_ser::<GraphPartitioning<SimpleGraph>>(any),
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),
Expand Down
73 changes: 70 additions & 3 deletions problemreductions-cli/src/mcp/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, MinimumSumMulticenter,
MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman,
};
use problemreductions::models::misc::Factoring;
use problemreductions::registry::collect_schemas;
Expand Down Expand Up @@ -514,6 +514,25 @@ impl McpServer {
(ser(Factoring::new(bits_m, bits_n, target))?, variant)
}

// 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())?;
let k = params
.get("k")
.and_then(|v| v.as_u64())
.map(|v| v as usize)
.ok_or_else(|| {
anyhow::anyhow!("MinimumSumMulticenter requires 'k' (number of centers)")
})?;
let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]);
(
ser(MinimumSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?,
variant,
)
}

_ => anyhow::bail!("{}", unknown_problem_error(&canonical)),
};

Expand Down Expand Up @@ -634,10 +653,34 @@ impl McpServer {
util::validate_k_param(resolved_variant, k_flag, Some(3), "KColoring")?;
util::ser_kcoloring(graph, k)?
}
"MinimumSumMulticenter" => {
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));
Comment on lines +668 to +672
let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]);
(
ser(MinimumSumMulticenter::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, MinimumSumMulticenter)",
canonical
),
};
Expand Down Expand Up @@ -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<Vec<i32>> {
match params.get("edge_lengths").and_then(|v| v.as_str()) {
Some(w) => {
let lengths: Vec<i32> = w
.split(',')
.map(|s| s.trim().parse::<i32>())
.collect::<std::result::Result<Vec<_>, _>>()?;
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<Vec<CNFClause>> {
let clauses_str = params
Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub const ALIASES: &[(&str, &str)] = &[
("MaxMatching", "MaximumMatching"),
("FVS", "MinimumFeedbackVertexSet"),
("FAS", "MinimumFeedbackArcSet"),
("pmedian", "MinimumSumMulticenter"),
];

/// Resolve a short alias to the canonical problem name.
Expand Down Expand Up @@ -63,6 +64,7 @@ pub fn resolve_alias(input: &str) -> String {
"lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(),
"fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(),
"fas" | "minimumfeedbackarcset" => "MinimumFeedbackArcSet".to_string(),
"minimumsummulticenter" | "pmedian" => "MinimumSumMulticenter".to_string(),
"subsetsum" => "SubsetSum".to_string(),
_ => input.to_string(), // pass-through for exact names
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ pub mod prelude {
};
pub use crate::models::graph::{
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumVertexCover,
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet,
MinimumSumMulticenter, MinimumVertexCover,
PartitionIntoTriangles, RuralPostman, TravelingSalesman,
};
pub use crate::models::misc::{
Expand Down
Loading
Loading