diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 1ae122994..6e9e51401 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], + "IntegralFlowWithMultipliers": [Integral Flow With Multipliers], "MinMaxMulticenter": [Min-Max Multicenter], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], @@ -5251,6 +5252,71 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ) ] +#{ + let x = load-model-example("IntegralFlowWithMultipliers") + let config = x.optimal_config + [ + #problem-def("IntegralFlowWithMultipliers")[ + Given a directed graph $G = (V, A)$, distinguished vertices $s, t in V$, arc capacities $c: A -> ZZ^+$, vertex multipliers $h: V backslash {s, t} -> ZZ^+$, and a requirement $R in ZZ^+$, determine whether there exists an integral flow function $f: A -> ZZ_(>= 0)$ such that (1) $f(a) <= c(a)$ for every $a in A$, (2) for each nonterminal vertex $v in V backslash {s, t}$, the value $h(v)$ times the total inflow into $v$ equals the total outflow from $v$, and (3) the net flow into $t$ is at least $R$. + ][ + Integral Flow With Multipliers is Garey and Johnson's gain/loss network problem ND33 @garey1979. Sahni includes the same integral vertex-multiplier formulation among his computationally related problems, where partition-style reductions show that adding discrete gain factors destroys the ordinary max-flow structure @sahni1974. The key wrinkle is that conservation is no longer symmetric: one unit entering a vertex may force several units to leave, so the feasible integral solutions behave more like multiplicative gadgets than classical flow balances. + + When every multiplier equals $1$, the model collapses to ordinary single-commodity max flow and becomes polynomial-time solvable by the standard network-flow machinery summarized in Garey and Johnson @garey1979. Jewell studies a different continuous flow-with-gains model in which gain factors live on arcs and the flow may be fractional @jewell1962. That continuous relaxation remains polynomially tractable, so it should not be conflated with the NP-complete integral vertex-multiplier decision problem catalogued here. In this implementation the witness stores one bounded integer variable per arc, giving the direct exact-search bound $O((C + 1)^m)$ where $m = |A|$ and $C = max_(a in A) c(a)$. + + *Example.* The canonical fixture encodes the Partition multiset ${2, 3, 4, 5, 6, 4}$ using source $s = v_0$, sink $t = v_7$, six unit-capacity arcs out of $s$, six sink arcs with capacities $(2, 3, 4, 5, 6, 4)$, and multipliers $(2, 3, 4, 5, 6, 4)$ on the intermediate vertices. Setting the source arcs to $v_1$, $v_3$, and $v_5$ to $1$ forces outgoing sink arcs of $2$, $4$, and $6$, respectively. The sink therefore receives net inflow $2 + 4 + 6 = 12$, exactly meeting the requirement $R = 12$. + + #pred-commands( + "pred create --example IntegralFlowWithMultipliers -o integral-flow-with-multipliers.json", + "pred solve integral-flow-with-multipliers.json --solver brute-force", + "pred evaluate integral-flow-with-multipliers.json --config " + config.map(str).join(","), + ) + + #figure( + canvas(length: 0.9cm, { + import draw: * + let blue = graph-colors.at(0) + let gray = luma(180) + let source = (0, 0) + let sink = (6, 0) + let mids = ( + (2.4, 2.5), + (2.4, 1.5), + (2.4, 0.5), + (2.4, -0.5), + (2.4, -1.5), + (2.4, -2.5), + ) + let labels = ( + [$v_1, h = 2$], + [$v_2, h = 3$], + [$v_3, h = 4$], + [$v_4, h = 5$], + [$v_5, h = 6$], + [$v_6, h = 4$], + ) + let active = (0, 2, 4) + + for (i, pos) in mids.enumerate() { + let chosen = active.contains(i) + let color = if chosen { blue } else { gray } + let thickness = if chosen { 1.3pt } else { 0.6pt } + line(source, pos, stroke: (paint: color, thickness: thickness), mark: (end: "straight", scale: 0.45)) + line(pos, sink, stroke: (paint: color, thickness: thickness), mark: (end: "straight", scale: 0.45)) + circle(pos, radius: 0.22, fill: if chosen { blue.lighten(75%) } else { white }, stroke: 0.6pt) + content((pos.at(0) + 0.85, pos.at(1)), text(6.5pt, labels.at(i))) + } + + circle(source, radius: 0.24, fill: blue.lighten(75%), stroke: 0.6pt) + circle(sink, radius: 0.24, fill: blue.lighten(75%), stroke: 0.6pt) + content(source, text(7pt, [$s = v_0$])) + content(sink, text(7pt, [$t = v_7$])) + }), + caption: [Integral Flow With Multipliers: the blue branches send one unit from $s$ into $v_1$, $v_3$, and $v_5$, forcing sink inflow $2 + 4 + 6 = 12$ at $t$.], + ) + ] + ] +} + #problem-def("AdditionalKey")[ Given a set $A$ of attribute names, a collection $F$ of functional dependencies on $A$, a subset $R subset.eq A$, and a set $K$ of candidate keys for the relational scheme $chevron.l R, F chevron.r$, diff --git a/docs/paper/references.bib b/docs/paper/references.bib index f24f1a092..0d9948cfa 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -255,6 +255,27 @@ @article{evenItaiShamir1976 doi = {10.1137/0205048} } +@article{sahni1974, + author = {Sartaj Sahni}, + title = {Computationally Related Problems}, + journal = {SIAM Journal on Computing}, + volume = {3}, + number = {4}, + pages = {262--279}, + year = {1974} +} + +@article{jewell1962, + author = {William S. Jewell}, + title = {Optimal Flow Through Networks with Gains}, + journal = {Operations Research}, + volume = {10}, + number = {4}, + pages = {476--499}, + year = {1962}, + doi = {10.1287/opre.10.4.476} +} + @article{abdelWahabKameda1978, author = {H. M. Abdel-Wahab and T. Kameda}, title = {Scheduling to Minimize Maximum Cumulative Cost Subject to Series-Parallel Precedence Constraints}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 02df552d7..54b6b382f 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -229,6 +229,7 @@ Flags by problem type: PartitionIntoTriangles --graph GraphPartitioning --graph GeneralizedHex --graph, --source, --sink + IntegralFlowWithMultipliers --arcs, --capacities, --source, --sink, --multipliers, --requirement MinimumCutIntoBoundedSets --graph, --edge-weights, --source, --sink, --size-bound, --cut-bound HamiltonianCircuit, HC --graph LongestCircuit --graph, --edge-weights, --bound @@ -313,6 +314,7 @@ Examples: pred create SAT --num-vars 3 --clauses \"1,2;-1,3\" pred create QUBO --matrix \"1,0.5;0.5,2\" pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5 + 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 pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10 pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\" @@ -356,12 +358,18 @@ pub struct CreateArgs { /// Edge capacities for multicommodity flow problems (e.g., 1,1,2) #[arg(long)] pub capacities: Option, + /// Vertex multipliers in vertex order (e.g., 1,2,3,1) + #[arg(long)] + pub multipliers: Option, /// Source vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub source: Option, /// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub sink: Option, + /// Required sink inflow for IntegralFlowWithMultipliers + #[arg(long)] + pub requirement: Option, /// Required number of paths for LengthBoundedDisjointPaths #[arg(long)] pub num_paths_required: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index d03de0341..35b77fec4 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -50,8 +50,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.edge_weights.is_none() && args.edge_lengths.is_none() && args.capacities.is_none() + && args.multipliers.is_none() && args.source.is_none() && args.sink.is_none() + && args.requirement.is_none() && args.num_paths_required.is_none() && args.couplings.is_none() && args.fields.is_none() @@ -513,6 +515,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "KClique" => "--graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3", "GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3", "GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5", + "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" + } "MinimumCutIntoBoundedSets" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --source 0 --sink 3 --size-bound 3 --cut-bound 1" } @@ -1116,6 +1121,95 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // 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"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers 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_str = args.capacities.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --capacities\n\n{usage}") + })?; + let capacities: Vec = util::parse_comma_list(capacities_str) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + if capacities.len() != num_arcs { + bail!( + "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(); + if !fits { + bail!( + "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!("IntegralFlowWithMultipliers requires --source\n\n{usage}") + })?; + let sink = args.sink.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --sink\n\n{usage}") + })?; + validate_vertex_index("source", source, num_vertices, usage)?; + validate_vertex_index("sink", sink, num_vertices, usage)?; + if source == sink { + bail!( + "IntegralFlowWithMultipliers requires distinct --source and --sink\n\n{}", + usage + ); + } + + let multipliers_str = args.multipliers.as_deref().ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --multipliers\n\n{usage}") + })?; + let multipliers: Vec = util::parse_comma_list(multipliers_str) + .map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + if multipliers.len() != num_vertices { + bail!( + "Expected {} multipliers but got {}\n\n{}", + num_vertices, + multipliers.len(), + usage + ); + } + if multipliers + .iter() + .enumerate() + .any(|(vertex, &multiplier)| vertex != source && vertex != sink && multiplier == 0) + { + bail!("non-terminal multipliers must be positive\n\n{usage}"); + } + + let requirement = args.requirement.ok_or_else(|| { + anyhow::anyhow!("IntegralFlowWithMultipliers requires --requirement\n\n{usage}") + })?; + ( + ser(IntegralFlowWithMultipliers::new( + graph, + source, + sink, + multipliers, + capacities, + requirement, + ))?, + resolved_variant.clone(), + ) + } + // Minimum cut into bounded sets (graph + edge weights + s/t/B/K) "MinimumCutIntoBoundedSets" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -5871,8 +5965,10 @@ mod tests { edge_weights: None, edge_lengths: None, capacities: None, + multipliers: None, source: None, sink: None, + requirement: None, num_paths_required: None, couplings: None, fields: None, diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 5c38df249..b736a5b18 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -656,6 +656,155 @@ 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_with_multipliers() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,0>3,0>4,0>5,0>6,1>7,2>7,3>7,4>7,5>7,6>7", + "--capacities", + "1,1,1,1,1,1,2,3,4,5,6,4", + "--source", + "0", + "--sink", + "7", + "--multipliers", + "1,2,3,4,5,6,4,1", + "--requirement", + "12", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "IntegralFlowWithMultipliers"); + assert_eq!(json["variant"], serde_json::json!({})); + assert_eq!(json["data"]["source"], 0); + assert_eq!(json["data"]["sink"], 7); + assert_eq!(json["data"]["requirement"], 12); + assert_eq!( + json["data"]["multipliers"], + serde_json::json!([1, 2, 3, 4, 5, 6, 4, 1]) + ); + assert_eq!( + json["data"]["capacities"], + serde_json::json!([1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4]) + ); +} + +#[test] +fn test_create_integral_flow_with_multipliers_missing_multipliers_shows_usage() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,1>3,2>3", + "--capacities", + "1,1,2,2", + "--source", + "0", + "--sink", + "3", + "--requirement", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("requires --multipliers")); + assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); +} + +#[test] +fn test_create_integral_flow_with_multipliers_rejects_wrong_multiplier_count() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,1>3,2>3", + "--capacities", + "1,1,2,2", + "--source", + "0", + "--sink", + "3", + "--multipliers", + "1,2,3", + "--requirement", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Expected 4 multipliers but got 3")); + assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); +} + +#[test] +fn test_create_integral_flow_with_multipliers_rejects_zero_nonterminal_multiplier() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,1>3,2>3", + "--capacities", + "1,1,2,2", + "--source", + "0", + "--sink", + "3", + "--multipliers", + "1,0,3,1", + "--requirement", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("non-terminal multipliers must be positive")); + assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); +} + +#[test] +fn test_create_integral_flow_with_multipliers_rejects_identical_source_and_sink() { + let output = pred() + .args([ + "create", + "IntegralFlowWithMultipliers", + "--arcs", + "0>1,0>2,1>3,2>3", + "--capacities", + "1,1,2,2", + "--source", + "0", + "--sink", + "0", + "--multipliers", + "1,2,3,1", + "--requirement", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("requires distinct --source and --sink")); + assert!(stderr.contains("Usage: pred create IntegralFlowWithMultipliers")); +} + #[test] fn test_create_consecutive_block_minimization_rejects_ragged_matrix() { let output = pred() @@ -5716,6 +5865,58 @@ fn test_inspect_undirected_two_commodity_integral_flow_reports_size_fields() { std::fs::remove_file(&result_file).ok(); } +#[test] +fn test_inspect_integral_flow_with_multipliers_reports_size_fields() { + let problem_file = std::env::temp_dir().join("pred_test_ifwm_inspect_in.json"); + let result_file = std::env::temp_dir().join("pred_test_ifwm_inspect_out.json"); + let create_out = pred() + .args([ + "-o", + problem_file.to_str().unwrap(), + "create", + "--example", + "IntegralFlowWithMultipliers", + ]) + .output() + .unwrap(); + assert!( + create_out.status.success(), + "stderr: {}", + String::from_utf8_lossy(&create_out.stderr) + ); + + let output = pred() + .args([ + "-o", + result_file.to_str().unwrap(), + "inspect", + problem_file.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let content = std::fs::read_to_string(&result_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + let size_fields: Vec<&str> = json["size_fields"] + .as_array() + .expect("size_fields should be an array") + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert!(size_fields.contains(&"num_vertices")); + assert!(size_fields.contains(&"num_arcs")); + assert!(size_fields.contains(&"max_capacity")); + assert!(size_fields.contains(&"requirement")); + + std::fs::remove_file(&problem_file).ok(); + std::fs::remove_file(&result_file).ok(); +} + #[test] fn test_inspect_acyclic_partition_reports_size_fields() { let problem_file = std::env::temp_dir().join("pred_test_acyclic_partition_inspect_in.json"); diff --git a/src/lib.rs b/src/lib.rs index f9e84dca0..50b9f4420 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,9 +51,9 @@ pub mod prelude { AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, BottleneckTravelingSalesman, BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GeneralizedHex, GraphPartitioning, HamiltonianCircuit, - HamiltonianPath, IsomorphicSpanningTree, KClique, KthBestSpanningTree, - LengthBoundedDisjointPaths, MixedChinesePostman, SpinGlass, SteinerTree, - StrongConnectivityAugmentation, SubgraphIsomorphism, + HamiltonianPath, 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_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs new file mode 100644 index 000000000..4f25d731e --- /dev/null +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -0,0 +1,257 @@ +//! Integral Flow With Multipliers problem implementation. +//! +//! Given a directed graph with arc capacities, vertex multipliers on +//! non-terminals, and a sink demand, determine whether there exists an +//! integral flow satisfying multiplier-scaled conservation. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; +use crate::topology::DirectedGraph; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "IntegralFlowWithMultipliers", + display_name: "Integral Flow With Multipliers", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Integral flow feasibility on a directed graph with multiplier-scaled conservation at non-terminal vertices", + fields: &[ + FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed graph G = (V, A)" }, + FieldInfo { name: "source", type_name: "usize", description: "Source vertex s" }, + FieldInfo { name: "sink", type_name: "usize", description: "Sink vertex t" }, + FieldInfo { name: "multipliers", type_name: "Vec", description: "Vertex multipliers h(v) in vertex order; source/sink entries are ignored" }, + FieldInfo { name: "capacities", type_name: "Vec", description: "Arc capacities c(a) in graph arc order" }, + FieldInfo { name: "requirement", type_name: "u64", description: "Required net inflow R at the sink" }, + ], + } +} + +inventory::submit! { + ProblemSizeFieldEntry { + name: "IntegralFlowWithMultipliers", + fields: &["num_vertices", "num_arcs", "max_capacity", "requirement"], + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IntegralFlowWithMultipliers { + graph: DirectedGraph, + source: usize, + sink: usize, + multipliers: Vec, + capacities: Vec, + requirement: u64, +} + +impl IntegralFlowWithMultipliers { + pub fn new( + graph: DirectedGraph, + source: usize, + sink: usize, + multipliers: Vec, + capacities: Vec, + requirement: u64, + ) -> Self { + assert_eq!( + capacities.len(), + graph.num_arcs(), + "capacities length must match graph num_arcs" + ); + assert_eq!( + multipliers.len(), + graph.num_vertices(), + "multipliers length must match graph num_vertices" + ); + + let num_vertices = graph.num_vertices(); + 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})" + ); + assert_ne!(source, sink, "source and sink must be distinct"); + + for (vertex, &multiplier) in multipliers.iter().enumerate() { + if vertex != source && vertex != sink { + assert!(multiplier > 0, "non-terminal multipliers must be positive"); + } + } + + for &capacity in &capacities { + let domain = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)); + assert!( + domain.is_some(), + "arc capacities must fit into usize for dims()" + ); + } + + Self { + graph, + source, + sink, + multipliers, + capacities, + requirement, + } + } + + pub fn graph(&self) -> &DirectedGraph { + &self.graph + } + + pub fn source(&self) -> usize { + self.source + } + + pub fn sink(&self) -> usize { + self.sink + } + + pub fn multipliers(&self) -> &[u64] { + &self.multipliers + } + + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + pub fn requirement(&self) -> u64 { + self.requirement + } + + 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) + } + + 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") + } + + pub fn is_feasible(&self, config: &[usize]) -> bool { + if config.len() != self.num_arcs() { + return false; + } + + let num_vertices = self.num_vertices(); + let mut inflow = vec![0_i128; num_vertices]; + let mut outflow = vec![0_i128; num_vertices]; + + for (arc_index, ((u, v), &capacity)) in self + .graph + .arcs() + .into_iter() + .zip(self.capacities.iter()) + .enumerate() + { + let Some(flow_usize) = config.get(arc_index).copied() else { + return false; + }; + let Ok(flow_u64) = u64::try_from(flow_usize) else { + return false; + }; + if flow_u64 > capacity { + return false; + } + let flow = i128::from(flow_u64); + outflow[u] += flow; + inflow[v] += flow; + } + + for vertex in 0..num_vertices { + if vertex == self.source || vertex == self.sink { + continue; + } + let multiplier = i128::from(self.multipliers[vertex]); + let Some(expected_outflow) = inflow[vertex].checked_mul(multiplier) else { + return false; + }; + if expected_outflow != outflow[vertex] { + return false; + } + } + + let sink_net_flow = inflow[self.sink] - outflow[self.sink]; + sink_net_flow >= i128::from(self.requirement) + } +} + +impl Problem for IntegralFlowWithMultipliers { + const NAME: &'static str = "IntegralFlowWithMultipliers"; + type Metric = bool; + + fn dims(&self) -> Vec { + self.capacities + .iter() + .map(|&capacity| Self::domain_size(capacity)) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.is_feasible(config) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for IntegralFlowWithMultipliers {} + +crate::declare_variants! { + default sat IntegralFlowWithMultipliers => "(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_with_multipliers", + instance: Box::new(IntegralFlowWithMultipliers::new( + DirectedGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (0, 6), + (1, 7), + (2, 7), + (3, 7), + (4, 7), + (5, 7), + (6, 7), + ], + ), + 0, + 7, + vec![1, 2, 3, 4, 5, 6, 4, 1], + vec![1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4], + 12, + )), + optimal_config: vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/integral_flow_with_multipliers.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 06627fb70..96373cade 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) +//! - [`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 @@ -56,6 +57,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_with_multipliers; pub(crate) mod isomorphic_spanning_tree; pub(crate) mod kclique; pub(crate) mod kcoloring; @@ -102,6 +104,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_with_multipliers::IntegralFlowWithMultipliers; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; pub use kclique::KClique; pub use kcoloring::KColoring; @@ -147,6 +150,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec IntegralFlowWithMultipliers { + let graph = DirectedGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (0, 6), + (1, 7), + (2, 7), + (3, 7), + (4, 7), + (5, 7), + (6, 7), + ], + ); + IntegralFlowWithMultipliers::new( + graph, + 0, + 7, + vec![1, 2, 3, 4, 5, 6, 4, 1], + vec![1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4], + 12, + ) +} + +fn yes_config() -> Vec { + vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0] +} + +fn no_instance() -> IntegralFlowWithMultipliers { + let graph = DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2)]); + IntegralFlowWithMultipliers::new(graph, 0, 3, vec![1, 2, 3, 1], vec![2, 1, 2, 5, 1], 7) +} + +#[test] +fn test_integral_flow_with_multipliers_creation_accessors_and_dimensions() { + let problem = yes_instance(); + assert_eq!(problem.graph().num_vertices(), 8); + assert_eq!(problem.num_arcs(), 12); + assert_eq!(problem.source(), 0); + assert_eq!(problem.sink(), 7); + assert_eq!(problem.requirement(), 12); + assert_eq!(problem.max_capacity(), 6); + assert_eq!(problem.multipliers(), &[1, 2, 3, 4, 5, 6, 4, 1]); + assert_eq!(problem.capacities(), &[1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4]); + assert_eq!(problem.dims(), vec![2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 5]); +} + +#[test] +fn test_integral_flow_with_multipliers_evaluate_yes_instance() { + assert!(yes_instance().evaluate(&yes_config())); +} + +#[test] +fn test_integral_flow_with_multipliers_evaluate_no_instance() { + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&no_instance()).is_none()); +} + +#[test] +fn test_integral_flow_with_multipliers_rejects_multiplier_conservation_violation() { + let config = vec![1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; + assert!(!yes_instance().evaluate(&config)); +} + +#[test] +fn test_integral_flow_with_multipliers_sink_requirement_is_at_least() { + let config = vec![0, 0, 1, 1, 1, 0, 0, 0, 4, 5, 6, 0]; + assert!(yes_instance().evaluate(&config)); +} + +#[test] +fn test_integral_flow_with_multipliers_rejects_wrong_config_length() { + let problem = yes_instance(); + assert!(!problem.evaluate(&[0; 11])); + assert!(!problem.evaluate(&[0; 13])); + assert!(!problem.evaluate(&[])); +} + +#[test] +fn test_integral_flow_with_multipliers_serialization_round_trip() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let decoded: IntegralFlowWithMultipliers = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.source(), problem.source()); + assert_eq!(decoded.sink(), problem.sink()); + assert_eq!(decoded.requirement(), problem.requirement()); + assert_eq!(decoded.multipliers(), problem.multipliers()); + assert_eq!(decoded.capacities(), problem.capacities()); +} + +#[test] +fn test_integral_flow_with_multipliers_solver_yes_instance() { + let problem = yes_instance(); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem).unwrap(); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_integral_flow_with_multipliers_problem_name_and_size_fields() { + assert_eq!( + ::NAME, + "IntegralFlowWithMultipliers" + ); + let fields: HashSet<&'static str> = declared_size_fields("IntegralFlowWithMultipliers") + .into_iter() + .collect(); + assert_eq!( + fields, + HashSet::from(["max_capacity", "num_arcs", "num_vertices", "requirement"]) + ); +} + +#[cfg(feature = "example-db")] +#[test] +fn test_integral_flow_with_multipliers_canonical_example_spec() { + let specs = canonical_model_example_specs(); + assert_eq!(specs.len(), 1); + let spec = &specs[0]; + assert_eq!(spec.id, "integral_flow_with_multipliers"); + assert_eq!(spec.optimal_config, yes_config()); + assert_eq!(spec.optimal_value, serde_json::json!(true)); +} + +#[test] +fn test_integral_flow_with_multipliers_paper_example() { + let problem = yes_instance(); + let config = yes_config(); + let solver = BruteForce::new(); + + assert!(problem.evaluate(&config)); + assert_eq!([config[0], config[2], config[4]], [1, 1, 1]); + assert_eq!([config[6], config[8], config[10]], [2, 4, 6]); + assert_eq!(config[6] + config[8] + config[10], 12); + + let all_solutions = solver.find_all_satisfying(&problem); + assert!(all_solutions.iter().any(|solution| solution == &config)); +}