diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index f7b6c964..94743514 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -131,6 +131,7 @@ "ConsecutiveBlockMinimization": [Consecutive Block Minimization], "ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], + "IntegralFlowHomologousArcs": [Integral Flow with Homologous Arcs], "IntegralFlowWithMultipliers": [Integral Flow With Multipliers], "MinMaxMulticenter": [Min-Max Multicenter], "FlowShopScheduling": [Flow Shop Scheduling], @@ -5205,6 +5206,89 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("IntegralFlowHomologousArcs") + let arcs = x.instance.graph.arcs + let sol = x.optimal_config + let source = x.instance.source + let sink = x.instance.sink + let requirement = x.instance.requirement + [ + #problem-def("IntegralFlowHomologousArcs")[ + Given a directed graph $G = (V, A)$ with source $s in V$, sink $t in V$, arc capacities $c: A -> ZZ^+$, requirement $R in ZZ^+$, and a set $H subset.eq A times A$ of homologous arc pairs, determine whether there exists an integral flow function $f: A -> ZZ_(>= 0)$ such that $f(a) <= c(a)$ for every $a in A$, flow is conserved at every vertex in $V backslash {s, t}$, $f(a) = f(a')$ for every $(a, a') in H$, and the net flow into $t$ is at least $R$. + ][ + Integral Flow with Homologous Arcs is the single-commodity equality-constrained flow problem listed as ND35 in Garey & Johnson @garey1979. Their catalog records the NP-completeness result attributed to Sahni and notes that the unit-capacity restriction remains hard, while the corresponding non-integral relaxation is polynomial-time equivalent to linear programming @garey1979. + + The implementation uses one integer variable per arc, so exhaustive search over the induced configuration space runs in $O((C + 1)^m)$ for $m = |A|$ and $C = max_(a in A) c(a)$#footnote[This is the exact search bound induced by the implemented per-arc domains $f(a) in {0, dots, c(a)}$. In the unit-capacity special case, it simplifies to $O(2^m)$.]. + + *Example.* The canonical fixture instance has source $s = v_#source$, sink $t = v_#sink$, unit capacities on all eight arcs, requirement $R = #requirement$, and homologous pairs $(a_2, a_5)$ and $(a_4, a_3)$. The stored satisfying configuration routes one unit along $0 -> 1 -> 3 -> 5$ and one unit along $0 -> 2 -> 4 -> 5$. Thus the paired arcs $(1,3)$ and $(2,4)$ both carry 1, while $(1,4)$ and $(2,3)$ both carry 0. Every nonterminal vertex has equal inflow and outflow, and the sink receives two units of flow, so the verifier returns true. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o integral-flow-homologous-arcs.json", + "pred solve integral-flow-homologous-arcs.json --solver brute-force", + "pred evaluate integral-flow-homologous-arcs.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure( + canvas(length: 1cm, { + import draw: * + let blue = graph-colors.at(0) + let orange = rgb("#f28e2b") + let red = rgb("#e15759") + let gray = luma(185) + let positions = ( + (0, 0), + (1.6, 1.1), + (1.6, -1.1), + (3.2, 1.1), + (3.2, -1.1), + (4.8, 0), + ) + let labels = ( + [$s = v_0$], + [$v_1$], + [$v_2$], + [$v_3$], + [$v_4$], + [$t = v_5$], + ) + for (idx, (u, v)) in arcs.enumerate() { + let stroke = if idx == 3 or idx == 4 { + (paint: orange, thickness: 1.3pt, dash: "dashed") + } else if sol.at(idx) == 1 { + (paint: blue, thickness: 1.8pt) + } else { + (paint: gray, thickness: 0.7pt) + } + line( + positions.at(u), + positions.at(v), + stroke: stroke, + mark: (end: "straight", scale: 0.5), + ) + } + for (i, pos) in positions.enumerate() { + let fill = if i == source { blue } else if i == sink { red } else { white } + g-node( + pos, + name: "ifha-" + str(i), + fill: fill, + label: if i == source or i == sink { + text(fill: white)[#labels.at(i)] + } else { + labels.at(i) + }, + ) + } + content((2.4, 1.55), text(8pt, fill: blue)[$f(a_2) = f(a_5) = 1$]) + content((2.4, -1.55), text(8pt, fill: orange)[$f(a_4) = f(a_3) = 0$]) + }), + caption: [Canonical YES instance for Integral Flow with Homologous Arcs. Solid blue arcs carry the satisfying integral flow; dashed orange arcs form the second homologous pair, constrained to equal zero.], + ) + ] + ] +} + #problem-def("DirectedTwoCommodityIntegralFlow")[ Given a directed graph $G = (V, A)$ with arc capacities $c: A -> ZZ^+$, two source-sink pairs $(s_1, t_1)$ and $(s_2, t_2)$, and requirements $R_1, R_2 in ZZ^+$, determine whether there exist two integral flow functions $f_1, f_2: A -> ZZ_(>= 0)$ such that (1) $f_1(a) + f_2(a) <= c(a)$ for all $a in A$, (2) flow $f_i$ is conserved at every vertex except $s_1, s_2, t_1, t_2$, and (3) the net flow into $t_i$ under $f_i$ is at least $R_i$ for $i in {1, 2}$. ][ diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 54b6b382..4925bd3c 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -235,6 +235,7 @@ Flags by problem type: LongestCircuit --graph, --edge-weights, --bound BoundedComponentSpanningForest --graph, --weights, --k, --bound UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 + IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs IsomorphicSpanningTree --graph, --tree KthBestSpanningTree --graph, --edge-weights, --k, --bound LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound @@ -325,6 +326,7 @@ Examples: pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5 pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1 pred create 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 + pred create 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\" 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\" pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3 pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2 @@ -367,7 +369,7 @@ pub struct CreateArgs { /// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub sink: Option, - /// Required sink inflow for IntegralFlowWithMultipliers + /// Required sink inflow for IntegralFlowHomologousArcs and IntegralFlowWithMultipliers #[arg(long)] pub requirement: Option, /// Required number of paths for LengthBoundedDisjointPaths @@ -536,6 +538,9 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Arc-index equality constraints for IntegralFlowHomologousArcs (semicolon-separated, e.g., "2=5;4=3") + #[arg(long)] + pub homologous_pairs: Option, /// Quantifiers for QBF (comma-separated, E=Exists, A=ForAll, e.g., "E,A,E") #[arg(long)] pub quantifiers: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 35b77fec..6277af66 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -109,6 +109,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.costs.is_none() && args.arc_costs.is_none() && args.arcs.is_none() + && args.homologous_pairs.is_none() && args.quantifiers.is_none() && args.usage.is_none() && args.storage.is_none() @@ -151,6 +152,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.sink_2.is_none() && args.requirement_1.is_none() && args.requirement_2.is_none() + && args.requirement.is_none() + && args.homologous_pairs.is_none() && args.num_attributes.is_none() && args.dependencies.is_none() && args.relation_attrs.is_none() @@ -528,6 +531,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" }, + "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\"" + } "LengthBoundedDisjointPaths" => { "--graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3" } @@ -750,6 +756,9 @@ fn help_flag_hint( } ("ShortestCommonSupersequence", "strings") => "symbol lists: \"0,1,2;1,2,0\"", ("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"", + ("IntegralFlowHomologousArcs", "homologous_pairs") => { + "semicolon-separated arc-index equalities: \"2=5;4=3\"" + } ("ConsistencyOfDatabaseFrequencyTables", "attribute_domains") => { "comma-separated domain sizes: 2,3,2" } @@ -3197,6 +3206,83 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // IntegralFlowHomologousArcs + "IntegralFlowHomologousArcs" => { + let usage = "Usage: pred create 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\""; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --arcs\n\n{usage}") + })?; + let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let capacities: Vec = if let Some(ref s) = args.capacities { + s.split(',') + .map(|token| { + let trimmed = token.trim(); + trimmed + .parse::() + .with_context(|| format!("Invalid capacity `{trimmed}`\n\n{usage}")) + }) + .collect::>>()? + } else { + vec![1; num_arcs] + }; + anyhow::ensure!( + capacities.len() == num_arcs, + "Expected {} capacities but got {}\n\n{}", + num_arcs, + capacities.len(), + usage + ); + for (arc_index, &capacity) in capacities.iter().enumerate() { + let fits = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(); + anyhow::ensure!( + fits, + "capacity {} at arc index {} is too large for this platform\n\n{}", + capacity, + arc_index, + usage + ); + } + let num_vertices = graph.num_vertices(); + let source = args.source.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --sink\n\n{usage}") + })?; + let requirement = args.requirement.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowHomologousArcs requires --requirement\n\n{usage}") + })?; + validate_vertex_index("source", source, num_vertices, usage)?; + validate_vertex_index("sink", sink, num_vertices, usage)?; + let homologous_pairs = + parse_homologous_pairs(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + for &(a, b) in &homologous_pairs { + anyhow::ensure!( + a < num_arcs && b < num_arcs, + "homologous pair ({}, {}) references arc >= num_arcs ({})\n\n{}", + a, + b, + num_arcs, + usage + ); + } + ( + ser(IntegralFlowHomologousArcs::new( + graph, + capacities, + source, + sink, + requirement, + homologous_pairs, + ))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackArcSet "MinimumFeedbackArcSet" => { let arcs_str = args.arcs.as_deref().ok_or_else(|| { @@ -4416,6 +4502,35 @@ fn parse_named_sets(sets_str: Option<&str>, flag: &str) -> Result .collect() } +fn parse_homologous_pairs(args: &CreateArgs) -> Result> { + let pairs = args.homologous_pairs.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "IntegralFlowHomologousArcs requires --homologous-pairs (e.g., \"2=5;4=3\")" + ) + })?; + + pairs + .split(';') + .filter(|entry| !entry.trim().is_empty()) + .map(|entry| { + let entry = entry.trim(); + let (left, right) = entry.split_once('=').ok_or_else(|| { + anyhow::anyhow!( + "Invalid homologous pair '{}': expected format u=v (e.g., 2=5)", + entry + ) + })?; + let left = left.trim().parse::().with_context(|| { + format!("Invalid homologous pair '{}': expected format u=v", entry) + })?; + let right = right.trim().parse::().with_context(|| { + format!("Invalid homologous pair '{}': expected format u=v", entry) + })?; + Ok((left, right)) + }) + .collect() +} + /// Parse a dependency string as semicolon-separated `lhs>rhs` pairs. /// E.g., "0,1>2,3;2,3>0,1" fn parse_deps(s: &str) -> Result, Vec)>> { @@ -6067,6 +6182,7 @@ mod tests { usage: None, storage: None, quantifiers: None, + homologous_pairs: None, } } @@ -6084,6 +6200,13 @@ mod tests { assert!(!all_data_flags_empty(&args)); } + #[test] + fn test_all_data_flags_empty_treats_homologous_pairs_as_input() { + let mut args = empty_args(); + args.homologous_pairs = Some("2=5;4=3".to_string()); + assert!(!all_data_flags_empty(&args)); + } + #[test] fn test_parse_potential_edges() { let mut args = empty_args(); @@ -6112,6 +6235,24 @@ mod tests { assert_eq!(parse_budget(&args).unwrap(), 7); } + #[test] + fn test_parse_homologous_pairs() { + let mut args = empty_args(); + args.homologous_pairs = Some("2=5;4=3".to_string()); + + assert_eq!(parse_homologous_pairs(&args).unwrap(), vec![(2, 5), (4, 3)]); + } + + #[test] + fn test_parse_homologous_pairs_rejects_invalid_token() { + let mut args = empty_args(); + args.homologous_pairs = Some("2-5".to_string()); + + let err = parse_homologous_pairs(&args).unwrap_err().to_string(); + + assert!(err.contains("u=v")); + } + #[test] fn test_parse_graph_respects_explicit_num_vertices() { let mut args = empty_args(); diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index b736a5b1..7cc95c1c 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -33,6 +33,18 @@ fn test_list_includes_undirected_two_commodity_integral_flow() { assert!(stdout.contains("UndirectedTwoCommodityIntegralFlow")); } +#[test] +fn test_list_includes_integral_flow_homologous_arcs() { + let output = pred().args(["list"]).output().unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("IntegralFlowHomologousArcs")); +} + #[test] fn test_solve_help_mentions_string_to_string_correction_bruteforce() { let output = pred().args(["solve", "--help"]).output().unwrap(); @@ -656,6 +668,97 @@ fn test_create_undirected_two_commodity_integral_flow_rejects_out_of_range_termi assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); } +#[test] +fn test_create_integral_flow_homologous_arcs() { + let output_file = + std::env::temp_dir().join("pred_test_create_integral_flow_homologous_arcs.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "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", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(output_file.exists()); + + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "IntegralFlowHomologousArcs"); + + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_integral_flow_homologous_arcs_requires_homologous_pairs() { + let output = pred() + .args([ + "create", + "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", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("requires --homologous-pairs")); + assert!(stderr.contains("Usage: pred create IntegralFlowHomologousArcs")); +} + +#[test] +fn test_create_integral_flow_homologous_arcs_rejects_invalid_pair_token() { + let output = pred() + .args([ + "create", + "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", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("u=v")); + assert!(stderr.contains("Usage: pred create IntegralFlowHomologousArcs")); +} + #[test] fn test_create_integral_flow_with_multipliers() { let output = pred() diff --git a/src/lib.rs b/src/lib.rs index 50b9f442..1b26c905 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,9 +51,10 @@ pub mod prelude { AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, BottleneckTravelingSalesman, BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GeneralizedHex, GraphPartitioning, HamiltonianCircuit, - HamiltonianPath, IntegralFlowWithMultipliers, IsomorphicSpanningTree, KClique, - KthBestSpanningTree, LengthBoundedDisjointPaths, MixedChinesePostman, SpinGlass, - SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, + HamiltonianPath, IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, + IsomorphicSpanningTree, KClique, KthBestSpanningTree, LengthBoundedDisjointPaths, + MixedChinesePostman, SpinGlass, SteinerTree, StrongConnectivityAugmentation, + SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs new file mode 100644 index 00000000..51ae2b0c --- /dev/null +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -0,0 +1,245 @@ +//! Integral Flow with Homologous Arcs problem implementation. +//! +//! Given a directed capacitated network with a source, sink, and pairs of arcs +//! that must carry equal flow, determine whether an integral flow meeting the +//! required sink inflow exists. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; +use crate::topology::DirectedGraph; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "IntegralFlowHomologousArcs", + display_name: "Integral Flow with Homologous Arcs", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Integral flow feasibility with arc-pair equality constraints", + fields: &[ + FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed graph G = (V, A)" }, + FieldInfo { name: "capacities", type_name: "Vec", description: "Capacity c(a) for each arc" }, + FieldInfo { name: "source", type_name: "usize", description: "Source vertex s" }, + FieldInfo { name: "sink", type_name: "usize", description: "Sink vertex t" }, + FieldInfo { name: "requirement", type_name: "u64", description: "Required net inflow R at the sink" }, + FieldInfo { name: "homologous_pairs", type_name: "Vec<(usize, usize)>", description: "Arc-index pairs (a, a') with f(a) = f(a')" }, + ], + } +} + +inventory::submit! { + ProblemSizeFieldEntry { + name: "IntegralFlowHomologousArcs", + fields: &["num_vertices", "num_arcs"], + } +} + +/// Integral flow with homologous arcs. +/// +/// A configuration stores one non-negative integer flow value for each arc in +/// the graph's arc order. The assignment is feasible when it respects arc +/// capacities, flow conservation at non-terminal vertices, every homologous-pair +/// equality constraint, and the required net inflow at the sink. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IntegralFlowHomologousArcs { + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + requirement: u64, + homologous_pairs: Vec<(usize, usize)>, +} + +impl IntegralFlowHomologousArcs { + pub fn new( + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + requirement: u64, + homologous_pairs: Vec<(usize, usize)>, + ) -> Self { + let num_vertices = graph.num_vertices(); + let num_arcs = graph.num_arcs(); + + assert_eq!( + capacities.len(), + num_arcs, + "capacities length must match graph.num_arcs()" + ); + assert!( + source < num_vertices, + "source ({source}) must be less than num_vertices ({num_vertices})" + ); + assert!( + sink < num_vertices, + "sink ({sink}) must be less than num_vertices ({num_vertices})" + ); + + for &(a, b) in &homologous_pairs { + assert!(a < num_arcs, "homologous arc index {a} out of range"); + assert!(b < num_arcs, "homologous arc index {b} out of range"); + } + + for &capacity in &capacities { + assert!( + usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(), + "capacities must fit into usize for dims()" + ); + } + + Self { + graph, + capacities, + source, + sink, + requirement, + homologous_pairs, + } + } + + pub fn graph(&self) -> &DirectedGraph { + &self.graph + } + + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + pub fn source(&self) -> usize { + self.source + } + + pub fn sink(&self) -> usize { + self.sink + } + + pub fn requirement(&self) -> u64 { + self.requirement + } + + pub fn homologous_pairs(&self) -> &[(usize, usize)] { + &self.homologous_pairs + } + + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + pub fn num_arcs(&self) -> usize { + self.graph.num_arcs() + } + + pub fn max_capacity(&self) -> u64 { + self.capacities.iter().copied().max().unwrap_or(0) + } + + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate(config) + } + + fn domain_size(capacity: u64) -> usize { + usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .expect("capacity already validated to fit into usize") + } +} + +impl Problem for IntegralFlowHomologousArcs { + const NAME: &'static str = "IntegralFlowHomologousArcs"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + self.capacities + .iter() + .map(|&capacity| Self::domain_size(capacity)) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_arcs() { + return false; + } + + for &(a, b) in &self.homologous_pairs { + if config[a] != config[b] { + return false; + } + } + + let mut balances = vec![0_i128; self.num_vertices()]; + for (arc_index, ((u, v), &capacity)) in self + .graph + .arcs() + .into_iter() + .zip(self.capacities.iter()) + .enumerate() + { + let Ok(flow) = u64::try_from(config[arc_index]) else { + return false; + }; + if flow > capacity { + return false; + } + let flow = i128::from(flow); + balances[u] -= flow; + balances[v] += flow; + } + + for (vertex, &balance) in balances.iter().enumerate() { + if vertex != self.source && vertex != self.sink && balance != 0 { + return false; + } + } + + balances[self.sink] >= i128::from(self.requirement) + } +} + +impl SatisfactionProblem for IntegralFlowHomologousArcs {} + +crate::declare_variants! { + default sat IntegralFlowHomologousArcs => "(max_capacity + 1)^num_arcs", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "integral_flow_homologous_arcs", + instance: Box::new(IntegralFlowHomologousArcs::new( + DirectedGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (1, 4), + (2, 4), + (3, 5), + (4, 5), + ], + ), + vec![1; 8], + 0, + 5, + 2, + vec![(2, 5), (4, 3)], + )), + optimal_config: vec![1, 1, 1, 0, 0, 1, 1, 1], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/integral_flow_homologous_arcs.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 96373cad..5a263558 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -42,6 +42,7 @@ //! - [`SteinerTree`]: Minimum-weight tree spanning all required terminals //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) //! - [`DirectedTwoCommodityIntegralFlow`]: Directed two-commodity integral flow (satisfaction) +//! - [`IntegralFlowHomologousArcs`]: Integral flow with arc-pair equality constraints //! - [`IntegralFlowWithMultipliers`]: Integral flow with vertex multipliers on a directed graph //! - [`UndirectedTwoCommodityIntegralFlow`]: Two-commodity integral flow on undirected graphs //! - [`StrongConnectivityAugmentation`]: Strong connectivity augmentation with weighted candidate arcs @@ -57,6 +58,7 @@ pub(crate) mod generalized_hex; pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_circuit; pub(crate) mod hamiltonian_path; +pub(crate) mod integral_flow_homologous_arcs; pub(crate) mod integral_flow_with_multipliers; pub(crate) mod isomorphic_spanning_tree; pub(crate) mod kclique; @@ -104,6 +106,7 @@ pub use generalized_hex::GeneralizedHex; pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_circuit::HamiltonianCircuit; pub use hamiltonian_path::HamiltonianPath; +pub use integral_flow_homologous_arcs::IntegralFlowHomologousArcs; pub use integral_flow_with_multipliers::IntegralFlowWithMultipliers; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; pub use kclique::KClique; @@ -185,6 +188,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec IntegralFlowHomologousArcs { + let graph = DirectedGraph::new( + 6, + vec![ + (0, 1), + (0, 2), + (1, 3), + (2, 3), + (1, 4), + (2, 4), + (3, 5), + (4, 5), + ], + ); + IntegralFlowHomologousArcs::new(graph, vec![1; 8], 0, 5, 2, vec![(2, 5), (4, 3)]) +} + +fn no_instance() -> IntegralFlowHomologousArcs { + let graph = DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]); + IntegralFlowHomologousArcs::new(graph, vec![1; 4], 0, 3, 1, vec![(0, 1)]) +} + +#[test] +fn test_integral_flow_homologous_arcs_creation() { + let problem = yes_instance(); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_arcs(), 8); + assert_eq!(problem.source(), 0); + assert_eq!(problem.sink(), 5); + assert_eq!(problem.requirement(), 2); + assert_eq!(problem.max_capacity(), 1); + assert_eq!(problem.homologous_pairs(), &[(2, 5), (4, 3)]); + assert_eq!(problem.dims(), vec![2; 8]); +} + +#[test] +fn test_integral_flow_homologous_arcs_evaluate_yes_instance() { + let problem = yes_instance(); + let config = vec![1, 1, 1, 0, 0, 1, 1, 1]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_integral_flow_homologous_arcs_evaluate_no_instance() { + let problem = no_instance(); + assert!(!problem.evaluate(&[0, 0, 0, 0])); +} + +#[test] +fn test_integral_flow_homologous_arcs_rejects_homologous_violation() { + let problem = yes_instance(); + let config = vec![1, 1, 1, 0, 0, 0, 1, 1]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_integral_flow_homologous_arcs_rejects_capacity_violation() { + let problem = yes_instance(); + let config = vec![2, 0, 0, 0, 0, 0, 0, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_integral_flow_homologous_arcs_rejects_conservation_violation() { + let problem = yes_instance(); + let config = vec![1, 0, 0, 0, 0, 0, 0, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_integral_flow_homologous_arcs_wrong_config_length_is_invalid() { + let problem = yes_instance(); + assert!(!problem.evaluate(&[0; 7])); + assert!(!problem.evaluate(&[0; 9])); +} + +#[test] +fn test_integral_flow_homologous_arcs_solver_yes() { + let problem = yes_instance(); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); +} + +#[test] +fn test_integral_flow_homologous_arcs_solver_no() { + let problem = no_instance(); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_integral_flow_homologous_arcs_serialization() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let restored: IntegralFlowHomologousArcs = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.num_vertices(), 6); + assert_eq!(restored.num_arcs(), 8); + assert_eq!(restored.requirement(), 2); + assert_eq!(restored.homologous_pairs(), &[(2, 5), (4, 3)]); +} + +#[test] +fn test_integral_flow_homologous_arcs_problem_name() { + assert_eq!( + ::NAME, + "IntegralFlowHomologousArcs" + ); +} + +#[test] +fn test_integral_flow_homologous_arcs_non_unit_capacity() { + // s=0 -> 1 -> 2=t, with capacities [3, 3], homologous pair (0,1) so both arcs carry + // equal flow. R=2 is satisfiable: f=[2,2]. + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = IntegralFlowHomologousArcs::new(graph, vec![3, 3], 0, 2, 2, vec![(0, 1)]); + assert_eq!(problem.dims(), vec![4, 4]); + assert_eq!(problem.max_capacity(), 3); + assert!(problem.evaluate(&[2, 2])); + assert!(problem.evaluate(&[3, 3])); + assert!(!problem.evaluate(&[2, 3])); // homologous violation + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert_eq!(solutions.len(), 2); // [2,2] and [3,3] +} + +#[test] +fn test_integral_flow_homologous_arcs_paper_example() { + let problem = yes_instance(); + let solver = BruteForce::new(); + let config = vec![1, 1, 1, 0, 0, 1, 1, 1]; + + assert!(problem.evaluate(&config)); + + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + assert!(solutions.iter().all(|solution| problem.evaluate(solution))); +}