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
61 changes: 61 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"ConsistencyOfDatabaseFrequencyTables": [Consistency of Database Frequency Tables],
"ClosestVectorProblem": [Closest Vector Problem],
"ConsecutiveSets": [Consecutive Sets],
"DisjointConnectingPaths": [Disjoint Connecting Paths],
"MinimumMultiwayCut": [Minimum Multiway Cut],
"OptimalLinearArrangement": [Optimal Linear Arrangement],
"RuralPostman": [Rural Postman],
Expand Down Expand Up @@ -962,6 +963,66 @@ is feasible: each set induces a connected subgraph, the component weights are $2
]
]
}
#{
let x = load-model-example("DisjointConnectingPaths")
let nv = graph-num-vertices(x.instance)
let ne = graph-num-edges(x.instance)
let chosen-edges = ((0, 1), (1, 3), (2, 4), (4, 5))
[
#problem-def("DisjointConnectingPaths")[
Given an undirected graph $G = (V, E)$ and pairwise disjoint terminal pairs $(s_1, t_1), dots, (s_k, t_k)$, determine whether $G$ contains $k$ mutually vertex-disjoint paths such that path $P_i$ joins $s_i$ to $t_i$ for every $i$.
][
Disjoint Connecting Paths is the classical routing form of the vertex-disjoint paths problem, catalogued as ND40 in Garey & Johnson @garey1979. When the number of terminal pairs $k$ is part of the input, the problem is NP-complete @karp1972. In contrast, for every fixed $k$, Robertson and Seymour give an $O(n^3)$ algorithm @robertsonSeymour1995, and Kawarabayashi, Kobayashi, and Reed later improve the dependence on $n$ to $O(n^2)$ @kawarabayashiKobayashiReed2012. The implementation in this crate uses one binary variable per undirected edge, so brute-force search explores an $O^*(2^|E|)$ configuration space.#footnote[This is the exact-search bound induced by the edge-subset encoding implemented in the codebase; no sharper general exact worst-case bound is claimed here.]

*Example.* Consider the repaired YES instance with $n = #nv$ vertices, $|E| = #ne$ edges, and terminal pairs $(v_0, v_3)$ and $(v_2, v_5)$. Selecting the edges $v_0v_1$, $v_1v_3$, $v_2v_4$, and $v_4v_5$ yields the two vertex-disjoint paths $v_0 arrow v_1 arrow v_3$ and $v_2 arrow v_4 arrow v_5$, so the instance is satisfying.

#pred-commands(
"pred create --example DisjointConnectingPaths -o disjoint-connecting-paths.json",
"pred solve disjoint-connecting-paths.json",
"pred evaluate disjoint-connecting-paths.json --config " + x.optimal_config.map(str).join(","),
)

#figure(
canvas(length: 1cm, {
let blue = graph-colors.at(0)
let gray = luma(180)
let verts = (
(0, 1.2),
(1.4, 1.2),
(0, 0),
(2.8, 1.2),
(1.4, 0),
(2.8, 0),
)
let edges = ((0, 1), (1, 3), (0, 2), (1, 4), (2, 4), (3, 5), (4, 5))
for (u, v) in edges {
let selected = chosen-edges.any(e =>
(e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u)
)
g-edge(verts.at(u), verts.at(v),
stroke: if selected { 2pt + blue } else { 1pt + gray })
}
for (k, pos) in verts.enumerate() {
let terminal = k == 0 or k == 2 or k == 3 or k == 5
g-node(pos, name: "v" + str(k),
fill: if terminal { blue } else { white },
label: if terminal {
text(fill: white)[
#if k == 0 { $s_1$ }
else if k == 3 { $t_1$ }
else if k == 2 { $s_2$ }
else { $t_2$ }
]
} else [
$v_#k$
])
}
}),
caption: [A satisfying Disjoint Connecting Paths instance with terminal pairs $(v_0, v_3)$ and $(v_2, v_5)$. The highlighted edges form the vertex-disjoint paths $v_0 arrow v_1 arrow v_3$ and $v_2 arrow v_4 arrow v_5$.],
) <fig:disjoint-connecting-paths>
]
]
}
#{
let x = load-model-example("GeneralizedHex")
let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1)))
Expand Down
22 changes: 22 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,28 @@ @article{busingstiller2011
doi = {10.1002/net.20386}
}

@article{kawarabayashiKobayashiReed2012,
author = {Ken-ichi Kawarabayashi and Yusuke Kobayashi and Bruce Reed},
title = {The disjoint paths problem in quadratic time},
journal = {Journal of Combinatorial Theory, Series B},
volume = {102},
number = {2},
pages = {424--435},
year = {2012},
doi = {10.1016/j.jctb.2011.07.004}
}

@article{robertsonSeymour1995,
author = {Neil Robertson and P. D. Seymour},
title = {Graph Minors. XIII. The Disjoint Paths Problem},
journal = {Journal of Combinatorial Theory, Series B},
volume = {63},
number = {1},
pages = {65--110},
year = {1995},
doi = {10.1006/jctb.1995.1006}
}

@article{bruckerGareyJohnson1977,
author = {Peter Brucker and Michael R. Garey and David S. Johnson},
title = {Scheduling equal-length tasks under tree-like precedence constraints to minimize maximum lateness},
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 @@ -238,6 +238,7 @@ Flags by problem type:
UndirectedFlowLowerBounds --graph, --capacities, --lower-bounds, --source, --sink, --requirement
IntegralFlowBundles --arcs, --bundles, --bundle-capacities, --source, --sink, --requirement [--num-vertices]
UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
DisjointConnectingPaths --graph, --terminal-pairs
IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs
IsomorphicSpanningTree --graph, --tree
KthBestSpanningTree --graph, --edge-weights, --k, --bound
Expand Down Expand Up @@ -522,6 +523,9 @@ pub struct CreateArgs {
/// Terminal vertices for SteinerTree or MinimumMultiwayCut (comma-separated indices, e.g., "0,2,4")
#[arg(long)]
pub terminals: Option<String>,
/// Terminal pairs for DisjointConnectingPaths (comma-separated pairs, e.g., "0-3,2-5")
#[arg(long = "terminal-pairs")]
pub terminal_pairs: Option<String>,
/// Tree edge list for IsomorphicSpanningTree (e.g., 0-1,1-2,2-3)
#[arg(long)]
pub tree: Option<String>,
Expand Down
115 changes: 111 additions & 4 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ use problemreductions::models::algebraic::{
};
use problemreductions::models::formula::Quantifier;
use problemreductions::models::graph::{
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles,
LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets,
MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, PathConstrainedNetworkFlow,
SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
DisjointConnectingPaths, GeneralizedHex, GraphPartitioning, HamiltonianCircuit,
HamiltonianPath, IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, LongestPath,
MinimumCutIntoBoundedSets, MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching,
PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
};
use problemreductions::models::misc::{
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery,
Expand Down Expand Up @@ -103,6 +103,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.deadlines.is_none()
&& args.lengths.is_none()
&& args.terminals.is_none()
&& args.terminal_pairs.is_none()
&& args.tree.is_none()
&& args.required_edges.is_none()
&& args.bound.is_none()
Expand Down Expand Up @@ -545,6 +546,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"UndirectedTwoCommodityIntegralFlow" => {
"--graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1"
},
"DisjointConnectingPaths" => {
"--graph 0-1,1-3,0-2,1-4,2-4,3-5,4-5 --terminal-pairs 0-3,2-5"
}
"IntegralFlowHomologousArcs" => {
"--arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\""
}
Expand Down Expand Up @@ -765,6 +769,7 @@ fn help_flag_hint(
match (canonical, field_name) {
("BoundedComponentSpanningForest", "max_weight") => "integer",
("SequencingWithinIntervals", "release_times") => "comma-separated integers: 0,0,5",
("DisjointConnectingPaths", "terminal_pairs") => "comma-separated pairs: 0-3,2-5",
("PrimeAttributeName", "dependencies") => {
"semicolon-separated dependencies: \"0,1>2,3;2,3>0,1\""
}
Expand Down Expand Up @@ -1152,6 +1157,19 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// DisjointConnectingPaths (graph + terminal pairs)
"DisjointConnectingPaths" => {
let usage =
"Usage: pred create DisjointConnectingPaths --graph 0-1,1-3,0-2,1-4,2-4,3-5,4-5 --terminal-pairs 0-3,2-5";
let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
let terminal_pairs = parse_terminal_pairs(args, graph.num_vertices())
.map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
(
ser(DisjointConnectingPaths::new(graph, terminal_pairs))?,
resolved_variant.clone(),
)
}

// IntegralFlowWithMultipliers (directed arcs + capacities + source/sink + multipliers + requirement)
"IntegralFlowWithMultipliers" => {
let usage = "Usage: pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2";
Expand Down Expand Up @@ -4487,6 +4505,38 @@ fn parse_terminals(args: &CreateArgs, num_vertices: usize) -> Result<Vec<usize>>
Ok(terminals)
}

/// Parse `--terminal-pairs` as comma-separated `u-v` vertex pairs.
fn parse_terminal_pairs(args: &CreateArgs, num_vertices: usize) -> Result<Vec<(usize, usize)>> {
let raw = args
.terminal_pairs
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--terminal-pairs required (e.g., \"0-3,2-5\")"))?;
let terminal_pairs = util::parse_edge_pairs(raw)?;
anyhow::ensure!(
!terminal_pairs.is_empty(),
"at least 1 terminal pair required"
);

let mut used = BTreeSet::new();
for &(source, sink) in &terminal_pairs {
anyhow::ensure!(
source < num_vertices,
"terminal pair source {source} >= num_vertices ({num_vertices})"
);
anyhow::ensure!(
sink < num_vertices,
"terminal pair sink {sink} >= num_vertices ({num_vertices})"
);
anyhow::ensure!(source != sink, "terminal pair endpoints must be distinct");
anyhow::ensure!(
used.insert(source) && used.insert(sink),
"terminal vertices must be pairwise disjoint across terminal pairs"
);
}

Ok(terminal_pairs)
}

fn ensure_positive_i32_values(values: &[i32], label: &str) -> Result<()> {
if values.iter().any(|&value| value <= 0) {
bail!("All {label} must be positive (> 0)");
Expand Down Expand Up @@ -6736,6 +6786,7 @@ mod tests {
release_times: None,
lengths: None,
terminals: None,
terminal_pairs: None,
tree: None,
required_edges: None,
bound: None,
Expand Down Expand Up @@ -6843,6 +6894,62 @@ mod tests {
assert_eq!(parse_budget(&args).unwrap(), 7);
}

#[test]
fn test_create_disjoint_connecting_paths_json() {
use crate::dispatch::ProblemJsonOutput;
use problemreductions::models::graph::DisjointConnectingPaths;

let mut args = empty_args();
args.problem = Some("DisjointConnectingPaths".to_string());
args.graph = Some("0-1,1-3,0-2,1-4,2-4,3-5,4-5".to_string());
args.terminal_pairs = Some("0-3,2-5".to_string());

let output_path =
std::env::temp_dir().join(format!("dcp-create-{}.json", std::process::id()));
let out = OutputConfig {
output: Some(output_path.clone()),
quiet: true,
json: false,
auto_json: false,
};

create(&args, &out).unwrap();

let json = std::fs::read_to_string(&output_path).unwrap();
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
assert_eq!(created.problem_type, "DisjointConnectingPaths");
assert_eq!(
created.variant,
BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())])
);

let problem: DisjointConnectingPaths<SimpleGraph> =
serde_json::from_value(created.data).unwrap();
assert_eq!(problem.num_vertices(), 6);
assert_eq!(problem.num_edges(), 7);
assert_eq!(problem.terminal_pairs(), &[(0, 3), (2, 5)]);

let _ = std::fs::remove_file(output_path);
}

#[test]
fn test_create_disjoint_connecting_paths_rejects_overlapping_terminal_pairs() {
let mut args = empty_args();
args.problem = Some("DisjointConnectingPaths".to_string());
args.graph = Some("0-1,1-2,2-3,3-4".to_string());
args.terminal_pairs = Some("0-2,2-4".to_string());

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("pairwise disjoint"));
}

#[test]
fn test_parse_homologous_pairs() {
let mut args = empty_args();
Expand Down
10 changes: 5 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ pub mod prelude {
pub use crate::models::graph::{
AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover,
BiconnectivityAugmentation, BottleneckTravelingSalesman, BoundedComponentSpanningForest,
DirectedTwoCommodityIntegralFlow, GeneralizedHex, GraphPartitioning, HamiltonianCircuit,
HamiltonianPath, IntegralFlowBundles, IntegralFlowHomologousArcs,
IntegralFlowWithMultipliers, IsomorphicSpanningTree, KClique, KthBestSpanningTree,
LengthBoundedDisjointPaths, LongestPath, MixedChinesePostman, SpinGlass, SteinerTree,
StrongConnectivityAugmentation, SubgraphIsomorphism,
DirectedTwoCommodityIntegralFlow, DisjointConnectingPaths, GeneralizedHex,
GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles,
IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, IsomorphicSpanningTree, KClique,
KthBestSpanningTree, LengthBoundedDisjointPaths, LongestPath, MixedChinesePostman,
SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism,
};
pub use crate::models::graph::{
KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet,
Expand Down
Loading
Loading