diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b3a286b6..af393d2b 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -89,6 +89,7 @@ "BMF": [Boolean Matrix Factorization], "PaintShop": [Paint Shop], "BicliqueCover": [Biclique Cover], + "BalancedCompleteBipartiteSubgraph": [Balanced Complete Bipartite Subgraph], "BinPacking": [Bin Packing], "ClosestVectorProblem": [Closest Vector Problem], "OptimalLinearArrangement": [Optimal Linear Arrangement], @@ -1517,6 +1518,72 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("BalancedCompleteBipartiteSubgraph") + let left-size = x.instance.graph.left_size + let right-size = x.instance.graph.right_size + let k = x.instance.k + let bip-edges = x.instance.graph.edges + let sol = x.optimal.at(0) + let left-selected = range(left-size).filter(i => sol.config.at(i) == 1) + let right-selected = range(right-size).filter(i => sol.config.at(left-size + i) == 1) + let selected-edges = bip-edges.filter(e => + left-selected.contains(e.at(0)) and right-selected.contains(e.at(1)) + ) + [ + #problem-def("BalancedCompleteBipartiteSubgraph")[ + Given a bipartite graph $G = (A, B, E)$ and an integer $k$, determine whether there exist subsets $A' subset.eq A$ and $B' subset.eq B$ such that $|A'| = |B'| = k$ and every cross pair is present: + $A' times B' subset.eq E.$ + ][ + Balanced Complete Bipartite Subgraph is a classical NP-complete bipartite containment problem from Garey and Johnson @garey1979. Unlike Biclique Cover, which asks for a collection of bicliques covering all edges, this problem asks for a _single_ balanced biclique of prescribed size. It arises naturally in biclustering, dense submatrix discovery, and pattern mining on bipartite data. Chen et al. give an exact $O^*(1.3803^n)$ algorithm for dense bipartite graphs, and the registry records that best-known bound in the catalog metadata. A straightforward baseline still enumerates all $k$-subsets of $A$ and $B$ and checks whether they induce a complete bipartite graph, taking $O(binom(|A|, k) dot binom(|B|, k) dot k^2) = O^*(2^(|A| + |B|))$ time. + + *Example.* Consider the bipartite graph with $A = {ell_1, ell_2, ell_3, ell_4}$, $B = {r_1, r_2, r_3, r_4}$, and edges $E = {#bip-edges.map(e => $(ell_#(e.at(0) + 1), r_#(e.at(1) + 1))$).join(", ")}$. For $k = #k$, the selected sets $A' = {#left-selected.map(i => $ell_#(i + 1)$).join(", ")}$ and $B' = {#right-selected.map(i => $r_#(i + 1)$).join(", ")}$ form a balanced complete bipartite subgraph: all #selected-edges.len() required cross edges are present. Vertex $ell_4$ is excluded because $(ell_4, r_3) in.not E$, so any witness using $ell_4$ cannot realize $K_(#k,#k)$. + + #figure( + canvas(length: 1cm, { + let lpos = range(left-size).map(i => (0, left-size - 1 - i)) + let rpos = range(right-size).map(i => (2.6, right-size - 1 - i)) + for (li, rj) in bip-edges { + let selected = selected-edges.any(e => e.at(0) == li and e.at(1) == rj) + g-edge( + lpos.at(li), + rpos.at(rj), + stroke: if selected { 2pt + graph-colors.at(0) } else { 1pt + luma(180) }, + ) + } + for (idx, pos) in lpos.enumerate() { + let selected = left-selected.contains(idx) + g-node( + pos, + name: "bcbs-l" + str(idx), + fill: if selected { graph-colors.at(0) } else { luma(240) }, + label: if selected { + text(fill: white)[$ell_#(idx + 1)$] + } else { + [$ell_#(idx + 1)$] + }, + ) + } + for (idx, pos) in rpos.enumerate() { + let selected = right-selected.contains(idx) + g-node( + pos, + name: "bcbs-r" + str(idx), + fill: if selected { graph-colors.at(0) } else { luma(240) }, + label: if selected { + text(fill: white)[$r_#(idx + 1)$] + } else { + [$r_#(idx + 1)$] + }, + ) + } + }), + caption: [Balanced complete bipartite subgraph with $k = #k$: the selected vertices $A' = {#left-selected.map(i => $ell_#(i + 1)$).join(", ")}$ and $B' = {#right-selected.map(i => $r_#(i + 1)$).join(", ")}$ are blue, and the 9 edges of the induced $K_(#k,#k)$ are highlighted. The missing edge $(ell_4, r_3)$ prevents including $ell_4$.], + ) + ] + ] +} + #{ let x = load-model-example("PartitionIntoTriangles") let nv = graph-num-vertices(x.instance) diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 3a6e9df8..fa8d6bef 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -15,6 +15,22 @@ } ] }, + { + "name": "BalancedCompleteBipartiteSubgraph", + "description": "Decide whether a bipartite graph contains a K_{k,k} subgraph", + "fields": [ + { + "name": "graph", + "type_name": "BipartiteGraph", + "description": "The bipartite graph G = (A, B, E)" + }, + { + "name": "k", + "type_name": "usize", + "description": "Balanced biclique size" + } + ] + }, { "name": "BicliqueCover", "description": "Cover bipartite edges with k bicliques", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index c1334cfb..21734ea6 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -7,6 +7,13 @@ "doc_path": "models/algebraic/struct.BMF.html", "complexity": "2^(rows * rank + rank * cols)" }, + { + "name": "BalancedCompleteBipartiteSubgraph", + "variant": {}, + "category": "graph", + "doc_path": "models/graph/struct.BalancedCompleteBipartiteSubgraph.html", + "complexity": "1.3803^num_vertices" + }, { "name": "BicliqueCover", "variant": {}, @@ -518,8 +525,8 @@ ], "edges": [ { - "source": 3, - "target": 12, + "source": 4, + "target": 13, "overhead": [ { "field": "num_vars", @@ -533,8 +540,8 @@ "doc_path": "rules/binpacking_ilp/index.html" }, { - "source": 4, - "target": 12, + "source": 5, + "target": 13, "overhead": [ { "field": "num_vars", @@ -548,8 +555,8 @@ "doc_path": "rules/circuit_ilp/index.html" }, { - "source": 4, - "target": 54, + "source": 5, + "target": 55, "overhead": [ { "field": "num_spins", @@ -563,8 +570,8 @@ "doc_path": "rules/circuit_spinglass/index.html" }, { - "source": 8, - "target": 4, + "source": 9, + "target": 5, "overhead": [ { "field": "num_variables", @@ -578,8 +585,8 @@ "doc_path": "rules/factoring_circuit/index.html" }, { - "source": 8, - "target": 13, + "source": 9, + "target": 14, "overhead": [ { "field": "num_vars", @@ -593,8 +600,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 12, - "target": 13, + "source": 13, + "target": 14, "overhead": [ { "field": "num_vars", @@ -608,8 +615,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 12, - "target": 49, + "source": 13, + "target": 50, "overhead": [ { "field": "num_vars", @@ -619,8 +626,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 16, - "target": 19, + "source": 17, + "target": 20, "overhead": [ { "field": "num_vertices", @@ -634,8 +641,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 19, - "target": 12, + "source": 20, + "target": 13, "overhead": [ { "field": "num_vars", @@ -649,8 +656,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 19, - "target": 49, + "source": 20, + "target": 50, "overhead": [ { "field": "num_vars", @@ -660,8 +667,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 20, - "target": 22, + "source": 21, + "target": 23, "overhead": [ { "field": "num_vars", @@ -675,8 +682,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 20, - "target": 49, + "source": 21, + "target": 50, "overhead": [ { "field": "num_vars", @@ -686,8 +693,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 22, + "source": 22, + "target": 23, "overhead": [ { "field": "num_vars", @@ -701,8 +708,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 21, - "target": 49, + "source": 22, + "target": 50, "overhead": [ { "field": "num_vars", @@ -712,8 +719,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 56, + "source": 22, + "target": 57, "overhead": [ { "field": "num_elements", @@ -723,8 +730,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 22, - "target": 51, + "source": 23, + "target": 52, "overhead": [ { "field": "num_clauses", @@ -742,8 +749,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 23, - "target": 49, + "source": 24, + "target": 50, "overhead": [ { "field": "num_vars", @@ -753,8 +760,8 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 24, - "target": 12, + "source": 25, + "target": 13, "overhead": [ { "field": "num_vars", @@ -768,8 +775,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 25, - "target": 54, + "source": 26, + "target": 55, "overhead": [ { "field": "num_spins", @@ -783,8 +790,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 27, - "target": 12, + "source": 28, + "target": 13, "overhead": [ { "field": "num_vars", @@ -798,8 +805,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 27, - "target": 31, + "source": 28, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -813,8 +820,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 28, - "target": 29, + "source": 29, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -828,8 +835,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 33, + "source": 29, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -843,8 +850,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 34, + "source": 30, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -858,8 +865,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 28, + "source": 31, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -873,8 +880,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 30, - "target": 31, + "source": 31, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -888,8 +895,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 32, + "source": 31, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -903,8 +910,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 30, - "target": 36, + "source": 31, + "target": 37, "overhead": [ { "field": "num_sets", @@ -918,8 +925,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 27, + "source": 32, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -933,8 +940,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 31, - "target": 38, + "source": 32, + "target": 39, "overhead": [ { "field": "num_sets", @@ -948,8 +955,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 45, + "source": 32, + "target": 46, "overhead": [ { "field": "num_vertices", @@ -963,8 +970,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 32, - "target": 34, + "source": 33, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -978,8 +985,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 30, + "source": 34, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -993,8 +1000,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 34, + "source": 34, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -1008,8 +1015,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 31, + "source": 35, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1023,8 +1030,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 12, + "source": 36, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1038,8 +1045,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 35, - "target": 38, + "source": 36, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1053,8 +1060,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 36, - "target": 30, + "source": 37, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1068,8 +1075,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 36, - "target": 38, + "source": 37, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1083,8 +1090,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 37, - "target": 49, + "source": 38, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1094,8 +1101,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 38, - "target": 12, + "source": 39, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1109,8 +1116,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 38, - "target": 31, + "source": 39, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1124,8 +1131,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 38, - "target": 37, + "source": 39, + "target": 38, "overhead": [ { "field": "num_sets", @@ -1139,8 +1146,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 39, - "target": 12, + "source": 40, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1154,8 +1161,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 42, - "target": 12, + "source": 43, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1169,8 +1176,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 45, - "target": 31, + "source": 46, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1184,8 +1191,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 45, - "target": 42, + "source": 46, + "target": 43, "overhead": [ { "field": "num_sets", @@ -1199,8 +1206,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 49, - "target": 12, + "source": 50, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1214,8 +1221,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 49, - "target": 53, + "source": 50, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1225,8 +1232,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 51, - "target": 4, + "source": 52, + "target": 5, "overhead": [ { "field": "num_variables", @@ -1240,8 +1247,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 51, - "target": 16, + "source": 52, + "target": 17, "overhead": [ { "field": "num_vertices", @@ -1255,8 +1262,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 51, - "target": 21, + "source": 52, + "target": 22, "overhead": [ { "field": "num_clauses", @@ -1270,8 +1277,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 51, - "target": 30, + "source": 52, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1285,8 +1292,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 51, - "target": 39, + "source": 52, + "target": 40, "overhead": [ { "field": "num_vertices", @@ -1300,8 +1307,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 53, - "target": 49, + "source": 54, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1311,8 +1318,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 54, - "target": 25, + "source": 55, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -1326,8 +1333,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 54, - "target": 53, + "source": 55, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1341,8 +1348,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 57, - "target": 12, + "source": 58, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1356,8 +1363,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 57, - "target": 49, + "source": 58, + "target": 50, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index c42f69c2..9562e5e7 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -232,6 +232,7 @@ Flags by problem type: MinimumSetCovering --universe, --sets [--weights] X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) BicliqueCover --left, --right, --biedges, --k + BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k BMF --matrix (0/1), --rank CVP --basis, --target-vec [--bounds] OptimalLinearArrangement --graph, --bound @@ -346,13 +347,13 @@ pub struct CreateArgs { /// Universe size for MinimumSetCovering #[arg(long)] pub universe: Option, - /// Bipartite graph edges for BicliqueCover (e.g., "0-0,0-1,1-2" for left-right pairs) + /// Bipartite graph edges for BicliqueCover / BalancedCompleteBipartiteSubgraph (e.g., "0-0,0-1,1-2" for left-right pairs) #[arg(long)] pub biedges: Option, - /// Left partition size for BicliqueCover + /// Left partition size for BicliqueCover / BalancedCompleteBipartiteSubgraph #[arg(long)] pub left: Option, - /// Right partition size for BicliqueCover + /// Right partition size for BicliqueCover / BalancedCompleteBipartiteSubgraph #[arg(long)] pub right: Option, /// Rank for BMF diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a1444224..d4b1605d 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -237,6 +237,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "MinimumSumMulticenter" => { "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2" } + "BalancedCompleteBipartiteSubgraph" => { + "--left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3" + } "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", "Factoring" => "--target 15 --m 4 --n 4", "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5", @@ -274,6 +277,19 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { // DirectedGraph fields use --arcs, not --graph let hint = type_format_hint(&field.type_name, graph_type); eprintln!(" --{:<16} {} ({})", "arcs", field.description, hint); + } else if field.type_name == "BipartiteGraph" { + eprintln!( + " --{:<16} {} ({})", + "left", "Vertices in the left partition", "integer" + ); + eprintln!( + " --{:<16} {} ({})", + "right", "Vertices in the right partition", "integer" + ); + eprintln!( + " --{:<16} {} ({})", + "biedges", "Bipartite edges as left-right pairs", "edge list: 0-0,0-1,1-2" + ); } else { let hint = type_format_hint(&field.type_name, graph_type); eprintln!( @@ -705,26 +721,27 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // BicliqueCover "BicliqueCover" => { - let left = args.left.ok_or_else(|| { - anyhow::anyhow!( - "BicliqueCover requires --left, --right, --biedges, and --k\n\n\ - Usage: pred create BicliqueCover --left 2 --right 2 --biedges 0-0,0-1,1-1 --k 2" - ) - })?; - let right = args.right.ok_or_else(|| { - anyhow::anyhow!("BicliqueCover requires --right (right partition size)") - })?; - let k = args.k.ok_or_else(|| { - anyhow::anyhow!("BicliqueCover requires --k (number of bicliques)") - })?; - let edges_str = args.biedges.as_deref().ok_or_else(|| { - anyhow::anyhow!("BicliqueCover requires --biedges (e.g., 0-0,0-1,1-1)") - })?; - let edges = util::parse_edge_pairs(edges_str)?; - let graph = BipartiteGraph::new(left, right, edges); + let usage = "pred create BicliqueCover --left 2 --right 2 --biedges 0-0,0-1,1-1 --k 2"; + let (graph, k) = + parse_bipartite_problem_input(args, "BicliqueCover", "number of bicliques", usage)?; (ser(BicliqueCover::new(graph, k))?, resolved_variant.clone()) } + // BalancedCompleteBipartiteSubgraph + "BalancedCompleteBipartiteSubgraph" => { + let usage = "pred create BalancedCompleteBipartiteSubgraph --left 4 --right 4 --biedges 0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3 --k 3"; + let (graph, k) = parse_bipartite_problem_input( + args, + "BalancedCompleteBipartiteSubgraph", + "balanced biclique size", + usage, + )?; + ( + ser(BalancedCompleteBipartiteSubgraph::new(graph, k))?, + resolved_variant.clone(), + ) + } + // BMF "BMF" => { let matrix = parse_bool_matrix(args)?; @@ -1098,6 +1115,108 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { emit_problem_output(&output, out) } +#[cfg(test)] +mod tests { + use super::*; + use crate::dispatch::ProblemJsonOutput; + use problemreductions::models::graph::BalancedCompleteBipartiteSubgraph; + + fn create_args_for_bcbs() -> CreateArgs { + CreateArgs { + problem: Some("BalancedCompleteBipartiteSubgraph".to_string()), + example: None, + example_target: None, + example_side: ExampleSide::Source, + graph: None, + weights: None, + edge_weights: None, + couplings: None, + fields: None, + clauses: None, + num_vars: None, + matrix: None, + k: Some(3), + random: false, + num_vertices: None, + edge_prob: None, + seed: None, + target: None, + m: None, + n: None, + positions: None, + radius: None, + sizes: None, + capacity: None, + sequence: None, + sets: None, + universe: None, + biedges: Some("0-0,0-1,0-2,1-0,1-1,1-2,2-0,2-1,2-2,3-0,3-1,3-3".to_string()), + left: Some(4), + right: Some(4), + rank: None, + basis: None, + target_vec: None, + bounds: None, + tree: None, + required_edges: None, + bound: None, + pattern: None, + strings: None, + arcs: None, + deadlines: None, + precedence_pairs: None, + task_lengths: None, + deadline: None, + num_processors: None, + alphabet_size: None, + } + } + + #[test] + fn test_create_balanced_complete_bipartite_subgraph() { + let args = create_args_for_bcbs(); + let output_path = + std::env::temp_dir().join(format!("bcbs-create-{}.json", std::process::id())); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let json = std::fs::read_to_string(&output_path).unwrap(); + let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap(); + assert_eq!(created.problem_type, "BalancedCompleteBipartiteSubgraph"); + assert!(created.variant.is_empty()); + + let problem: BalancedCompleteBipartiteSubgraph = + serde_json::from_value(created.data).unwrap(); + assert_eq!(problem.left_size(), 4); + assert_eq!(problem.right_size(), 4); + assert_eq!(problem.num_edges(), 12); + assert_eq!(problem.k(), 3); + + let _ = std::fs::remove_file(output_path); + } + + #[test] + fn test_create_balanced_complete_bipartite_subgraph_rejects_out_of_range_biedges() { + let mut args = create_args_for_bcbs(); + args.biedges = Some("4-0".to_string()); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("out of bounds for left partition size 4")); + } +} + /// Reject non-unit weights when the resolved variant uses `weight=One`. fn reject_nonunit_weights_for_one_variant( canonical: &str, @@ -1217,6 +1336,48 @@ fn variant_map(pairs: &[(&str, &str)]) -> BTreeMap { util::variant_map(pairs) } +fn parse_bipartite_problem_input( + args: &CreateArgs, + canonical: &str, + k_description: &str, + usage: &str, +) -> Result<(BipartiteGraph, usize)> { + let left = args.left.ok_or_else(|| { + anyhow::anyhow!( + "{canonical} requires --left, --right, --biedges, and --k\n\nUsage: {usage}" + ) + })?; + let right = args.right.ok_or_else(|| { + anyhow::anyhow!("{canonical} requires --right (right partition size)\n\nUsage: {usage}") + })?; + let k = args.k.ok_or_else(|| { + anyhow::anyhow!("{canonical} requires --k ({k_description})\n\nUsage: {usage}") + })?; + let edges_str = args.biedges.as_deref().ok_or_else(|| { + anyhow::anyhow!("{canonical} requires --biedges (e.g., 0-0,0-1,1-1)\n\nUsage: {usage}") + })?; + let edges = util::parse_edge_pairs(edges_str)?; + validate_bipartite_edges(canonical, left, right, &edges)?; + Ok((BipartiteGraph::new(left, right, edges), k)) +} + +fn validate_bipartite_edges( + canonical: &str, + left: usize, + right: usize, + edges: &[(usize, usize)], +) -> Result<()> { + for &(u, v) in edges { + if u >= left { + bail!("{canonical} edge {u}-{v} is out of bounds for left partition size {left}"); + } + if v >= right { + bail!("{canonical} edge {u}-{v} is out of bounds for right partition size {right}"); + } + } + Ok(()) +} + /// Parse `--graph` into a SimpleGraph, inferring num_vertices from max index. fn parse_graph(args: &CreateArgs) -> Result<(SimpleGraph, usize)> { let edges_str = args diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 659bba48..99ae4d78 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 `pred solve --solver brute-force` for exhaustive search.", + 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 f011bad1..3c8bc743 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -76,6 +76,49 @@ fn test_show_variant_info() { ); } +#[test] +fn test_show_balanced_complete_bipartite_subgraph_complexity() { + let output = pred() + .args(["show", "BalancedCompleteBipartiteSubgraph"]) + .output() + .unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("1.3803^num_vertices"), + "expected updated complexity metadata, got: {stdout}" + ); +} + +#[test] +fn test_solve_balanced_complete_bipartite_subgraph_suggests_bruteforce() { + let tmp = std::env::temp_dir().join("pred_test_bcbs_problem.json"); + let create = pred() + .args([ + "create", + "--example", + "BalancedCompleteBipartiteSubgraph", + "--json", + ]) + .output() + .unwrap(); + assert!(create.status.success()); + std::fs::write(&tmp, create.stdout).unwrap(); + + let solve = pred() + .args(["solve", tmp.to_str().unwrap()]) + .output() + .unwrap(); + assert!(!solve.status.success()); + let stderr = String::from_utf8(solve.stderr).unwrap(); + assert!( + stderr.contains("--solver brute-force"), + "expected brute-force hint, got: {stderr}" + ); + + std::fs::remove_file(tmp).ok(); +} + #[test] fn test_path() { let output = pred().args(["path", "MIS", "QUBO"]).output().unwrap(); @@ -179,6 +222,22 @@ fn test_show_includes_fields() { assert!(stdout.contains("weights")); } +#[test] +fn test_create_balanced_complete_bipartite_subgraph_help_uses_bipartite_flags() { + let output = pred() + .args(["create", "BalancedCompleteBipartiteSubgraph"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--left"), "stderr: {stderr}"); + assert!(stderr.contains("--right"), "stderr: {stderr}"); + assert!(stderr.contains("--biedges"), "stderr: {stderr}"); + assert!(!stderr.contains("--left-size"), "stderr: {stderr}"); + assert!(!stderr.contains("--right-size"), "stderr: {stderr}"); + assert!(!stderr.contains("--edges"), "stderr: {stderr}"); +} + #[test] fn test_list_json() { let tmp = std::env::temp_dir().join("pred_test_list.json"); diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index e48eef82..3a114980 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -1,6 +1,7 @@ { "models": [ {"problem":"BMF","variant":{},"instance":{"k":2,"m":3,"matrix":[[true,true,false],[true,true,true],[false,true,true]],"n":3},"samples":[{"config":[1,0,1,1,0,1,1,1,0,0,1,1],"metric":{"Valid":0}}],"optimal":[{"config":[0,1,1,1,1,0,0,1,1,1,1,0],"metric":{"Valid":0}},{"config":[1,0,1,1,0,1,1,1,0,0,1,1],"metric":{"Valid":0}}]}, + {"problem":"BalancedCompleteBipartiteSubgraph","variant":{},"instance":{"graph":{"edges":[[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2],[3,0],[3,1],[3,3]],"left_size":4,"right_size":4},"k":3},"samples":[{"config":[1,1,1,0,1,1,1,0],"metric":true}],"optimal":[{"config":[1,1,1,0,1,1,1,0],"metric":true}]}, {"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}}]}, diff --git a/src/lib.rs b/src/lib.rs index bceccfe2..05400632 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,8 +45,8 @@ 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, SpinGlass, - SubgraphIsomorphism, + BalancedCompleteBipartiteSubgraph, BicliqueCover, GraphPartitioning, HamiltonianPath, + IsomorphicSpanningTree, SpinGlass, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs new file mode 100644 index 00000000..dd648f84 --- /dev/null +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -0,0 +1,187 @@ +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::BipartiteGraph; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +inventory::submit! { + ProblemSchemaEntry { + name: "BalancedCompleteBipartiteSubgraph", + display_name: "Balanced Complete Bipartite Subgraph", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Decide whether a bipartite graph contains a K_{k,k} subgraph", + fields: &[ + FieldInfo { name: "graph", type_name: "BipartiteGraph", description: "The bipartite graph G = (A, B, E)" }, + FieldInfo { name: "k", type_name: "usize", description: "Balanced biclique size" }, + ], + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(from = "BalancedCompleteBipartiteSubgraphRepr")] +pub struct BalancedCompleteBipartiteSubgraph { + graph: BipartiteGraph, + k: usize, + #[serde(skip)] + edge_lookup: HashSet<(usize, usize)>, +} + +impl BalancedCompleteBipartiteSubgraph { + pub fn new(graph: BipartiteGraph, k: usize) -> Self { + let edge_lookup = Self::build_edge_lookup(&graph); + Self { + graph, + k, + edge_lookup, + } + } + + pub fn graph(&self) -> &BipartiteGraph { + &self.graph + } + + pub fn left_size(&self) -> usize { + self.graph.left_size() + } + + pub fn right_size(&self) -> usize { + self.graph.right_size() + } + + pub fn num_vertices(&self) -> usize { + self.left_size() + self.right_size() + } + + pub fn num_edges(&self) -> usize { + self.graph.left_edges().len() + } + + pub fn k(&self) -> usize { + self.k + } + + fn build_edge_lookup(graph: &BipartiteGraph) -> HashSet<(usize, usize)> { + graph.left_edges().iter().copied().collect() + } + + fn selected_vertices(&self, config: &[usize]) -> Option<(Vec, Vec)> { + if config.len() != self.num_vertices() { + return None; + } + + let mut selected_left = Vec::new(); + let mut selected_right = Vec::new(); + + for (index, &value) in config.iter().enumerate() { + match value { + 0 => {} + 1 => { + if index < self.left_size() { + selected_left.push(index); + } else { + selected_right.push(index - self.left_size()); + } + } + _ => return None, + } + } + + Some((selected_left, selected_right)) + } + + fn has_selected_edge(&self, left: usize, right: usize) -> bool { + self.edge_lookup.contains(&(left, right)) + } + + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate(config) + } +} + +impl Problem for BalancedCompleteBipartiteSubgraph { + const NAME: &'static str = "BalancedCompleteBipartiteSubgraph"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![2; self.num_vertices()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let Some((selected_left, selected_right)) = self.selected_vertices(config) else { + return false; + }; + + if selected_left.len() != self.k || selected_right.len() != self.k { + return false; + } + + selected_left.iter().all(|&left| { + selected_right + .iter() + .all(|&right| self.has_selected_edge(left, right)) + }) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for BalancedCompleteBipartiteSubgraph {} + +#[derive(Deserialize)] +struct BalancedCompleteBipartiteSubgraphRepr { + graph: BipartiteGraph, + k: usize, +} + +impl From for BalancedCompleteBipartiteSubgraph { + fn from(repr: BalancedCompleteBipartiteSubgraphRepr) -> Self { + Self::new(repr.graph, repr.k) + } +} + +crate::declare_variants! { + default sat BalancedCompleteBipartiteSubgraph => "1.3803^num_vertices", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "balanced_complete_bipartite_subgraph", + build: || { + let problem = BalancedCompleteBipartiteSubgraph::new( + BipartiteGraph::new( + 4, + 4, + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 1), + (1, 2), + (2, 0), + (2, 1), + (2, 2), + (3, 0), + (3, 1), + (3, 3), + ], + ), + 3, + ); + + crate::example_db::specs::satisfaction_example( + problem, + vec![vec![1, 1, 1, 0, 1, 1, 1, 0]], + ) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 7d76de65..eb799a1b 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -17,12 +17,14 @@ //! - [`SpinGlass`]: Ising model Hamiltonian //! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex) //! - [`BicliqueCover`]: Biclique cover on bipartite graphs +//! - [`BalancedCompleteBipartiteSubgraph`]: Balanced biclique decision problem //! - [`OptimalLinearArrangement`]: Optimal linear arrangement (total edge length at most K) //! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs //! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median) //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) +pub(crate) mod balanced_complete_bipartite_subgraph; pub(crate) mod biclique_cover; pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_path; @@ -45,6 +47,7 @@ pub(crate) mod spin_glass; pub(crate) mod subgraph_isomorphism; pub(crate) mod traveling_salesman; +pub use balanced_complete_bipartite_subgraph::BalancedCompleteBipartiteSubgraph; pub use biclique_cover::BicliqueCover; pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_path::HamiltonianPath; @@ -85,6 +88,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec BipartiteGraph { + BipartiteGraph::new( + 4, + 4, + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 1), + (1, 3), + (2, 0), + (2, 2), + (2, 3), + (3, 1), + ], + ) +} + +fn issue_instance_2_graph() -> BipartiteGraph { + BipartiteGraph::new( + 4, + 4, + vec![ + (0, 0), + (0, 1), + (0, 2), + (1, 0), + (1, 1), + (1, 2), + (2, 0), + (2, 1), + (2, 2), + (3, 0), + (3, 1), + (3, 3), + ], + ) +} + +fn issue_instance_2_witness() -> Vec { + vec![1, 1, 1, 0, 1, 1, 1, 0] +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_creation() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert_eq!(problem.left_size(), 4); + assert_eq!(problem.right_size(), 4); + assert_eq!(problem.num_vertices(), 8); + assert_eq!(problem.num_edges(), 10); + assert_eq!(problem.k(), 2); + assert_eq!(problem.dims(), vec![2; 8]); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_evaluation_yes_instance() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert!(problem.evaluate(&[1, 1, 0, 0, 1, 1, 0, 0])); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_evaluation_no_instance() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 3); + + assert!(!problem.evaluate(&[1, 1, 1, 0, 1, 1, 1, 0])); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_invalid_pairing() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert!(!problem.evaluate(&[1, 1, 0, 0, 1, 0, 1, 0])); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_edge_lookup() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert!(problem.has_selected_edge(0, 0)); + assert!(problem.has_selected_edge(1, 3)); + assert!(!problem.has_selected_edge(3, 3)); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_rejects_invalid_configs() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 2); + + assert!(!problem.evaluate(&[1, 1, 0, 0, 1, 1, 0])); + assert!(!problem.evaluate(&[1, 2, 0, 0, 1, 1, 0, 0])); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_solver_yes_instance() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); + let solver = BruteForce::new(); + + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); + + let all = solver.find_all_satisfying(&problem); + assert_eq!(all, vec![issue_instance_2_witness()]); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_solver_no_instance() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 3); + let solver = BruteForce::new(); + + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_serialization() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); + let witness = issue_instance_2_witness(); + + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: BalancedCompleteBipartiteSubgraph = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.left_size(), 4); + assert_eq!(deserialized.right_size(), 4); + assert_eq!(deserialized.num_edges(), 12); + assert_eq!( + deserialized.graph().left_edges(), + problem.graph().left_edges() + ); + assert_eq!(deserialized.k(), 3); + assert!(deserialized.evaluate(&witness)); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_is_valid_solution() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); + let yes_config = issue_instance_2_witness(); + let no_config = vec![1, 1, 0, 1, 1, 1, 0, 0]; + + assert!(problem.is_valid_solution(&yes_config)); + assert!(!problem.is_valid_solution(&no_config)); + assert!(!problem.is_valid_solution(&[1, 1, 1])); +} + +#[test] +fn test_balanced_complete_bipartite_subgraph_paper_example() { + let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); + let witness = issue_instance_2_witness(); + let solver = BruteForce::new(); + + assert!(problem.evaluate(&witness)); + assert_eq!(solver.find_all_satisfying(&problem), vec![witness]); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index eee5dc64..03885562 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -80,6 +80,13 @@ fn test_all_problems_implement_trait_correctly() { &BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1), "BicliqueCover", ); + check_problem_trait( + &BalancedCompleteBipartiteSubgraph::new( + BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0), (1, 1)]), + 2, + ), + "BalancedCompleteBipartiteSubgraph", + ); check_problem_trait(&Factoring::new(6, 2, 2), "Factoring"); let circuit = Circuit::new(vec![Assignment::new(