Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ cd problem-reductions
make cli # builds target/release/pred
```

If you prefer a workspace-local build without installing into your global Cargo bin directory:

```bash
cargo build -p problemreductions-cli --release
./target/release/pred --version
```

See the [Getting Started](https://codingthrust.github.io/problem-reductions/getting-started.html) guide for usage examples, the reduction workflow, and [CLI usage](https://codingthrust.github.io/problem-reductions/cli.html).

## MCP Server (AI Integration)
Expand Down
45 changes: 45 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"SubsetSum": [Subset Sum],
"MinimumFeedbackArcSet": [Minimum Feedback Arc Set],
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
"StrongConnectivityAugmentation": [Strong Connectivity Augmentation],
"ShortestCommonSupersequence": [Shortest Common Supersequence],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
"SubgraphIsomorphism": [Subgraph Isomorphism],
Expand Down Expand Up @@ -892,6 +893,50 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
]
}

#{
let x = load-model-example("StrongConnectivityAugmentation")
let nv = graph-num-vertices(x.instance)
let ne = graph-num-edges(x.instance)
let arcs = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1)))
let candidates = x.instance.candidate_arcs
let bound = x.instance.bound
let sol = x.optimal.at(0)
let chosen = candidates.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc)
let arc = chosen.at(0)
let blue = graph-colors.at(0)
[
#problem-def("StrongConnectivityAugmentation")[
Given a directed graph $G = (V, A)$, a set $C subset.eq (V times V backslash A) times ZZ_(> 0)$ of weighted candidate arcs, and a bound $B in ZZ_(>= 0)$, determine whether there exists a subset $C' subset.eq C$ such that $sum_((u, v, w) in C') w <= B$ and the augmented digraph $(V, A union {(u, v) : (u, v, w) in C'})$ is strongly connected.
][
Strong Connectivity Augmentation models network design problems where a partially connected directed communication graph may be repaired by buying additional arcs. Eswaran and Tarjan showed that the unweighted augmentation problem is solvable in linear time, while the weighted variant is substantially harder @eswarantarjan1976. The decision version recorded as ND19 in Garey and Johnson is NP-complete @garey1979. The implementation here uses one binary variable per candidate arc, so brute-force over the candidate set yields a worst-case bound of $O^*(2^m)$ where $m = "num_potential_arcs"$. #footnote[No exact algorithm improving on brute-force is claimed here for the weighted candidate-arc formulation implemented in the codebase.]

*Example.* The canonical instance has $n = #nv$ vertices, $|A| = #ne$ existing arcs, #candidates.len() weighted candidate arcs, and bound $B = #bound$. The base graph already contains the directed 3-cycle $v_0 -> v_1 -> v_2 -> v_0$ and the strongly connected component on ${v_3, v_4, v_5}$, with only the forward bridge $v_2 -> v_3$ between them. The unique satisfying augmentation under this bound selects the single candidate arc $(v_#arc.at(0), v_#arc.at(1)))$ of weight #arc.at(2), closing the cycle $v_2 -> v_3 -> v_4 -> v_5 -> v_2$ and making every vertex reachable from every other. The all-zero configuration is infeasible because no path returns from ${v_3, v_4, v_5}$ to ${v_0, v_1, v_2}$.

#figure({
let verts = ((0, 1), (1.2, 1.6), (1.2, 0.4), (3.4, 1.0), (4.6, 1.5), (4.6, 0.5))
canvas(length: 1cm, {
for (u, v) in arcs {
draw.line(verts.at(u), verts.at(v),
stroke: 1pt + black,
mark: (end: "straight", scale: 0.4))
}
draw.line(verts.at(arc.at(0)), verts.at(arc.at(1)),
stroke: 1.6pt + blue,
mark: (end: "straight", scale: 0.45))
for (k, pos) in verts.enumerate() {
let highlighted = k == arc.at(0) or k == arc.at(1)
g-node(pos, name: "v" + str(k),
fill: if highlighted { blue.transparentize(65%) } else { white },
label: [$v_#k$])
}
})
},
caption: [Strong Connectivity Augmentation on a #{nv}-vertex digraph. Black arcs are present in $A$; the added candidate arc $(v_#arc.at(0), v_#arc.at(1)))$ is shown in blue. With bound $B = #bound$, this single augmentation makes the digraph strongly connected.],
) <fig:strong-connectivity-augmentation>
]
]
}

#{
let x = load-model-example("MinimumSumMulticenter")
let nv = graph-num-vertices(x.instance)
Expand Down
11 changes: 11 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ @book{garey1979
year = {1979}
}

@article{eswarantarjan1976,
author = {K. P. Eswaran and Robert E. Tarjan},
title = {Augmentation Problems},
journal = {SIAM Journal on Computing},
volume = {5},
number = {4},
pages = {653--665},
year = {1976},
doi = {10.1137/0205044}
}

@article{gareyJohnsonStockmeyer1976,
author = {Michael R. Garey and David S. Johnson and Larry Stockmeyer},
title = {Some Simplified {NP}-Complete Graph Problems},
Expand Down
8 changes: 8 additions & 0 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ cd problem-reductions
make cli # builds target/release/pred
```

If you prefer a workspace-local build without installing into your global Cargo bin directory:

```bash
cargo build -p problemreductions-cli --release
./target/release/pred --version
```

Verify the installation:

```bash
Expand Down Expand Up @@ -276,6 +283,7 @@ pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json
pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json
pred create X3C --universe 9 --sets "0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8" -o x3c.json
pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json
pred create StrongConnectivityAugmentation --arcs "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5>3" --candidate-arcs "3>0:5,3>1:3,3>2:4,4>0:6,4>1:2,4>2:7,5>0:4,5>1:3,5>2:1,0>3:8,0>4:3,0>5:2,1>3:6,1>4:4,1>5:5,2>4:3,2>5:7,1>0:2" --bound 1 -o sca.json
```

Canonical examples are useful when you want a known-good instance from the paper/example database.
Expand Down
21 changes: 21 additions & 0 deletions docs/src/reductions/problem_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,27 @@
}
]
},
{
"name": "StrongConnectivityAugmentation",
"description": "Add a bounded set of weighted candidate arcs to make a digraph strongly connected",
"fields": [
{
"name": "graph",
"type_name": "DirectedGraph",
"description": "The initial directed graph G=(V,A)"
},
{
"name": "candidate_arcs",
"type_name": "Vec<(usize, usize, W)>",
"description": "Candidate augmenting arcs (u, v, w(u,v)) not already present in G"
},
{
"name": "bound",
"type_name": "W::Sum",
"description": "Upper bound B on the total added weight"
}
]
},
{
"name": "SubgraphIsomorphism",
"description": "Determine if host graph G contains a subgraph isomorphic to pattern graph H",
Expand Down
15 changes: 12 additions & 3 deletions docs/src/reductions/reduction_graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,15 @@
"doc_path": "models/graph/struct.SpinGlass.html",
"complexity": "2^num_spins"
},
{
"name": "StrongConnectivityAugmentation",
"variant": {
"weight": "i32"
},
"category": "graph",
"doc_path": "models/graph/struct.StrongConnectivityAugmentation.html",
"complexity": "2^num_potential_arcs"
},
{
"name": "SubgraphIsomorphism",
"variant": {},
Expand Down Expand Up @@ -713,7 +722,7 @@
},
{
"source": 21,
"target": 56,
"target": 57,
"overhead": [
{
"field": "num_elements",
Expand Down Expand Up @@ -1341,7 +1350,7 @@
"doc_path": "rules/spinglass_casts/index.html"
},
{
"source": 57,
"source": 58,
"target": 12,
"overhead": [
{
Expand All @@ -1356,7 +1365,7 @@
"doc_path": "rules/travelingsalesman_ilp/index.html"
},
{
"source": 57,
"source": 58,
"target": 49,
"overhead": [
{
Expand Down
4 changes: 4 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ Flags by problem type:
LCS --strings
FAS --arcs [--weights] [--num-vertices]
FVS --arcs [--weights] [--num-vertices]
StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices]
FlowShopScheduling --task-lengths, --deadline [--num-processors]
MinimumTardinessSequencing --n, --deadlines [--precedence-pairs]
SCS --strings, --bound [--alphabet-size]
Expand Down Expand Up @@ -385,6 +386,9 @@ pub struct CreateArgs {
/// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0)
#[arg(long)]
pub arcs: Option<String>,
/// Candidate weighted arcs for StrongConnectivityAugmentation (e.g., 2>0:1,2>1:3)
#[arg(long)]
pub candidate_arcs: Option<String>,
/// Deadlines for MinimumTardinessSequencing (comma-separated, e.g., "5,5,5,3,3")
#[arg(long)]
pub deadlines: Option<String>,
Expand Down
100 changes: 91 additions & 9 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::cli::{CreateArgs, ExampleSide};
use crate::dispatch::ProblemJsonOutput;
use crate::output::OutputConfig;
use crate::problem_name::{resolve_problem_ref, unknown_problem_error};
use crate::problem_name::{resolve_create_problem_ref, resolve_problem_ref, unknown_problem_error};
use crate::util;
use anyhow::{bail, Context, Result};
use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample};
Expand Down Expand Up @@ -57,6 +57,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.pattern.is_none()
&& args.strings.is_none()
&& args.arcs.is_none()
&& args.candidate_arcs.is_none()
&& args.deadlines.is_none()
&& args.precedence_pairs.is_none()
&& args.task_lengths.is_none()
Expand Down Expand Up @@ -208,6 +209,8 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
"Vec<BigUint>" => "comma-separated nonnegative decimal integers: 3,7,1,8",
"Vec<i64>" => "comma-separated integers: 3,7,1,8",
"DirectedGraph" => "directed arcs: 0>1,1>2,2>0",
"Vec<(usize, usize, W)>" => "candidate arcs with weights: 0>2:5,2>1:3",
"W::Sum" => "integer",
_ => "value",
}
}
Expand Down Expand Up @@ -237,6 +240,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"MinimumSumMulticenter" => {
"--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2"
}
"StrongConnectivityAugmentation" => {
"--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1"
}
"PartitionIntoTriangles" => "--graph 0-1,1-2,0-2",
"Factoring" => "--target 15 --m 4 --n 4",
"OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5",
Expand Down Expand Up @@ -320,13 +326,13 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
anyhow::anyhow!("Missing problem type.\n\nUsage: pred create <PROBLEM> [FLAGS]")
})?;
let rgraph = problemreductions::rules::ReductionGraph::new();
let resolved = resolve_problem_ref(problem, &rgraph)?;
let canonical = &resolved.name;
let resolved = resolve_create_problem_ref(problem, &rgraph)?;
let canonical = resolved.name;
let resolved_variant = resolved.variant;
let graph_type = resolved_graph_type(&resolved_variant);

if args.random {
return create_random(args, canonical, &resolved_variant, out);
return create_random(args, &canonical, &resolved_variant, out);
}

// ILP and CircuitSAT have complex input structures not suited for CLI flags.
Expand All @@ -347,7 +353,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
} else {
None
};
print_problem_help(canonical, gt)?;
print_problem_help(&canonical, gt)?;
std::process::exit(2);
}

Expand All @@ -357,7 +363,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
| "MinimumVertexCover"
| "MaximumClique"
| "MinimumDominatingSet" => {
create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)?
create_vertex_weight_problem(args, &canonical, graph_type, &resolved_variant)?
}

// Graph partitioning (graph only, no weights)
Expand Down Expand Up @@ -563,7 +569,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {

// MaximalIS — same as MIS (graph + vertex weights)
"MaximalIS" => {
create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)?
create_vertex_weight_problem(args, &canonical, graph_type, &resolved_variant)?
}

// BinPacking
Expand Down Expand Up @@ -930,6 +936,27 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// StrongConnectivityAugmentation
"StrongConnectivityAugmentation" => {
let usage = "Usage: pred create StrongConnectivityAugmentation --arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1 [--num-vertices N]";
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"StrongConnectivityAugmentation requires --arcs\n\n\
{usage}"
)
})?;
let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?;
let candidate_arcs = parse_candidate_arcs(args, graph.num_vertices())?;
let bound = parse_nonnegative_i32_bound(args, "StrongConnectivityAugmentation", usage)?;
(
ser(
StrongConnectivityAugmentation::try_new(graph, candidate_arcs, bound)
.map_err(|e| anyhow::anyhow!(e))?,
)?,
resolved_variant.clone(),
)
}

// MinimumSumMulticenter (p-median)
"MinimumSumMulticenter" => {
let (graph, n) = parse_graph(args).map_err(|e| {
Expand Down Expand Up @@ -1086,11 +1113,11 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

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

let output = ProblemJsonOutput {
problem_type: canonical.to_string(),
problem_type: canonical,
variant,
data,
};
Expand Down Expand Up @@ -1565,6 +1592,61 @@ fn parse_arc_weights(args: &CreateArgs, num_arcs: usize) -> Result<Vec<i32>> {
}
}

/// Parse `--candidate-arcs` as `u>v:w` entries for StrongConnectivityAugmentation.
fn parse_candidate_arcs(
args: &CreateArgs,
num_vertices: usize,
) -> Result<Vec<(usize, usize, i32)>> {
let arcs_str = args.candidate_arcs.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"StrongConnectivityAugmentation requires --candidate-arcs (e.g., \"2>0:1,2>1:3\")"
)
})?;

arcs_str
.split(',')
.map(|entry| {
let entry = entry.trim();
let (arc_part, weight_part) = entry.split_once(':').ok_or_else(|| {
anyhow::anyhow!(
"Invalid candidate arc '{}': expected format u>v:w (e.g., 2>0:1)",
entry
)
})?;
let parts: Vec<&str> = arc_part.split('>').collect();
if parts.len() != 2 {
bail!(
"Invalid candidate arc '{}': expected format u>v:w (e.g., 2>0:1)",
entry
);
}

let u: usize = parts[0].parse()?;
let v: usize = parts[1].parse()?;
anyhow::ensure!(
u < num_vertices && v < num_vertices,
"candidate arc ({}, {}) references vertex >= num_vertices ({})",
u,
v,
num_vertices
);

let weight: i32 = weight_part.parse()?;
Ok((u, v, weight))
})
.collect()
}

fn parse_nonnegative_i32_bound(args: &CreateArgs, problem: &str, usage: &str) -> Result<i32> {
let raw_bound = args
.bound
.ok_or_else(|| anyhow::anyhow!("{problem} requires --bound\n\n{usage}"))?;
let bound =
i32::try_from(raw_bound).map_err(|_| anyhow::anyhow!("{problem} bound must fit in i32"))?;
anyhow::ensure!(bound >= 0, "{problem} bound must be nonnegative");
Ok(bound)
}

/// Handle `pred create <PROBLEM> --random ...`
fn create_random(
args: &CreateArgs,
Expand Down
Loading
Loading