diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index f8214db14..eba813fd8 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -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], @@ -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$.], + ) + ] + ] +} #{ let x = load-model-example("GeneralizedHex") let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 2bf8a880d..004689c35 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -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}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index fa5ed7f60..4fe588faa 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -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 @@ -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, + /// Terminal pairs for DisjointConnectingPaths (comma-separated pairs, e.g., "0-3,2-5") + #[arg(long = "terminal-pairs")] + pub terminal_pairs: Option, /// Tree edge list for IsomorphicSpanningTree (e.g., 0-1,1-2,2-3) #[arg(long)] pub tree: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 4abc75010..005fbc713 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -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, @@ -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() @@ -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\"" } @@ -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\"" } @@ -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"; @@ -4487,6 +4505,38 @@ fn parse_terminals(args: &CreateArgs, num_vertices: usize) -> Result> Ok(terminals) } +/// Parse `--terminal-pairs` as comma-separated `u-v` vertex pairs. +fn parse_terminal_pairs(args: &CreateArgs, num_vertices: usize) -> Result> { + 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)"); @@ -6736,6 +6786,7 @@ mod tests { release_times: None, lengths: None, terminals: None, + terminal_pairs: None, tree: None, required_edges: None, bound: None, @@ -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 = + 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(); diff --git a/src/lib.rs b/src/lib.rs index 96c852059..458193d8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, diff --git a/src/models/graph/disjoint_connecting_paths.rs b/src/models/graph/disjoint_connecting_paths.rs new file mode 100644 index 000000000..2f6bcebd6 --- /dev/null +++ b/src/models/graph/disjoint_connecting_paths.rs @@ -0,0 +1,269 @@ +//! Disjoint Connecting Paths problem implementation. +//! +//! The problem asks whether an undirected graph contains pairwise +//! vertex-disjoint paths connecting a prescribed collection of terminal pairs. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use crate::variant::VariantParam; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; + +inventory::submit! { + ProblemSchemaEntry { + name: "DisjointConnectingPaths", + display_name: "Disjoint Connecting Paths", + aliases: &[], + dimensions: &[ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + ], + module_path: module_path!(), + description: "Find pairwise vertex-disjoint paths connecting given terminal pairs", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, + FieldInfo { name: "terminal_pairs", type_name: "Vec<(usize, usize)>", description: "Disjoint terminal pairs (s_i, t_i)" }, + ], + } +} + +/// Disjoint Connecting Paths on an undirected graph. +/// +/// A configuration uses one binary variable per edge in the graph's canonical +/// sorted edge list. A valid solution selects exactly the edges of one simple +/// path for each terminal pair, with all such paths pairwise vertex-disjoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] +pub struct DisjointConnectingPaths { + graph: G, + terminal_pairs: Vec<(usize, usize)>, +} + +impl DisjointConnectingPaths { + /// Create a new Disjoint Connecting Paths instance. + /// + /// # Panics + /// + /// Panics if no terminal pairs are provided, if a pair uses invalid or + /// repeated endpoints, or if any terminal appears in more than one pair. + pub fn new(graph: G, terminal_pairs: Vec<(usize, usize)>) -> Self { + assert!( + !terminal_pairs.is_empty(), + "terminal_pairs must contain at least one pair" + ); + + let num_vertices = graph.num_vertices(); + let mut used = vec![false; num_vertices]; + for &(source, sink) in &terminal_pairs { + assert!(source < num_vertices, "terminal pair source out of bounds"); + assert!(sink < num_vertices, "terminal pair sink out of bounds"); + assert_ne!(source, sink, "terminal pair endpoints must be distinct"); + assert!( + !used[source], + "terminal vertices must be pairwise disjoint across pairs" + ); + assert!( + !used[sink], + "terminal vertices must be pairwise disjoint across pairs" + ); + used[source] = true; + used[sink] = true; + } + + Self { + graph, + terminal_pairs, + } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get the terminal pairs. + pub fn terminal_pairs(&self) -> &[(usize, usize)] { + &self.terminal_pairs + } + + /// Get the number of vertices in the graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges in the graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Get the number of terminal pairs. + pub fn num_pairs(&self) -> usize { + self.terminal_pairs.len() + } + + /// Return the canonical lexicographically sorted undirected edge list. + pub fn ordered_edges(&self) -> Vec<(usize, usize)> { + canonical_edges(&self.graph) + } + + /// Check whether a configuration is a valid solution. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + is_valid_disjoint_connecting_paths(&self.graph, &self.terminal_pairs, config) + } +} + +impl Problem for DisjointConnectingPaths +where + G: Graph + VariantParam, +{ + const NAME: &'static str = "DisjointConnectingPaths"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G] + } + + fn dims(&self) -> Vec { + vec![2; self.num_edges()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.is_valid_solution(config) + } +} + +impl SatisfactionProblem for DisjointConnectingPaths {} + +fn canonical_edges(graph: &G) -> Vec<(usize, usize)> { + let mut edges = graph + .edges() + .into_iter() + .map(|(u, v)| if u <= v { (u, v) } else { (v, u) }) + .collect::>(); + edges.sort_unstable(); + edges +} + +fn normalize_edge(u: usize, v: usize) -> (usize, usize) { + if u <= v { + (u, v) + } else { + (v, u) + } +} + +fn is_valid_disjoint_connecting_paths( + graph: &G, + terminal_pairs: &[(usize, usize)], + config: &[usize], +) -> bool { + let edges = canonical_edges(graph); + if config.len() != edges.len() { + return false; + } + if config.iter().any(|&value| value > 1) { + return false; + } + + let num_vertices = graph.num_vertices(); + let mut adjacency = vec![Vec::new(); num_vertices]; + let mut degrees = vec![0usize; num_vertices]; + for (index, &chosen) in config.iter().enumerate() { + if chosen == 1 { + let (u, v) = edges[index]; + adjacency[u].push(v); + adjacency[v].push(u); + degrees[u] += 1; + degrees[v] += 1; + } + } + + let mut terminal_vertices = vec![false; num_vertices]; + let required_pairs = terminal_pairs + .iter() + .map(|&(u, v)| { + terminal_vertices[u] = true; + terminal_vertices[v] = true; + normalize_edge(u, v) + }) + .collect::>(); + let mut matched_pairs = BTreeSet::new(); + let mut visited = vec![false; num_vertices]; + let mut component_count = 0usize; + + for start in 0..num_vertices { + if degrees[start] == 0 || visited[start] { + continue; + } + + component_count += 1; + let mut stack = vec![start]; + let mut vertices = Vec::new(); + let mut degree_sum = 0usize; + visited[start] = true; + + while let Some(vertex) = stack.pop() { + vertices.push(vertex); + degree_sum += degrees[vertex]; + for &neighbor in &adjacency[vertex] { + if !visited[neighbor] { + visited[neighbor] = true; + stack.push(neighbor); + } + } + } + + let edge_count = degree_sum / 2; + if edge_count + 1 != vertices.len() { + return false; + } + + let mut endpoints = Vec::new(); + for &vertex in &vertices { + match degrees[vertex] { + 1 => endpoints.push(vertex), + 2 => { + if terminal_vertices[vertex] { + return false; + } + } + _ => return false, + } + } + + if endpoints.len() != 2 { + return false; + } + + let realized_pair = normalize_edge(endpoints[0], endpoints[1]); + if !required_pairs.contains(&realized_pair) || !matched_pairs.insert(realized_pair) { + return false; + } + } + + component_count == terminal_pairs.len() && matched_pairs.len() == terminal_pairs.len() +} + +crate::declare_variants! { + default sat DisjointConnectingPaths => "2^num_edges", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "disjoint_connecting_paths_simplegraph", + instance: Box::new(DisjointConnectingPaths::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 3), (0, 2), (1, 4), (2, 4), (3, 5), (4, 5)], + ), + vec![(0, 3), (2, 5)], + )), + optimal_config: vec![1, 0, 1, 0, 1, 0, 1], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/disjoint_connecting_paths.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index def6ecda2..f0270ae9a 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -50,6 +50,7 @@ //! - [`UndirectedFlowLowerBounds`]: Feasible s-t flow in an undirected graph with lower/upper bounds //! - [`UndirectedTwoCommodityIntegralFlow`]: Two-commodity integral flow on undirected graphs //! - [`StrongConnectivityAugmentation`]: Strong connectivity augmentation with weighted candidate arcs +//! - [`DisjointConnectingPaths`]: Vertex-disjoint paths connecting prescribed terminal pairs pub(crate) mod acyclic_partition; pub(crate) mod balanced_complete_bipartite_subgraph; @@ -58,6 +59,7 @@ pub(crate) mod biconnectivity_augmentation; pub(crate) mod bottleneck_traveling_salesman; pub(crate) mod bounded_component_spanning_forest; pub(crate) mod directed_two_commodity_integral_flow; +pub(crate) mod disjoint_connecting_paths; pub(crate) mod generalized_hex; pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_circuit; @@ -110,6 +112,7 @@ pub use biconnectivity_augmentation::BiconnectivityAugmentation; pub use bottleneck_traveling_salesman::BottleneckTravelingSalesman; pub use bounded_component_spanning_forest::BoundedComponentSpanningForest; pub use directed_two_commodity_integral_flow::DirectedTwoCommodityIntegralFlow; +pub use disjoint_connecting_paths::DisjointConnectingPaths; pub use generalized_hex::GeneralizedHex; pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_circuit::HamiltonianCircuit; @@ -199,6 +202,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec DisjointConnectingPaths { + DisjointConnectingPaths::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 3), (0, 2), (1, 4), (2, 4), (3, 5), (4, 5)], + ), + vec![(0, 3), (2, 5)], + ) +} + +fn issue_yes_config() -> Vec { + vec![1, 0, 1, 0, 1, 0, 1] +} + +fn issue_no_problem() -> DisjointConnectingPaths { + DisjointConnectingPaths::new( + SimpleGraph::new(6, vec![(0, 2), (1, 2), (2, 3), (3, 4), (3, 5)]), + vec![(0, 4), (1, 5)], + ) +} + +#[test] +fn test_disjoint_connecting_paths_creation() { + let problem = issue_yes_problem(); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_edges(), 7); + assert_eq!(problem.num_pairs(), 2); + assert_eq!(problem.terminal_pairs(), &[(0, 3), (2, 5)]); + assert_eq!(problem.dims(), vec![2; 7]); + assert_eq!( + problem.ordered_edges(), + vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (3, 5), (4, 5)] + ); +} + +#[test] +#[should_panic(expected = "terminal_pairs must contain at least one pair")] +fn test_disjoint_connecting_paths_rejects_empty_pairs() { + let _ = DisjointConnectingPaths::new(SimpleGraph::new(2, vec![(0, 1)]), vec![]); +} + +#[test] +#[should_panic(expected = "terminal vertices must be pairwise disjoint across pairs")] +fn test_disjoint_connecting_paths_rejects_overlapping_terminals() { + let _ = DisjointConnectingPaths::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![(0, 2), (2, 3)], + ); +} + +#[test] +fn test_disjoint_connecting_paths_yes_instance() { + let problem = issue_yes_problem(); + assert!(problem.evaluate(&issue_yes_config())); +} + +#[test] +fn test_disjoint_connecting_paths_no_instance() { + let problem = issue_no_problem(); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_disjoint_connecting_paths_rejects_wrong_length_config() { + let problem = issue_yes_problem(); + assert!(!problem.evaluate(&[1, 0, 1])); +} + +#[test] +fn test_disjoint_connecting_paths_rejects_non_binary_entries() { + let problem = issue_yes_problem(); + let mut config = issue_yes_config(); + config[3] = 2; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_disjoint_connecting_paths_rejects_branching_subgraph() { + let problem = issue_yes_problem(); + assert!(!problem.evaluate(&[1, 0, 1, 1, 1, 0, 1])); +} + +#[test] +fn test_disjoint_connecting_paths_serialization() { + let problem = issue_yes_problem(); + let json = serde_json::to_value(&problem).unwrap(); + let round_trip: DisjointConnectingPaths = serde_json::from_value(json).unwrap(); + assert_eq!(round_trip.num_vertices(), 6); + assert_eq!(round_trip.num_edges(), 7); + assert_eq!(round_trip.terminal_pairs(), &[(0, 3), (2, 5)]); +} + +#[test] +fn test_disjoint_connecting_paths_paper_example() { + let problem = issue_yes_problem(); + let config = issue_yes_config(); + assert!(problem.evaluate(&config)); + + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); +}