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 @@ -108,6 +108,7 @@
"ShortestCommonSupersequence": [Shortest Common Supersequence],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
"SteinerTree": [Steiner Tree],
"StrongConnectivityAugmentation": [Strong Connectivity Augmentation],
"SubgraphIsomorphism": [Subgraph Isomorphism],
"PartitionIntoTriangles": [Partition Into Triangles],
"FlowShopScheduling": [Flow Shop Scheduling],
Expand Down Expand Up @@ -1070,6 +1071,49 @@ is feasible: each set induces a connected subgraph, the component weights are $2
]
]
}
#{
let x = load-model-example("StrongConnectivityAugmentation")
let nv = x.instance.graph.inner.nodes.len()
let ne = x.instance.graph.inner.edges.len()
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("MinimumMultiwayCut")
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
1 change: 1 addition & 0 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,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
```

For `LengthBoundedDisjointPaths`, the CLI flag `--bound` maps to the JSON field
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 @@ -249,6 +249,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 @@ -447,6 +448,9 @@ pub struct CreateArgs {
/// Total budget for selected potential edges
#[arg(long)]
pub budget: 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
78 changes: 77 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExamp
use problemreductions::models::algebraic::{ClosestVectorProblem, BMF};
use problemreductions::models::graph::{
GraphPartitioning, HamiltonianPath, LengthBoundedDisjointPaths, MinimumMultiwayCut,
MultipleChoiceBranching, SteinerTree,
MultipleChoiceBranching, SteinerTree, StrongConnectivityAugmentation,
};
use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
Expand Down Expand Up @@ -78,6 +78,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.potential_edges.is_none()
&& args.budget.is_none()
&& args.precedence_pairs.is_none()
Expand Down Expand Up @@ -288,6 +289,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" --capacities 1,1,1,1,1,1,1,1 --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 --requirement-1 1 --requirement-2 1"
}
"MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"",
"StrongConnectivityAugmentation" => {
"--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1"
}
"RuralPostman" => {
"--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4"
}
Expand Down Expand Up @@ -1422,6 +1426,32 @@ 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 = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"StrongConnectivityAugmentation requires --bound\n\n\
{usage}"
)
})? as i32;
(
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 @@ -2280,6 +2310,51 @@ 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 w: i32 = weight_part.parse()?;
Ok((u, v, w))
})
.collect()
}

/// Handle `pred create <PROBLEM> --random ...`
fn create_random(
args: &CreateArgs,
Expand Down Expand Up @@ -2613,6 +2688,7 @@ mod tests {
arcs: None,
potential_edges: None,
budget: None,
candidate_arcs: None,
deadlines: None,
precedence_pairs: None,
task_lengths: None,
Expand Down
24 changes: 24 additions & 0 deletions problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ mod tests {
use problemreductions::models::graph::MaximumIndependentSet;
use problemreductions::models::misc::BinPacking;
use problemreductions::topology::SimpleGraph;
use serde_json::json;

#[test]
fn test_load_problem_alias_uses_registry_dispatch() {
Expand All @@ -208,6 +209,29 @@ mod tests {
assert!(loaded.is_err());
}

#[test]
fn test_load_problem_rejects_invalid_strong_connectivity_augmentation_instance() {
let variant = BTreeMap::from([("weight".to_string(), "i32".to_string())]);
let data = json!({
"graph": {
"inner": {
"edge_property": "directed",
"nodes": [null, null, null],
"node_holes": [],
"edges": [[0, 1, null], [1, 2, null]]
}
},
"candidate_arcs": [[0, 3, 1]],
"bound": 1
});

let loaded = load_problem("StrongConnectivityAugmentation", &variant, data);
assert!(loaded.is_err());
let err = loaded.err().unwrap().to_string();
assert!(err.contains("candidate arc"), "err: {err}");
assert!(err.contains("num_vertices"), "err: {err}");
}

#[test]
fn test_serialize_any_problem_round_trips_bin_packing() {
let problem = BinPacking::new(vec![3i32, 3, 2, 2], 5i32);
Expand Down
1 change: 1 addition & 0 deletions src/example_db/fixtures/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
{"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]},
{"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]},
{"problem":"SteinerTree","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,5,2,1,5,6,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}]},
{"problem":"StrongConnectivityAugmentation","variant":{"weight":"i32"},"instance":{"bound":1,"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]],"graph":{"inner":{"edge_property":"directed","edges":[[0,1,null],[1,2,null],[2,0,null],[3,4,null],[4,3,null],[2,3,null],[4,5,null],[5,3,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"metric":true},{"config":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"metric":true}]},
{"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]},
{"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]}
],
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub mod prelude {
BicliqueCover, BiconnectivityAugmentation, BoundedComponentSpanningForest,
DirectedTwoCommodityIntegralFlow, GraphPartitioning, HamiltonianPath,
IsomorphicSpanningTree, LengthBoundedDisjointPaths, SpinGlass, SteinerTree,
SubgraphIsomorphism,
StrongConnectivityAugmentation, SubgraphIsomorphism,
};
pub use crate::models::graph::{
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
Expand Down
4 changes: 4 additions & 0 deletions src/models/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
//! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem)
//! - [`DirectedTwoCommodityIntegralFlow`]: Directed two-commodity integral flow (satisfaction)
//! - [`UndirectedTwoCommodityIntegralFlow`]: Two-commodity integral flow on undirected graphs
//! - [`StrongConnectivityAugmentation`]: Strong connectivity augmentation with weighted candidate arcs

pub(crate) mod biclique_cover;
pub(crate) mod biconnectivity_augmentation;
Expand Down Expand Up @@ -57,6 +58,7 @@ pub(crate) mod partition_into_triangles;
pub(crate) mod rural_postman;
pub(crate) mod spin_glass;
pub(crate) mod steiner_tree;
pub(crate) mod strong_connectivity_augmentation;
pub(crate) mod subgraph_isomorphism;
pub(crate) mod traveling_salesman;
pub(crate) mod undirected_two_commodity_integral_flow;
Expand Down Expand Up @@ -87,6 +89,7 @@ pub use partition_into_triangles::PartitionIntoTriangles;
pub use rural_postman::RuralPostman;
pub use spin_glass::SpinGlass;
pub use steiner_tree::SteinerTree;
pub use strong_connectivity_augmentation::StrongConnectivityAugmentation;
pub use subgraph_isomorphism::SubgraphIsomorphism;
pub use traveling_salesman::TravelingSalesman;
pub use undirected_two_commodity_integral_flow::UndirectedTwoCommodityIntegralFlow;
Expand Down Expand Up @@ -118,5 +121,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(steiner_tree::canonical_model_example_specs());
specs.extend(directed_two_commodity_integral_flow::canonical_model_example_specs());
specs.extend(undirected_two_commodity_integral_flow::canonical_model_example_specs());
specs.extend(strong_connectivity_augmentation::canonical_model_example_specs());
specs
}
Loading
Loading