Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
"MinMaxMulticenter": [Min-Max Multicenter],
"FlowShopScheduling": [Flow Shop Scheduling],
"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
Expand Down Expand Up @@ -1616,6 +1617,49 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("MinMaxMulticenter")
let nv = graph-num-vertices(x.instance)
let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1)))
let K = x.instance.k
let B = x.instance.bound
let sol = (config: x.optimal_config, metric: x.optimal_value)
let centers = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)
[
#problem-def("MinMaxMulticenter")[
Given a graph $G = (V, E)$ with vertex weights $w: V -> ZZ_(>= 0)$, edge lengths $l: E -> ZZ_(>= 0)$, a positive integer $K <= |V|$, and a rational bound $B > 0$, does there exist $S subset.eq V$ with $|S| = K$ such that $max_(v in V) w(v) dot d(v, S) <= B$, where $d(v, S) = min_(s in S) d(v, s)$ is the shortest weighted-path distance from $v$ to the nearest vertex in $S$?
][
Also known as the _vertex p-center problem_ (Garey & Johnson A2 ND50). The goal is to place $K$ facilities so that the worst-case weighted distance from any demand point to its nearest facility is at most $B$. NP-complete even with unit weights and unit edge lengths (Kariv and Hakimi, 1979).

Closely related to Dominating Set: on unweighted unit-length graphs, a $K$-center with radius $B = 1$ is exactly a dominating set of size $K$. The best known exact algorithm runs in $O^*(1.4969^n)$ via binary search over distance thresholds combined with dominating set computation @vanrooij2011. An optimal 2-approximation exists (Hochbaum and Shmoys, 1985); no $(2 - epsilon)$-approximation is possible unless $P = "NP"$ (Hsu and Nemhauser, 1979).

Variables: $n = |V|$ binary variables, one per vertex. $x_v = 1$ if vertex $v$ is selected as a center. A configuration is satisfying when exactly $K$ centers are selected and $max_(v in V) w(v) dot d(v, S) <= B$.

*Example.* Consider the graph $G$ on #nv vertices with unit weights $w(v) = 1$, unit edge lengths, edges ${#edges.map(((u, v)) => $(#u, #v)$).join(", ")}$, $K = #K$, and $B = #B$. Placing centers at $S = {#centers.map(i => $v_#i$).join(", ")}$ gives maximum distance $max_v d(v, S) = 1 <= B$, so this is a feasible solution.

#figure({
let blue = graph-colors.at(0)
let gray = luma(200)
canvas(length: 1cm, {
import draw: *
let verts = ((-1.5, 0.8), (0, 1.5), (1.5, 0.8), (1.5, -0.8), (0, -1.5), (-1.5, -0.8))
for (u, v) in edges {
g-edge(verts.at(u), verts.at(v), stroke: 1pt + gray)
}
for (k, pos) in verts.enumerate() {
let is-center = centers.any(c => c == k)
g-node(pos, name: "v" + str(k),
fill: if is-center { blue } else { white },
label: if is-center { text(fill: white)[$v_#k$] } else { [$v_#k$] })
}
})
},
caption: [Min-Max Multicenter with $K = #K$, $B = #B$ on a #{nv}-vertex graph. Centers #centers.map(i => $v_#i$).join(" and ") (blue) ensure every vertex is within distance $B$ of some center.],
) <fig:min-max-multicenter>
]
]
}

#{
let x = load-model-example("MultipleCopyFileAllocation")
let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1)))
Expand Down
4 changes: 3 additions & 1 deletion docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json
pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3 -o kth.json
pred create SpinGlass --graph 0-1,1-2 -o sg.json
pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json
pred create MinMaxMulticenter --graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2 --bound 1 -o pcenter.json
pred create RectilinearPictureCompression --matrix "1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1" --k 2 -o rpc.json
pred solve rpc.json --solver brute-force
pred create MinimumMultiwayCut --graph 0-1,1-2,2-3,3-0 --terminals 0,2 --edge-weights 3,1,2,4 -o mmc.json
Expand Down Expand Up @@ -514,6 +515,7 @@ Stdin is supported with `-`:
```bash
pred create MIS --graph 0-1,1-2,2-3 | pred solve -
pred create MIS --graph 0-1,1-2,2-3 | pred solve - --solver brute-force
pred create MinMaxMulticenter --graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2 --bound 1 | pred solve - --solver brute-force
pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets "0,1,2;3,4,5;1,3;2,4;0,5" | pred solve - --solver brute-force
```

Expand Down Expand Up @@ -541,7 +543,7 @@ Source evaluation: Valid(2)

> **Note:** The ILP solver requires a reduction path from the target problem to ILP.
> Some problems do not currently have one. Examples include BoundedComponentSpanningForest,
> LengthBoundedDisjointPaths, MinimumCardinalityKey, QUBO, SpinGlass, MaxCut, CircuitSAT, and MultiprocessorScheduling.
> LengthBoundedDisjointPaths, MinimumCardinalityKey, QUBO, SpinGlass, MaxCut, CircuitSAT, MinMaxMulticenter, and MultiprocessorScheduling.
> Use `pred solve <file> --solver brute-force` for these, or reduce to a problem that supports ILP first.
> For other problems, use `pred path <PROBLEM> ILP` to check whether an ILP reduction path exists.

Expand Down
5 changes: 3 additions & 2 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ Flags by problem type:
MultiprocessorScheduling --lengths, --num-processors, --deadline
SequencingWithinIntervals --release-times, --deadlines, --lengths
OptimalLinearArrangement --graph, --bound
MinMaxMulticenter (pCenter) --graph, --weights, --edge-weights, --k, --bound
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
MultipleChoiceBranching --arcs [--weights] --partition --bound [--num-vertices]
AdditionalKey --num-attributes, --dependencies, --relation-attrs [--known-keys]
Expand Down Expand Up @@ -622,8 +623,8 @@ Solve via explicit reduction:
Input: a problem JSON from `pred create`, or a reduction bundle from `pred reduce`.
When given a bundle, the target is solved and the solution is mapped back to the source.
The ILP solver auto-reduces non-ILP problems before solving.
Problems without an ILP reduction path, such as `LengthBoundedDisjointPaths` and
`StringToStringCorrection`, currently need `--solver brute-force`.
Problems without an ILP reduction path, such as `LengthBoundedDisjointPaths`,
`MinMaxMulticenter`, and `StringToStringCorrection`, currently need `--solver brute-force`.

ILP backend (default: HiGHS). To use a different backend:
cargo install problemreductions-cli --features coin-cbc
Expand Down
55 changes: 55 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,14 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
}
}

fn cli_flag_name(field_name: &str) -> String {
match field_name {
"vertex_weights" => "weights".to_string(),
"edge_lengths" => "edge-weights".to_string(),
_ => field_name.replace('_', "-"),
}
}

fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
match canonical {
"MaximumIndependentSet"
Expand Down Expand Up @@ -405,6 +413,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"SpinGlass" => "--graph 0-1,1-2 --couplings 1,1",
"KColoring" => "--graph 0-1,1-2,2-0 --k 3",
"HamiltonianCircuit" => "--graph 0-1,1-2,2-3,3-0",
"MinMaxMulticenter" => {
"--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2 --bound 2"
}
"MinimumSumMulticenter" => {
"--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2"
}
Expand Down Expand Up @@ -2608,6 +2619,50 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// MinMaxMulticenter (vertex p-center)
"MinMaxMulticenter" => {
let usage = "Usage: pred create MinMaxMulticenter --graph 0-1,1-2,2-3 [--weights 1,1,1,1] [--edge-weights 1,1,1] --k 2 --bound 2";
let (graph, n) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
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!(
"MinMaxMulticenter requires --k (number of centers)\n\n\
Usage: pred create MinMaxMulticenter --graph 0-1,1-2,2-3 --k 2 --bound 2"
)
})?;
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"MinMaxMulticenter requires --bound (distance bound B)\n\n\
Usage: pred create MinMaxMulticenter --graph 0-1,1-2,2-3 --k 2 --bound 2"
)
})?;
let bound = i32::try_from(bound).map_err(|_| {
anyhow::anyhow!(
"MinMaxMulticenter --bound must fit in i32 (got {bound})\n\n{usage}"
)
})?;
if vertex_weights.iter().any(|&weight| weight < 0) {
bail!("MinMaxMulticenter --weights must be non-negative");
}
if edge_lengths.iter().any(|&length| length < 0) {
bail!("MinMaxMulticenter --edge-weights must be non-negative");
}
if bound < 0 {
bail!("MinMaxMulticenter --bound must be non-negative");
}
(
ser(MinMaxMulticenter::new(
graph,
vertex_weights,
edge_lengths,
k,
bound,
))?,
resolved_variant.clone(),
)
}

// StrongConnectivityAugmentation
"StrongConnectivityAugmentation" => {
let usage = "Usage: pred create StrongConnectivityAugmentation --arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1 [--num-vertices N]";
Expand Down
9 changes: 9 additions & 0 deletions problemreductions-cli/src/commands/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ enum SolveInput {
Bundle(ReductionBundle),
}

fn add_bruteforce_hint(err: anyhow::Error) -> anyhow::Error {
let message = err.to_string();
if message.contains("No reduction path from") {
anyhow::anyhow!("{message}\n\nTry: pred solve <INPUT> --solver brute-force")
} else {
err
}
}

fn parse_input(path: &Path) -> Result<SolveInput> {
let content = read_input(path)?;
let json: serde_json::Value = serde_json::from_str(&content).context("Failed to parse JSON")?;
Expand Down
35 changes: 35 additions & 0 deletions problemreductions-cli/src/mcp/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,41 @@ mod tests {
assert_eq!(json["source"], "MaximumIndependentSet");
}

#[test]
fn test_inspect_minmaxmulticenter_lists_bruteforce_only() {
let server = McpServer::new();
let problem_json = serde_json::json!({
"type": "MinMaxMulticenter",
"variant": {"graph": "SimpleGraph", "weight": "i32"},
"data": {
"graph": {
"inner": {
"nodes": [null, null, null, null],
"node_holes": [],
"edge_property": "undirected",
"edges": [[0, 1, null], [1, 2, null], [2, 3, null]]
}
},
"vertex_weights": [1, 1, 1, 1],
"edge_lengths": [1, 1, 1],
"k": 2,
"bound": 1
}
})
.to_string();

let result = server.inspect_problem_inner(&problem_json);
assert!(result.is_ok());
let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
let solvers: Vec<&str> = json["solvers"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(solvers, vec!["brute-force"]);
}

#[test]
fn test_solve_sat_problem() {
let server = McpServer::new();
Expand Down
3 changes: 2 additions & 1 deletion problemreductions-cli/src/mcp/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,14 +690,15 @@ impl McpServer {
let mut targets: Vec<String> = outgoing.iter().map(|e| e.target_name.to_string()).collect();
targets.sort();
targets.dedup();
let solvers = problem.available_solvers();

let result = serde_json::json!({
"kind": "problem",
"type": name,
"variant": variant,
"size_fields": size_fields,
"num_variables": problem.num_variables_dyn(),
"solvers": ["ilp", "brute-force"],
"solvers": solvers,
"reduces_to": targets,
});
Ok(serde_json::to_string_pretty(&result)?)
Expand Down
Loading
Loading