Skip to content

Commit adfe9ae

Browse files
GiggleLiuisPANNclaude
authored
Fix #297: [Model] DisjointConnectingPaths (#746)
* Add plan for #297: [Model] DisjointConnectingPaths * Implement #297: [Model] DisjointConnectingPaths * chore: remove plan file after implementation * cargo fmt after merge --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6356007 commit adfe9ae

9 files changed

Lines changed: 592 additions & 16 deletions

File tree

docs/paper/reductions.typ

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
"ConsistencyOfDatabaseFrequencyTables": [Consistency of Database Frequency Tables],
121121
"ClosestVectorProblem": [Closest Vector Problem],
122122
"ConsecutiveSets": [Consecutive Sets],
123+
"DisjointConnectingPaths": [Disjoint Connecting Paths],
123124
"MinimumMultiwayCut": [Minimum Multiway Cut],
124125
"OptimalLinearArrangement": [Optimal Linear Arrangement],
125126
"RuralPostman": [Rural Postman],
@@ -962,6 +963,66 @@ is feasible: each set induces a connected subgraph, the component weights are $2
962963
]
963964
]
964965
}
966+
#{
967+
let x = load-model-example("DisjointConnectingPaths")
968+
let nv = graph-num-vertices(x.instance)
969+
let ne = graph-num-edges(x.instance)
970+
let chosen-edges = ((0, 1), (1, 3), (2, 4), (4, 5))
971+
[
972+
#problem-def("DisjointConnectingPaths")[
973+
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$.
974+
][
975+
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.]
976+
977+
*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.
978+
979+
#pred-commands(
980+
"pred create --example DisjointConnectingPaths -o disjoint-connecting-paths.json",
981+
"pred solve disjoint-connecting-paths.json",
982+
"pred evaluate disjoint-connecting-paths.json --config " + x.optimal_config.map(str).join(","),
983+
)
984+
985+
#figure(
986+
canvas(length: 1cm, {
987+
let blue = graph-colors.at(0)
988+
let gray = luma(180)
989+
let verts = (
990+
(0, 1.2),
991+
(1.4, 1.2),
992+
(0, 0),
993+
(2.8, 1.2),
994+
(1.4, 0),
995+
(2.8, 0),
996+
)
997+
let edges = ((0, 1), (1, 3), (0, 2), (1, 4), (2, 4), (3, 5), (4, 5))
998+
for (u, v) in edges {
999+
let selected = chosen-edges.any(e =>
1000+
(e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u)
1001+
)
1002+
g-edge(verts.at(u), verts.at(v),
1003+
stroke: if selected { 2pt + blue } else { 1pt + gray })
1004+
}
1005+
for (k, pos) in verts.enumerate() {
1006+
let terminal = k == 0 or k == 2 or k == 3 or k == 5
1007+
g-node(pos, name: "v" + str(k),
1008+
fill: if terminal { blue } else { white },
1009+
label: if terminal {
1010+
text(fill: white)[
1011+
#if k == 0 { $s_1$ }
1012+
else if k == 3 { $t_1$ }
1013+
else if k == 2 { $s_2$ }
1014+
else { $t_2$ }
1015+
]
1016+
} else [
1017+
$v_#k$
1018+
])
1019+
}
1020+
}),
1021+
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$.],
1022+
) <fig:disjoint-connecting-paths>
1023+
]
1024+
]
1025+
}
9651026
#{
9661027
let x = load-model-example("GeneralizedHex")
9671028
let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1)))

docs/paper/references.bib

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,28 @@ @article{busingstiller2011
191191
doi = {10.1002/net.20386}
192192
}
193193

194+
@article{kawarabayashiKobayashiReed2012,
195+
author = {Ken-ichi Kawarabayashi and Yusuke Kobayashi and Bruce Reed},
196+
title = {The disjoint paths problem in quadratic time},
197+
journal = {Journal of Combinatorial Theory, Series B},
198+
volume = {102},
199+
number = {2},
200+
pages = {424--435},
201+
year = {2012},
202+
doi = {10.1016/j.jctb.2011.07.004}
203+
}
204+
205+
@article{robertsonSeymour1995,
206+
author = {Neil Robertson and P. D. Seymour},
207+
title = {Graph Minors. XIII. The Disjoint Paths Problem},
208+
journal = {Journal of Combinatorial Theory, Series B},
209+
volume = {63},
210+
number = {1},
211+
pages = {65--110},
212+
year = {1995},
213+
doi = {10.1006/jctb.1995.1006}
214+
}
215+
194216
@article{bruckerGareyJohnson1977,
195217
author = {Peter Brucker and Michael R. Garey and David S. Johnson},
196218
title = {Scheduling equal-length tasks under tree-like precedence constraints to minimize maximum lateness},

problemreductions-cli/src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ Flags by problem type:
238238
UndirectedFlowLowerBounds --graph, --capacities, --lower-bounds, --source, --sink, --requirement
239239
IntegralFlowBundles --arcs, --bundles, --bundle-capacities, --source, --sink, --requirement [--num-vertices]
240240
UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
241+
DisjointConnectingPaths --graph, --terminal-pairs
241242
IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs
242243
IsomorphicSpanningTree --graph, --tree
243244
KthBestSpanningTree --graph, --edge-weights, --k, --bound
@@ -522,6 +523,9 @@ pub struct CreateArgs {
522523
/// Terminal vertices for SteinerTree or MinimumMultiwayCut (comma-separated indices, e.g., "0,2,4")
523524
#[arg(long)]
524525
pub terminals: Option<String>,
526+
/// Terminal pairs for DisjointConnectingPaths (comma-separated pairs, e.g., "0-3,2-5")
527+
#[arg(long = "terminal-pairs")]
528+
pub terminal_pairs: Option<String>,
525529
/// Tree edge list for IsomorphicSpanningTree (e.g., 0-1,1-2,2-3)
526530
#[arg(long)]
527531
pub tree: Option<String>,

problemreductions-cli/src/commands/create.rs

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ use problemreductions::models::algebraic::{
1212
};
1313
use problemreductions::models::formula::Quantifier;
1414
use problemreductions::models::graph::{
15-
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles,
16-
LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets,
17-
MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, PathConstrainedNetworkFlow,
18-
SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
15+
DisjointConnectingPaths, GeneralizedHex, GraphPartitioning, HamiltonianCircuit,
16+
HamiltonianPath, IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, LongestPath,
17+
MinimumCutIntoBoundedSets, MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching,
18+
PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
1919
};
2020
use problemreductions::models::misc::{
2121
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery,
@@ -103,6 +103,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
103103
&& args.deadlines.is_none()
104104
&& args.lengths.is_none()
105105
&& args.terminals.is_none()
106+
&& args.terminal_pairs.is_none()
106107
&& args.tree.is_none()
107108
&& args.required_edges.is_none()
108109
&& args.bound.is_none()
@@ -545,6 +546,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
545546
"UndirectedTwoCommodityIntegralFlow" => {
546547
"--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"
547548
},
549+
"DisjointConnectingPaths" => {
550+
"--graph 0-1,1-3,0-2,1-4,2-4,3-5,4-5 --terminal-pairs 0-3,2-5"
551+
}
548552
"IntegralFlowHomologousArcs" => {
549553
"--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\""
550554
}
@@ -765,6 +769,7 @@ fn help_flag_hint(
765769
match (canonical, field_name) {
766770
("BoundedComponentSpanningForest", "max_weight") => "integer",
767771
("SequencingWithinIntervals", "release_times") => "comma-separated integers: 0,0,5",
772+
("DisjointConnectingPaths", "terminal_pairs") => "comma-separated pairs: 0-3,2-5",
768773
("PrimeAttributeName", "dependencies") => {
769774
"semicolon-separated dependencies: \"0,1>2,3;2,3>0,1\""
770775
}
@@ -1152,6 +1157,19 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
11521157
)
11531158
}
11541159

1160+
// DisjointConnectingPaths (graph + terminal pairs)
1161+
"DisjointConnectingPaths" => {
1162+
let usage =
1163+
"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";
1164+
let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
1165+
let terminal_pairs = parse_terminal_pairs(args, graph.num_vertices())
1166+
.map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
1167+
(
1168+
ser(DisjointConnectingPaths::new(graph, terminal_pairs))?,
1169+
resolved_variant.clone(),
1170+
)
1171+
}
1172+
11551173
// IntegralFlowWithMultipliers (directed arcs + capacities + source/sink + multipliers + requirement)
11561174
"IntegralFlowWithMultipliers" => {
11571175
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";
@@ -4487,6 +4505,38 @@ fn parse_terminals(args: &CreateArgs, num_vertices: usize) -> Result<Vec<usize>>
44874505
Ok(terminals)
44884506
}
44894507

4508+
/// Parse `--terminal-pairs` as comma-separated `u-v` vertex pairs.
4509+
fn parse_terminal_pairs(args: &CreateArgs, num_vertices: usize) -> Result<Vec<(usize, usize)>> {
4510+
let raw = args
4511+
.terminal_pairs
4512+
.as_deref()
4513+
.ok_or_else(|| anyhow::anyhow!("--terminal-pairs required (e.g., \"0-3,2-5\")"))?;
4514+
let terminal_pairs = util::parse_edge_pairs(raw)?;
4515+
anyhow::ensure!(
4516+
!terminal_pairs.is_empty(),
4517+
"at least 1 terminal pair required"
4518+
);
4519+
4520+
let mut used = BTreeSet::new();
4521+
for &(source, sink) in &terminal_pairs {
4522+
anyhow::ensure!(
4523+
source < num_vertices,
4524+
"terminal pair source {source} >= num_vertices ({num_vertices})"
4525+
);
4526+
anyhow::ensure!(
4527+
sink < num_vertices,
4528+
"terminal pair sink {sink} >= num_vertices ({num_vertices})"
4529+
);
4530+
anyhow::ensure!(source != sink, "terminal pair endpoints must be distinct");
4531+
anyhow::ensure!(
4532+
used.insert(source) && used.insert(sink),
4533+
"terminal vertices must be pairwise disjoint across terminal pairs"
4534+
);
4535+
}
4536+
4537+
Ok(terminal_pairs)
4538+
}
4539+
44904540
fn ensure_positive_i32_values(values: &[i32], label: &str) -> Result<()> {
44914541
if values.iter().any(|&value| value <= 0) {
44924542
bail!("All {label} must be positive (> 0)");
@@ -6736,6 +6786,7 @@ mod tests {
67366786
release_times: None,
67376787
lengths: None,
67386788
terminals: None,
6789+
terminal_pairs: None,
67396790
tree: None,
67406791
required_edges: None,
67416792
bound: None,
@@ -6843,6 +6894,62 @@ mod tests {
68436894
assert_eq!(parse_budget(&args).unwrap(), 7);
68446895
}
68456896

6897+
#[test]
6898+
fn test_create_disjoint_connecting_paths_json() {
6899+
use crate::dispatch::ProblemJsonOutput;
6900+
use problemreductions::models::graph::DisjointConnectingPaths;
6901+
6902+
let mut args = empty_args();
6903+
args.problem = Some("DisjointConnectingPaths".to_string());
6904+
args.graph = Some("0-1,1-3,0-2,1-4,2-4,3-5,4-5".to_string());
6905+
args.terminal_pairs = Some("0-3,2-5".to_string());
6906+
6907+
let output_path =
6908+
std::env::temp_dir().join(format!("dcp-create-{}.json", std::process::id()));
6909+
let out = OutputConfig {
6910+
output: Some(output_path.clone()),
6911+
quiet: true,
6912+
json: false,
6913+
auto_json: false,
6914+
};
6915+
6916+
create(&args, &out).unwrap();
6917+
6918+
let json = std::fs::read_to_string(&output_path).unwrap();
6919+
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
6920+
assert_eq!(created.problem_type, "DisjointConnectingPaths");
6921+
assert_eq!(
6922+
created.variant,
6923+
BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())])
6924+
);
6925+
6926+
let problem: DisjointConnectingPaths<SimpleGraph> =
6927+
serde_json::from_value(created.data).unwrap();
6928+
assert_eq!(problem.num_vertices(), 6);
6929+
assert_eq!(problem.num_edges(), 7);
6930+
assert_eq!(problem.terminal_pairs(), &[(0, 3), (2, 5)]);
6931+
6932+
let _ = std::fs::remove_file(output_path);
6933+
}
6934+
6935+
#[test]
6936+
fn test_create_disjoint_connecting_paths_rejects_overlapping_terminal_pairs() {
6937+
let mut args = empty_args();
6938+
args.problem = Some("DisjointConnectingPaths".to_string());
6939+
args.graph = Some("0-1,1-2,2-3,3-4".to_string());
6940+
args.terminal_pairs = Some("0-2,2-4".to_string());
6941+
6942+
let out = OutputConfig {
6943+
output: None,
6944+
quiet: true,
6945+
json: false,
6946+
auto_json: false,
6947+
};
6948+
6949+
let err = create(&args, &out).unwrap_err().to_string();
6950+
assert!(err.contains("pairwise disjoint"));
6951+
}
6952+
68466953
#[test]
68476954
fn test_parse_homologous_pairs() {
68486955
let mut args = empty_args();

src/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ pub mod prelude {
5050
pub use crate::models::graph::{
5151
AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover,
5252
BiconnectivityAugmentation, BottleneckTravelingSalesman, BoundedComponentSpanningForest,
53-
DirectedTwoCommodityIntegralFlow, GeneralizedHex, GraphPartitioning, HamiltonianCircuit,
54-
HamiltonianPath, IntegralFlowBundles, IntegralFlowHomologousArcs,
55-
IntegralFlowWithMultipliers, IsomorphicSpanningTree, KClique, KthBestSpanningTree,
56-
LengthBoundedDisjointPaths, LongestPath, MixedChinesePostman, SpinGlass, SteinerTree,
57-
StrongConnectivityAugmentation, SubgraphIsomorphism,
53+
DirectedTwoCommodityIntegralFlow, DisjointConnectingPaths, GeneralizedHex,
54+
GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles,
55+
IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, IsomorphicSpanningTree, KClique,
56+
KthBestSpanningTree, LengthBoundedDisjointPaths, LongestPath, MixedChinesePostman,
57+
SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism,
5858
};
5959
pub use crate::models::graph::{
6060
KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet,

0 commit comments

Comments
 (0)