diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 707822b0..76cddb44 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -108,6 +108,7 @@ "PartitionIntoTriangles": [Partition Into Triangles], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], + "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], ) // Definition label: "def:" — each definition block must have a matching label @@ -2091,6 +2092,53 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#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}$. +][ + Directed Two-Commodity Integral Flow is a fundamental NP-complete problem in multicommodity flow theory, catalogued as ND38 in Garey & Johnson @garey1979. While single-commodity max-flow is solvable in polynomial time and fractional multicommodity flow reduces to linear programming, requiring integral flows with just two commodities makes the problem NP-complete. + + NP-completeness was proved by Even, Itai, and Shamir via reduction from 3-SAT @even1976. The problem remains NP-complete even when all arc capacities are 1 and $R_1 = 1$. No sub-exponential exact algorithm is known; brute-force enumeration over $(C + 1)^(2|A|)$ flow assignments dominates, where $C = max_(a in A) c(a)$.#footnote[No algorithm improving on brute-force is known for Directed Two-Commodity Integral Flow.] + + *Example.* Consider a directed graph with 6 vertices and 8 arcs (all with unit capacity), sources $s_1 = 0$, $s_2 = 1$, sinks $t_1 = 4$, $t_2 = 5$, and requirements $R_1 = R_2 = 1$. Commodity 1 routes along the path $0 -> 2 -> 4$ and commodity 2 along $1 -> 3 -> 5$, satisfying all capacity and conservation constraints. + + #figure( + canvas(length: 1cm, { + import draw: * + let positions = ( + (0, 1), // 0 = s1 + (0, -1), // 1 = s2 + (2, 1), // 2 + (2, -1), // 3 + (4, 1), // 4 = t1 + (4, -1), // 5 = t2 + ) + let labels = ($s_1$, $s_2$, $2$, $3$, $t_1$, $t_2$) + let arcs = ((0, 2), (0, 3), (1, 2), (1, 3), (2, 4), (2, 5), (3, 4), (3, 5)) + // Commodity 1 path: arcs 0 (0->2) and 4 (2->4) + let c1-arcs = (0, 4) + // Commodity 2 path: arcs 3 (1->3) and 7 (3->5) + let c2-arcs = (3, 7) + + // Draw arcs + for (idx, (u, v)) in arcs.enumerate() { + let from = positions.at(u) + let to = positions.at(v) + let color = if c1-arcs.contains(idx) { blue } else if c2-arcs.contains(idx) { red } else { gray.darken(20%) } + let thickness = if c1-arcs.contains(idx) or c2-arcs.contains(idx) { 1.2pt } else { 0.6pt } + line(from, to, stroke: (paint: color, thickness: thickness), mark: (end: "straight", scale: 0.5)) + } + + // Draw vertices + for (k, pos) in positions.enumerate() { + let fill = if k == 0 or k == 4 { blue.lighten(70%) } else if k == 1 or k == 5 { red.lighten(70%) } else { white } + circle(pos, radius: 0.3, fill: fill, stroke: 0.6pt, name: str(k)) + content(pos, text(8pt, labels.at(k))) + } + }), + caption: [Two-commodity flow: commodity 1 (blue, $s_1 -> 2 -> t_1$) and commodity 2 (red, $s_2 -> 3 -> t_2$).], + ) +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 05740229..6482b48a 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -682,6 +682,17 @@ @inproceedings{cordella2004 doi = {10.1109/TPAMI.2004.75} } +@article{even1976, + author = {Shimon Even and Alon Itai and Adi Shamir}, + title = {On the Complexity of Timetable and Multicommodity Flow Problems}, + journal = {SIAM Journal on Computing}, + volume = {5}, + number = {4}, + pages = {691--703}, + year = {1976}, + doi = {10.1137/0205048} +} + @article{papadimitriou1982, author = {Christos H. Papadimitriou and Mihalis Yannakakis}, title = {The Complexity of Restricted Spanning Tree Problems}, diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 7d60308b..a69a1275 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -89,6 +89,52 @@ } ] }, + { + "name": "DirectedTwoCommodityIntegralFlow", + "description": "Two-commodity integral flow feasibility on a directed graph", + "fields": [ + { + "name": "graph", + "type_name": "DirectedGraph", + "description": "Directed graph G = (V, A)" + }, + { + "name": "capacities", + "type_name": "Vec", + "description": "Capacity c(a) for each arc" + }, + { + "name": "source_1", + "type_name": "usize", + "description": "Source vertex s_1 for commodity 1" + }, + { + "name": "sink_1", + "type_name": "usize", + "description": "Sink vertex t_1 for commodity 1" + }, + { + "name": "source_2", + "type_name": "usize", + "description": "Source vertex s_2 for commodity 2" + }, + { + "name": "sink_2", + "type_name": "usize", + "description": "Sink vertex t_2 for commodity 2" + }, + { + "name": "requirement_1", + "type_name": "u64", + "description": "Flow requirement R_1 for commodity 1" + }, + { + "name": "requirement_2", + "type_name": "u64", + "description": "Flow requirement R_2 for commodity 2" + } + ] + }, { "name": "ExactCoverBy3Sets", "description": "Determine if a collection of 3-element subsets contains an exact cover", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index eb200845..5429b350 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -57,6 +57,13 @@ "doc_path": "models/algebraic/struct.ClosestVectorProblem.html", "complexity": "2^num_basis_vectors" }, + { + "name": "DirectedTwoCommodityIntegralFlow", + "variant": {}, + "category": "graph", + "doc_path": "models/graph/struct.DirectedTwoCommodityIntegralFlow.html", + "complexity": "(max_capacity + 1)^(2 * num_arcs)" + }, { "name": "ExactCoverBy3Sets", "variant": {}, @@ -562,7 +569,7 @@ "edges": [ { "source": 3, - "target": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -577,7 +584,7 @@ }, { "source": 4, - "target": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -592,7 +599,7 @@ }, { "source": 4, - "target": 56, + "target": 57, "overhead": [ { "field": "num_spins", @@ -606,7 +613,7 @@ "doc_path": "rules/circuit_spinglass/index.html" }, { - "source": 8, + "source": 9, "target": 4, "overhead": [ { @@ -621,8 +628,8 @@ "doc_path": "rules/factoring_circuit/index.html" }, { - "source": 8, - "target": 13, + "source": 9, + "target": 14, "overhead": [ { "field": "num_vars", @@ -636,8 +643,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 12, - "target": 13, + "source": 13, + "target": 14, "overhead": [ { "field": "num_vars", @@ -651,8 +658,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 12, - "target": 50, + "source": 13, + "target": 51, "overhead": [ { "field": "num_vars", @@ -662,8 +669,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 16, - "target": 19, + "source": 17, + "target": 20, "overhead": [ { "field": "num_vertices", @@ -677,8 +684,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 19, - "target": 12, + "source": 20, + "target": 13, "overhead": [ { "field": "num_vars", @@ -692,8 +699,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 19, - "target": 50, + "source": 20, + "target": 51, "overhead": [ { "field": "num_vars", @@ -703,8 +710,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 20, - "target": 22, + "source": 21, + "target": 23, "overhead": [ { "field": "num_vars", @@ -718,8 +725,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 20, - "target": 50, + "source": 21, + "target": 51, "overhead": [ { "field": "num_vars", @@ -729,8 +736,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 22, + "source": 22, + "target": 23, "overhead": [ { "field": "num_vars", @@ -744,8 +751,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 21, - "target": 50, + "source": 22, + "target": 51, "overhead": [ { "field": "num_vars", @@ -755,8 +762,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 60, + "source": 22, + "target": 61, "overhead": [ { "field": "num_elements", @@ -766,8 +773,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 22, - "target": 52, + "source": 23, + "target": 53, "overhead": [ { "field": "num_clauses", @@ -785,8 +792,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 23, - "target": 50, + "source": 24, + "target": 51, "overhead": [ { "field": "num_vars", @@ -796,8 +803,8 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 25, - "target": 12, + "source": 26, + "target": 13, "overhead": [ { "field": "num_vars", @@ -811,8 +818,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 26, - "target": 56, + "source": 27, + "target": 57, "overhead": [ { "field": "num_spins", @@ -826,8 +833,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 28, - "target": 12, + "source": 29, + "target": 13, "overhead": [ { "field": "num_vars", @@ -841,8 +848,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 28, - "target": 32, + "source": 29, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -856,8 +863,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 29, - "target": 30, + "source": 30, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -871,8 +878,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 34, + "source": 30, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -886,8 +893,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 35, + "source": 31, + "target": 36, "overhead": [ { "field": "num_vertices", @@ -901,8 +908,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 31, - "target": 29, + "source": 32, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -916,8 +923,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 31, - "target": 32, + "source": 32, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -931,8 +938,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 31, - "target": 33, + "source": 32, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -946,8 +953,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 31, - "target": 37, + "source": 32, + "target": 38, "overhead": [ { "field": "num_sets", @@ -961,8 +968,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 32, - "target": 28, + "source": 33, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -976,8 +983,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 32, - "target": 39, + "source": 33, + "target": 40, "overhead": [ { "field": "num_sets", @@ -991,8 +998,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 32, - "target": 46, + "source": 33, + "target": 47, "overhead": [ { "field": "num_vertices", @@ -1006,8 +1013,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 33, - "target": 35, + "source": 34, + "target": 36, "overhead": [ { "field": "num_vertices", @@ -1021,8 +1028,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 31, + "source": 35, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1036,8 +1043,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 35, + "source": 35, + "target": 36, "overhead": [ { "field": "num_vertices", @@ -1051,8 +1058,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 32, + "source": 36, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -1066,8 +1073,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 36, - "target": 12, + "source": 37, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1081,8 +1088,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 36, - "target": 39, + "source": 37, + "target": 40, "overhead": [ { "field": "num_sets", @@ -1096,8 +1103,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 37, - "target": 31, + "source": 38, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1111,8 +1118,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 37, - "target": 39, + "source": 38, + "target": 40, "overhead": [ { "field": "num_sets", @@ -1126,8 +1133,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 38, - "target": 50, + "source": 39, + "target": 51, "overhead": [ { "field": "num_vars", @@ -1137,8 +1144,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 39, - "target": 12, + "source": 40, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1152,8 +1159,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 39, - "target": 32, + "source": 40, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -1167,8 +1174,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 39, - "target": 38, + "source": 40, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1182,8 +1189,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 40, - "target": 12, + "source": 41, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1197,8 +1204,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 43, - "target": 12, + "source": 44, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1212,8 +1219,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 46, - "target": 32, + "source": 47, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -1227,8 +1234,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 46, - "target": 43, + "source": 47, + "target": 44, "overhead": [ { "field": "num_sets", @@ -1242,8 +1249,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 50, - "target": 12, + "source": 51, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1257,8 +1264,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 50, - "target": 55, + "source": 51, + "target": 56, "overhead": [ { "field": "num_spins", @@ -1268,7 +1275,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 52, + "source": 53, "target": 4, "overhead": [ { @@ -1283,8 +1290,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 52, - "target": 16, + "source": 53, + "target": 17, "overhead": [ { "field": "num_vertices", @@ -1298,8 +1305,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 52, - "target": 21, + "source": 53, + "target": 22, "overhead": [ { "field": "num_clauses", @@ -1313,8 +1320,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 52, - "target": 31, + "source": 53, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1328,8 +1335,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 52, - "target": 40, + "source": 53, + "target": 41, "overhead": [ { "field": "num_vertices", @@ -1343,8 +1350,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 55, - "target": 50, + "source": 56, + "target": 51, "overhead": [ { "field": "num_vars", @@ -1354,8 +1361,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 56, - "target": 26, + "source": 57, + "target": 27, "overhead": [ { "field": "num_vertices", @@ -1369,8 +1376,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 56, - "target": 55, + "source": 57, + "target": 56, "overhead": [ { "field": "num_spins", @@ -1384,8 +1391,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 61, - "target": 12, + "source": 62, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1399,8 +1406,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 61, - "target": 50, + "source": 62, + "target": 51, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index a289e915..32d58428 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -247,6 +247,7 @@ Flags by problem type: FlowShopScheduling --task-lengths, --deadline [--num-processors] MinimumTardinessSequencing --n, --deadlines [--precedence-pairs] SCS --strings, --bound [--alphabet-size] + D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 ILP, CircuitSAT (via reduction only) Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph): diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a6261ac0..dc1aaca6 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -76,6 +76,13 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.deadline.is_none() && args.num_processors.is_none() && args.alphabet_size.is_none() + && args.capacities.is_none() + && args.source_1.is_none() + && args.sink_1.is_none() + && args.source_2.is_none() + && args.sink_2.is_none() + && args.requirement_1.is_none() + && args.requirement_2.is_none() } fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> { @@ -277,6 +284,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "Factoring" => "--target 15 --m 4 --n 4", "SteinerTree" => "--graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4", "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5", + "DirectedTwoCommodityIntegralFlow" => { + "--arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" --capacities 1,1,1,1,1,1,1,1 --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 --requirement-1 1 --requirement-2 1" + } "MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"", "RuralPostman" => { "--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4" @@ -1155,6 +1165,67 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // DirectedTwoCommodityIntegralFlow + "DirectedTwoCommodityIntegralFlow" => { + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "DirectedTwoCommodityIntegralFlow requires --arcs\n\n\ + Usage: pred create DirectedTwoCommodityIntegralFlow \ + --arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" \ + --capacities 1,1,1,1,1,1,1,1 \ + --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 \ + --requirement-1 1 --requirement-2 1" + ) + })?; + let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; + let capacities: Vec = if let Some(ref s) = args.capacities { + util::parse_comma_list(s)? + } else { + vec![1; num_arcs] + }; + anyhow::ensure!( + capacities.len() == num_arcs, + "capacities length ({}) must match number of arcs ({num_arcs})", + capacities.len() + ); + let n = graph.num_vertices(); + let source_1 = args.source_1.ok_or_else(|| { + anyhow::anyhow!("DirectedTwoCommodityIntegralFlow requires --source-1") + })?; + let sink_1 = args.sink_1.ok_or_else(|| { + anyhow::anyhow!("DirectedTwoCommodityIntegralFlow requires --sink-1") + })?; + let source_2 = args.source_2.ok_or_else(|| { + anyhow::anyhow!("DirectedTwoCommodityIntegralFlow requires --source-2") + })?; + let sink_2 = args.sink_2.ok_or_else(|| { + anyhow::anyhow!("DirectedTwoCommodityIntegralFlow requires --sink-2") + })?; + for (name, idx) in [ + ("source_1", source_1), + ("sink_1", sink_1), + ("source_2", source_2), + ("sink_2", sink_2), + ] { + anyhow::ensure!(idx < n, "{name} ({idx}) >= num_vertices ({n})"); + } + let requirement_1 = args.requirement_1.unwrap_or(1); + let requirement_2 = args.requirement_2.unwrap_or(1); + ( + ser(DirectedTwoCommodityIntegralFlow::new( + graph, + capacities, + source_1, + sink_1, + source_2, + sink_2, + requirement_1, + requirement_2, + ))?, + resolved_variant.clone(), + ) + } + // MinimumFeedbackArcSet "MinimumFeedbackArcSet" => { let arcs_str = args.arcs.as_deref().ok_or_else(|| { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 659bba48..938d8643 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -79,8 +79,12 @@ impl LoadedProblem { } } - let reduction_path = - best_path.ok_or_else(|| anyhow::anyhow!("No reduction path from {} to ILP", name))?; + let reduction_path = best_path.ok_or_else(|| { + anyhow::anyhow!( + "No reduction path from {} to ILP. Try `--solver brute-force`, or reduce to a problem that supports ILP.", + name + ) + })?; let chain = graph .reduce_along_path(&reduction_path, self.as_any()) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 419e690a..8d481fcc 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -824,6 +824,100 @@ fn test_create_x3c_alias() { std::fs::remove_file(&output_file).ok(); } +#[test] +fn test_create_d2cif_alias() { + let output_file = std::env::temp_dir().join("pred_test_create_d2cif.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "D2CIF", + "--arcs", + "0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5", + "--capacities", + "1,1,1,1,1,1,1,1", + "--source-1", + "0", + "--sink-1", + "4", + "--source-2", + "1", + "--sink-2", + "5", + "--requirement-1", + "1", + "--requirement-2", + "1", + ]) + .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"], "DirectedTwoCommodityIntegralFlow"); + + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_solve_d2cif_default_solver_suggests_bruteforce() { + let output_file = std::env::temp_dir().join("pred_test_solve_d2cif.json"); + let create_output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "D2CIF", + "--arcs", + "0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5", + "--capacities", + "1,1,1,1,1,1,1,1", + "--source-1", + "0", + "--sink-1", + "4", + "--source-2", + "1", + "--sink-2", + "5", + "--requirement-1", + "1", + "--requirement-2", + "1", + ]) + .output() + .unwrap(); + assert!( + create_output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&create_output.stderr) + ); + + let solve_output = pred() + .args(["solve", output_file.to_str().unwrap()]) + .output() + .unwrap(); + assert!( + !solve_output.status.success(), + "stdout: {}", + String::from_utf8_lossy(&solve_output.stdout) + ); + let stderr = String::from_utf8_lossy(&solve_output.stderr); + assert!( + stderr.contains("--solver brute-force"), + "expected brute-force hint, got: {stderr}" + ); + + std::fs::remove_file(&output_file).ok(); +} + #[test] fn test_create_x3c_rejects_duplicate_subset_elements() { let output = pred() diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 891fe8e7..39501490 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -4,6 +4,7 @@ {"problem":"BicliqueCover","variant":{},"instance":{"graph":{"edges":[[0,0],[0,1],[1,1],[1,2]],"left_size":2,"right_size":3},"k":2},"samples":[{"config":[1,0,0,1,1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[0,1,0,1,0,1,0,1,0,1],"metric":{"Valid":5}},{"config":[1,0,1,0,1,0,1,0,1,0],"metric":{"Valid":5}}]}, {"problem":"CircuitSAT","variant":{},"instance":{"circuit":{"assignments":[{"expr":{"op":{"And":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["a"]},{"expr":{"op":{"Or":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["b"]},{"expr":{"op":{"Xor":[{"op":{"Var":"a"}},{"op":{"Var":"b"}}]}},"outputs":["c"]}]},"variables":["a","b","c","x1","x2"]},"samples":[{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true}],"optimal":[{"config":[0,0,0,0,0],"metric":true},{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true},{"config":[1,1,0,1,1],"metric":true}]}, {"problem":"ClosestVectorProblem","variant":{"weight":"i32"},"instance":{"basis":[[2,0],[1,2]],"bounds":[{"lower":-2,"upper":4},{"lower":-2,"upper":4}],"target":[2.8,1.5]},"samples":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}],"optimal":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}]}, + {"problem":"DirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"directed","edges":[[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,4,null],[2,5,null],[3,4,null],[3,5,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":4,"sink_2":5,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true}],"optimal":[{"config":[0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,1,0,1,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,1,0,1,1,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,1,1,1,0,1],"metric":true},{"config":[0,1,0,1,0,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,1,1,1,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,1,0,1,1,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,1,1,1,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,1,0,1,1,1,0,0,0,0,1,0,0,0,1],"metric":true}]}, {"problem":"ExactCoverBy3Sets","variant":{},"instance":{"subsets":[[0,1,2],[0,2,4],[3,4,5],[3,5,7],[6,7,8],[1,4,6],[2,5,8]],"universe_size":9},"samples":[{"config":[1,0,1,0,1,0,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,0],"metric":true}]}, {"problem":"Factoring","variant":{},"instance":{"m":2,"n":3,"target":15},"samples":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}],"optimal":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}]}, {"problem":"HamiltonianPath","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[3,4,null],[3,5,null],[4,2,null],[5,1,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,2,4,3,1,5],"metric":true}],"optimal":[{"config":[0,1,5,3,2,4],"metric":true},{"config":[0,1,5,3,4,2],"metric":true},{"config":[0,2,4,3,1,5],"metric":true},{"config":[0,2,4,3,5,1],"metric":true},{"config":[1,0,2,4,3,5],"metric":true},{"config":[1,5,3,4,2,0],"metric":true},{"config":[2,0,1,5,3,4],"metric":true},{"config":[2,4,3,5,1,0],"metric":true},{"config":[3,4,2,0,1,5],"metric":true},{"config":[3,5,1,0,2,4],"metric":true},{"config":[4,2,0,1,3,5],"metric":true},{"config":[4,2,0,1,5,3],"metric":true},{"config":[4,2,3,5,1,0],"metric":true},{"config":[4,3,2,0,1,5],"metric":true},{"config":[4,3,5,1,0,2],"metric":true},{"config":[5,1,0,2,3,4],"metric":true},{"config":[5,1,0,2,4,3],"metric":true},{"config":[5,1,3,4,2,0],"metric":true},{"config":[5,3,1,0,2,4],"metric":true},{"config":[5,3,4,2,0,1],"metric":true}]}, diff --git a/src/lib.rs b/src/lib.rs index acbd1d76..80e37f7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,8 +45,9 @@ pub mod prelude { pub use crate::models::algebraic::{BMF, QUBO}; pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use crate::models::graph::{ - BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, - LengthBoundedDisjointPaths, SpinGlass, SteinerTree, SubgraphIsomorphism, + BicliqueCover, DirectedTwoCommodityIntegralFlow, GraphPartitioning, HamiltonianPath, + IsomorphicSpanningTree, LengthBoundedDisjointPaths, SpinGlass, SteinerTree, + SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, diff --git a/src/models/graph/directed_two_commodity_integral_flow.rs b/src/models/graph/directed_two_commodity_integral_flow.rs new file mode 100644 index 00000000..ea6f8962 --- /dev/null +++ b/src/models/graph/directed_two_commodity_integral_flow.rs @@ -0,0 +1,305 @@ +//! Directed Two-Commodity Integral Flow problem implementation. +//! +//! Given a directed graph with arc capacities and two source-sink pairs with +//! flow requirements, determine whether two integral flow functions exist that +//! jointly satisfy capacity, conservation, and requirement constraints. +//! +//! NP-complete even with unit capacities (Even, Itai, and Shamir, 1976). + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::DirectedGraph; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "DirectedTwoCommodityIntegralFlow", + display_name: "Directed Two-Commodity Integral Flow", + aliases: &["D2CIF"], + dimensions: &[], + module_path: module_path!(), + description: "Two-commodity integral flow feasibility on a directed graph", + 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_1", type_name: "usize", description: "Source vertex s_1 for commodity 1" }, + FieldInfo { name: "sink_1", type_name: "usize", description: "Sink vertex t_1 for commodity 1" }, + FieldInfo { name: "source_2", type_name: "usize", description: "Source vertex s_2 for commodity 2" }, + FieldInfo { name: "sink_2", type_name: "usize", description: "Sink vertex t_2 for commodity 2" }, + FieldInfo { name: "requirement_1", type_name: "u64", description: "Flow requirement R_1 for commodity 1" }, + FieldInfo { name: "requirement_2", type_name: "u64", description: "Flow requirement R_2 for commodity 2" }, + ], + } +} + +/// Directed Two-Commodity Integral Flow problem. +/// +/// Given a directed graph G = (V, A) with arc capacities c(a), two source-sink +/// pairs (s_1, t_1) and (s_2, t_2), and requirements R_1, R_2, determine +/// whether two integral flow functions f_1, f_2: A -> Z_0^+ exist such that: +/// 1. Joint capacity: f_1(a) + f_2(a) <= c(a) for all a in A +/// 2. Flow conservation: for each commodity i, flow is conserved at every +/// vertex except the four terminals +/// 3. Requirements: net flow into t_i under f_i is at least R_i +/// +/// # Variables +/// +/// 2|A| variables: first |A| for commodity 1's flow on each arc, +/// next |A| for commodity 2's flow on each arc. Variable j for arc a +/// of commodity i ranges over {0, ..., c(a)}. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::graph::DirectedTwoCommodityIntegralFlow; +/// use problemreductions::topology::DirectedGraph; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 6-vertex network: s1=0, s2=1, t1=4, t2=5 +/// let graph = DirectedGraph::new(6, vec![ +/// (0, 2), (0, 3), (1, 2), (1, 3), +/// (2, 4), (2, 5), (3, 4), (3, 5), +/// ]); +/// let problem = DirectedTwoCommodityIntegralFlow::new( +/// graph, vec![1; 8], 0, 4, 1, 5, 1, 1, +/// ); +/// let solver = BruteForce::new(); +/// assert!(solver.find_satisfying(&problem).is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DirectedTwoCommodityIntegralFlow { + /// The directed graph G = (V, A). + graph: DirectedGraph, + /// Capacity c(a) for each arc. + capacities: Vec, + /// Source vertex s_1 for commodity 1. + source_1: usize, + /// Sink vertex t_1 for commodity 1. + sink_1: usize, + /// Source vertex s_2 for commodity 2. + source_2: usize, + /// Sink vertex t_2 for commodity 2. + sink_2: usize, + /// Flow requirement R_1 for commodity 1. + requirement_1: u64, + /// Flow requirement R_2 for commodity 2. + requirement_2: u64, +} + +impl DirectedTwoCommodityIntegralFlow { + /// Create a new Directed Two-Commodity Integral Flow problem. + /// + /// # Panics + /// + /// Panics if: + /// - `capacities.len() != graph.num_arcs()` + /// - Any terminal vertex index >= `graph.num_vertices()` + #[allow(clippy::too_many_arguments)] + pub fn new( + graph: DirectedGraph, + capacities: Vec, + source_1: usize, + sink_1: usize, + source_2: usize, + sink_2: usize, + requirement_1: u64, + requirement_2: u64, + ) -> Self { + let n = graph.num_vertices(); + assert_eq!( + capacities.len(), + graph.num_arcs(), + "capacities length must match graph num_arcs" + ); + assert!(source_1 < n, "source_1 ({source_1}) >= num_vertices ({n})"); + assert!(sink_1 < n, "sink_1 ({sink_1}) >= num_vertices ({n})"); + assert!(source_2 < n, "source_2 ({source_2}) >= num_vertices ({n})"); + assert!(sink_2 < n, "sink_2 ({sink_2}) >= num_vertices ({n})"); + Self { + graph, + capacities, + source_1, + sink_1, + source_2, + sink_2, + requirement_1, + requirement_2, + } + } + + /// Get a reference to the underlying directed graph. + pub fn graph(&self) -> &DirectedGraph { + &self.graph + } + + /// Get a reference to the capacities. + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + /// Get source vertex for commodity 1. + pub fn source_1(&self) -> usize { + self.source_1 + } + + /// Get sink vertex for commodity 1. + pub fn sink_1(&self) -> usize { + self.sink_1 + } + + /// Get source vertex for commodity 2. + pub fn source_2(&self) -> usize { + self.source_2 + } + + /// Get sink vertex for commodity 2. + pub fn sink_2(&self) -> usize { + self.sink_2 + } + + /// Get requirement for commodity 1. + pub fn requirement_1(&self) -> u64 { + self.requirement_1 + } + + /// Get requirement for commodity 2. + pub fn requirement_2(&self) -> u64 { + self.requirement_2 + } + + /// Get the number of vertices. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of arcs. + pub fn num_arcs(&self) -> usize { + self.graph.num_arcs() + } + + /// Get the maximum capacity across all arcs. + pub fn max_capacity(&self) -> u64 { + self.capacities.iter().copied().max().unwrap_or(0) + } + + /// Check whether a flow assignment is feasible. + /// + /// `config` has 2*|A| entries: first |A| for commodity 1, next |A| for commodity 2. + pub fn is_feasible(&self, config: &[usize]) -> bool { + let m = self.graph.num_arcs(); + if config.len() != 2 * m { + return false; + } + let arcs = self.graph.arcs(); + let terminals = [self.source_1, self.sink_1, self.source_2, self.sink_2]; + + // (1) Joint capacity constraint + for a in 0..m { + let f1 = config[a] as u64; + let f2 = config[m + a] as u64; + if f1 + f2 > self.capacities[a] { + return false; + } + } + + // (2) Flow conservation for each commodity at non-terminal vertices + let n = self.graph.num_vertices(); + let mut balances = [vec![0_i128; n], vec![0_i128; n]]; + for (a, &(u, w)) in arcs.iter().enumerate() { + let flow_1 = config[a] as i128; + let flow_2 = config[m + a] as i128; + + balances[0][u] -= flow_1; + balances[0][w] += flow_1; + balances[1][u] -= flow_2; + balances[1][w] += flow_2; + } + + for (commodity, commodity_balances) in balances.iter().enumerate() { + for (v, &balance) in commodity_balances.iter().enumerate() { + if !terminals.contains(&v) && balance != 0 { + return false; + } + } + + let snk = if commodity == 0 { + self.sink_1 + } else { + self.sink_2 + }; + let req = if commodity == 0 { + self.requirement_1 + } else { + self.requirement_2 + }; + + if commodity_balances[snk] < i128::from(req) { + return false; + } + } + + true + } +} + +impl Problem for DirectedTwoCommodityIntegralFlow { + const NAME: &'static str = "DirectedTwoCommodityIntegralFlow"; + type Metric = bool; + + fn dims(&self) -> Vec { + self.capacities + .iter() + .chain(self.capacities.iter()) + .map(|&c| (c as usize) + 1) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.is_feasible(config) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for DirectedTwoCommodityIntegralFlow {} + +crate::declare_variants! { + default sat DirectedTwoCommodityIntegralFlow => "(max_capacity + 1)^(2 * num_arcs)", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "directed_two_commodity_integral_flow", + build: || { + let graph = DirectedGraph::new( + 6, + vec![ + (0, 2), + (0, 3), + (1, 2), + (1, 3), + (2, 4), + (2, 5), + (3, 4), + (3, 5), + ], + ); + let problem = + DirectedTwoCommodityIntegralFlow::new(graph, vec![1; 8], 0, 4, 1, 5, 1, 1); + // Solution: commodity 1 path 0->2->4 (arcs 0,4), commodity 2 path 1->3->5 (arcs 3,7) + // config = [f1(a0)..f1(a7), f2(a0)..f2(a7)] + // = [1,0,0,0,1,0,0,0, 0,0,0,1,0,0,0,1] + crate::example_db::specs::satisfaction_example( + problem, + vec![vec![1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]], + ) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/directed_two_commodity_integral_flow.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index bde7b663..ef940c03 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -24,9 +24,11 @@ //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) //! - [`SteinerTree`]: Minimum-weight tree spanning all required terminals //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) +//! - [`DirectedTwoCommodityIntegralFlow`]: Directed two-commodity integral flow (satisfaction) //! - [`UndirectedTwoCommodityIntegralFlow`]: Two-commodity integral flow on undirected graphs pub(crate) mod biclique_cover; +pub(crate) mod directed_two_commodity_integral_flow; pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_path; pub(crate) mod isomorphic_spanning_tree; @@ -52,6 +54,7 @@ pub(crate) mod traveling_salesman; pub(crate) mod undirected_two_commodity_integral_flow; pub use biclique_cover::BicliqueCover; +pub use directed_two_commodity_integral_flow::DirectedTwoCommodityIntegralFlow; pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_path::HamiltonianPath; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; @@ -97,6 +100,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec DirectedTwoCommodityIntegralFlow { + let graph = DirectedGraph::new( + 6, + vec![ + (0, 2), + (0, 3), + (1, 2), + (1, 3), + (2, 4), + (2, 5), + (3, 4), + (3, 5), + ], + ); + DirectedTwoCommodityIntegralFlow::new(graph, vec![1; 8], 0, 4, 1, 5, 1, 1) +} + +/// NO instance: 4 vertices, 3 arcs (all capacity 1). +/// s1=0, t1=3, s2=1, t2=3, R1=1, R2=1. +/// Bottleneck at arc (2,3) with capacity 1. +fn no_instance() -> DirectedTwoCommodityIntegralFlow { + let graph = DirectedGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]); + DirectedTwoCommodityIntegralFlow::new(graph, vec![1; 3], 0, 3, 1, 3, 1, 1) +} + +#[test] +fn test_directed_two_commodity_integral_flow_creation() { + let problem = yes_instance(); + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_arcs(), 8); + assert_eq!(problem.dims().len(), 16); // 2 * 8 + assert!(problem.dims().iter().all(|&d| d == 2)); // capacity 1 -> domain {0,1} + assert_eq!(problem.source_1(), 0); + assert_eq!(problem.sink_1(), 4); + assert_eq!(problem.source_2(), 1); + assert_eq!(problem.sink_2(), 5); + assert_eq!(problem.requirement_1(), 1); + assert_eq!(problem.requirement_2(), 1); + assert_eq!(problem.max_capacity(), 1); +} + +#[test] +fn test_directed_two_commodity_integral_flow_evaluation_satisfying() { + let problem = yes_instance(); + // Commodity 1: path 0->2->4 (arcs 0,4) + // Commodity 2: path 1->3->5 (arcs 3,7) + // config = [f1(a0..a7), f2(a0..a7)] + let config = vec![1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_directed_two_commodity_integral_flow_evaluation_unsatisfying() { + let problem = no_instance(); + // All zeros: no flow at all + let config = vec![0, 0, 0, 0, 0, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_directed_two_commodity_integral_flow_capacity_violation() { + let problem = yes_instance(); + // Try sending both commodities through the same arc (arc 0: 0->2) + // f1(a0)=1, f2(a0)=1 -> violates capacity 1 + let mut config = vec![0; 16]; + config[0] = 1; // f1 on arc 0 + config[8] = 1; // f2 on arc 0 + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_directed_two_commodity_integral_flow_conservation_violation() { + let problem = yes_instance(); + // f1 sends flow into vertex 2 but not out + let mut config = vec![0; 16]; + config[0] = 1; // f1 on arc 0 (0->2): flow into vertex 2 + // No outgoing flow from vertex 2 for commodity 1 + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_directed_two_commodity_integral_flow_negative_net_flow_at_sink_is_infeasible() { + let graph = DirectedGraph::new(3, vec![(1, 2)]); + let problem = DirectedTwoCommodityIntegralFlow::new(graph, vec![1], 0, 1, 2, 2, 1, 0); + + // Commodity 1 sends flow out of its sink with no incoming flow. + let config = vec![1, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_directed_two_commodity_integral_flow_solver_yes() { + let problem = yes_instance(); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + let sol = solution.unwrap(); + assert!(problem.evaluate(&sol)); +} + +#[test] +fn test_directed_two_commodity_integral_flow_solver_no() { + let problem = no_instance(); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_directed_two_commodity_integral_flow_serialization() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: DirectedTwoCommodityIntegralFlow = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.num_vertices(), 6); + assert_eq!(deserialized.num_arcs(), 8); + assert_eq!(deserialized.source_1(), 0); + assert_eq!(deserialized.sink_1(), 4); + assert_eq!(deserialized.source_2(), 1); + assert_eq!(deserialized.sink_2(), 5); + assert_eq!(deserialized.requirement_1(), 1); + assert_eq!(deserialized.requirement_2(), 1); +} + +#[test] +fn test_directed_two_commodity_integral_flow_problem_name() { + assert_eq!( + ::NAME, + "DirectedTwoCommodityIntegralFlow" + ); +} + +#[test] +fn test_directed_two_commodity_integral_flow_accessors() { + let problem = yes_instance(); + assert_eq!(problem.graph().num_vertices(), 6); + assert_eq!(problem.graph().num_arcs(), 8); + assert_eq!(problem.capacities(), &[1, 1, 1, 1, 1, 1, 1, 1]); +} + +#[test] +fn test_directed_two_commodity_integral_flow_paper_example() { + let problem = yes_instance(); + let solver = BruteForce::new(); + + // Verify the known solution evaluates to true + let config = vec![1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]; + assert!(problem.evaluate(&config)); + + // Find all satisfying solutions and verify count + let all_solutions = solver.find_all_satisfying(&problem); + assert!(!all_solutions.is_empty()); + + // Each solution must evaluate to true + for sol in &all_solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_directed_two_commodity_integral_flow_wrong_config_length() { + let problem = yes_instance(); + // Config with wrong length should return false (infeasible) + assert!(!problem.evaluate(&[0; 15])); // too short + assert!(!problem.evaluate(&[0; 17])); // too long + assert!(!problem.evaluate(&[])); // empty +} + +#[test] +fn test_directed_two_commodity_integral_flow_higher_capacity() { + // Test with capacity 2: two paths can share an arc + let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = DirectedTwoCommodityIntegralFlow::new( + graph, + vec![2, 2], // capacity 2 on both arcs + 0, + 2, + 0, + 2, + 1, + 1, + ); + assert_eq!(problem.dims(), vec![3, 3, 3, 3]); // each variable in {0,1,2} + + // Both commodities can share: f1=1, f2=1 on both arcs + let config = vec![1, 1, 1, 1]; + assert!(problem.evaluate(&config)); + + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_some()); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index e5117e24..ef0f09f6 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -106,6 +106,19 @@ fn test_all_problems_implement_trait_correctly() { ), "MinimumFeedbackArcSet", ); + check_problem_trait( + &DirectedTwoCommodityIntegralFlow::new( + DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![1; 3], + 0, + 3, + 0, + 3, + 1, + 1, + ), + "DirectedTwoCommodityIntegralFlow", + ); check_problem_trait( &MinimumSumMulticenter::new( SimpleGraph::new(3, vec![(0, 1), (1, 2)]),