problemreductions/rules/ksatisfiability_bicliquecover.rs
1//! Reduction from KSatisfiability/K3 (3-SAT) to BicliqueCover.
2//!
3//! Implements the Chandran–Issac–Karrenbauer construction (IPEC 2016,
4//! Theorem 6 and Section 3). The reduction has two stages:
5//!
6//! 1. **Normalize** the source 3-CNF formula:
7//! - For each original variable `x_i`, create two normalized variables
8//! `t_i` and `f_i`. Replace literal `x_i` by `t_i`, replace literal
9//! `¬x_i` by `f_i`. Add exactly-one clauses
10//! `(t_i ∨ f_i ∨ f_i)` and `(¬t_i ∨ ¬f_i ∨ ¬f_i)` so that any
11//! satisfying assignment has `t_i = ¬f_i`.
12//! - Pad with dummy variable pairs until the total number of
13//! normalized variables `n` is a power of two `2^ell`.
14//! - Every satisfying normalized assignment has exactly `n/2` true
15//! variables.
16//!
17//! 2. **Build the gadget** `G = (U, V, E)` with edges partitioned into
18//! *important* and *free* edges:
19//! - Crown graph `H_n` on `{h_i^u}` ∪ `{h_i^v}` with `n(n-1)` important
20//! edges (omit the diagonal `h_i^u h_i^v`).
21//! - Clause induced matchings `P_i` of size 3 (one important edge per
22//! literal slot).
23//! - Domino gadgets `S_j` (`j ∈ [ell]`) with 7 important edges each.
24//! - Guard induced matching `Q` of size 2.
25//! - Important `H-S` cross-edges `s_j2^u h_i^v` and `s_j2^v h_i^u`.
26//! - Free edges between `H-S` (extreme rows), `P-P`, `P-Q`, `H-P`
27//! (literal-aware omissions), `S_1-P`.
28//! - Forcing matching `Y` of `k_f` parallel edges, each `y_r^u y_r^v`
29//! made bisimplicial with one free-edge biclique `B_r^f`.
30//!
31//! Set `k_f = 4·ell + 2·ceil(log2 m) + 6` and target rank
32//! `k = k_f + 2·ell + 2`.
33//!
34//! By Lemmas 16–19 of the paper, the BicliqueCover instance has rank `k`
35//! iff the (normalized) formula is satisfiable. Solution extraction
36//! identifies the biclique `B_1` covering `s_11^u s_11^v` and reads off
37//! `x_i = true` iff `h_i^u ∈ B_1` (after mapping `t_i, f_i` back to the
38//! source variables).
39//!
40//! See issue #1057 for the full construction; this file mirrors the
41//! issue body section-by-section.
42
43#[cfg(feature = "example-db")]
44use crate::models::formula::CNFClause;
45use crate::models::formula::KSatisfiability;
46use crate::models::graph::BicliqueCover;
47use crate::reduction;
48use crate::rules::traits::{ReduceTo, ReductionResult};
49use crate::topology::BipartiteGraph;
50use crate::variant::K3;
51use std::collections::BTreeSet;
52
53/// Result of reducing KSatisfiability/K3 to BicliqueCover.
54///
55/// Carries the normalization metadata needed for solution extraction:
56/// the source variable count, the number of normalized variables, and
57/// the offset of the `H` vertex block inside the bipartite gadget.
58#[derive(Debug, Clone)]
59pub struct ReductionKSatisfiabilityToBicliqueCover {
60 target: BicliqueCover,
61 /// Number of variables in the source 3-CNF formula.
62 source_num_vars: usize,
63 /// Number of normalized variables `n = 2^ell` (a power of two and
64 /// at least `2 * source_num_vars`).
65 normalized_n: usize,
66 /// Bipartite-local offset of the `S_1` block on the left side.
67 /// Used to locate vertex `s_11^u` for B_1 identification.
68 s1_left_offset: usize,
69 /// Bipartite-local offset of the `S_1` block on the right side.
70 /// Used to locate vertex `s_11^v` for B_1 identification.
71 s1_right_offset: usize,
72 /// Bipartite-local offset of the `Y` block on the left side.
73 /// Used to skip free-edge bicliques during extraction.
74 y_left_offset: usize,
75 /// Bipartite-local offset of the `Y` block on the right side.
76 y_right_offset: usize,
77 /// Number of free-edge bicliques `k_f`.
78 k_f: usize,
79}
80
81impl ReductionResult for ReductionKSatisfiabilityToBicliqueCover {
82 type Source = KSatisfiability<K3>;
83 type Target = BicliqueCover;
84
85 fn target_problem(&self) -> &Self::Target {
86 &self.target
87 }
88
89 /// Recover a source assignment from a BicliqueCover witness.
90 ///
91 /// 1. Ignore bicliques that cover the `Y` matching edges — these are
92 /// the `k_f` free-edge bicliques fixed by Lemma 17.
93 /// 2. Identify `B_1` as a biclique containing both `s_11^u` and
94 /// `s_11^v`, but no `y_r^u` or `y_r^v` (so it is an
95 /// important-edge biclique).
96 /// 3. For each normalized variable `i`, set the normalized
97 /// `t_i = true` iff `h_i^u ∈ B_1`.
98 /// 4. Map normalized variables back to source variables by reading
99 /// each original `t_i`.
100 ///
101 /// If no qualifying `B_1` is found (e.g. the witness is invalid),
102 /// the extracted assignment defaults to all-false.
103 fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
104 let n = self.normalized_n;
105 let left_size = self.target.left_size();
106 let k = self.target.k();
107
108 // Unified-vertex helpers for the named gadget anchors.
109 let s11_u = self.s1_left_offset; // s_{1,1}^u
110 let s11_v = left_size + self.s1_right_offset; // s_{1,1}^v
111 let h_left = |i: usize| -> usize { i }; // h_i^u (i in 0..n)
112 let y_left = |r: usize| -> usize { self.y_left_offset + r };
113 let y_right = |r: usize| -> usize { left_size + self.y_right_offset + r };
114
115 // Find a biclique containing both s_11^u and s_11^v, but no
116 // Y-matching vertex. By Lemma 17, free-edge bicliques touch the
117 // Y matching; the important-edge biclique B_1 does not.
118 let mut b1_index: Option<usize> = None;
119 for r in 0..k {
120 let in_b1 = |vertex: usize| -> bool {
121 target_solution.get(vertex * k + r).copied().unwrap_or(0) == 1
122 };
123 if !in_b1(s11_u) || !in_b1(s11_v) {
124 continue;
125 }
126 // Reject bicliques that touch the Y matching on either side.
127 let touches_y = (0..self.k_f).any(|r_y| in_b1(y_left(r_y)) || in_b1(y_right(r_y)));
128 if touches_y {
129 continue;
130 }
131 b1_index = Some(r);
132 break;
133 }
134
135 // Read off normalized assignment: t_i = (h_i^u in B_1) for i in 0..n.
136 let mut normalized_assignment = vec![false; n];
137 if let Some(r) = b1_index {
138 for (i, slot) in normalized_assignment.iter_mut().enumerate() {
139 *slot = target_solution.get(h_left(i) * k + r).copied().unwrap_or(0) == 1;
140 }
141 }
142
143 // Map normalized t_i back to the source: source x_s = t_s
144 // (with s in 1..=source_num_vars). t_s sits at normalized index
145 // 2 * (s - 1).
146 let mut source_assignment = vec![0usize; self.source_num_vars];
147 for (s, slot) in source_assignment.iter_mut().enumerate() {
148 let t_idx = 2 * s;
149 *slot = if normalized_assignment.get(t_idx).copied().unwrap_or(false) {
150 1
151 } else {
152 0
153 };
154 }
155 source_assignment
156 }
157}
158
159/// Smallest power of two greater than or equal to `n`. Returns at least
160/// `1` (so `next_power_of_two(0) == 1`).
161fn next_power_of_two_at_least(n: usize) -> usize {
162 let mut p = 1usize;
163 while p < n {
164 p *= 2;
165 }
166 p
167}
168
169/// `ceil(log2(m))` with the convention `ceil_log2(0) = ceil_log2(1) = 0`.
170fn ceil_log2(m: usize) -> usize {
171 if m <= 1 {
172 return 0;
173 }
174 let mut bits = 0usize;
175 let mut x = m - 1;
176 while x > 0 {
177 bits += 1;
178 x >>= 1;
179 }
180 bits
181}
182
183/// Build the normalized 3-CNF formula from a source formula.
184///
185/// Returns `(n, normalized_clauses)` where `n` is a power of two
186/// (the normalized variable count). Normalized clauses use signed
187/// integer literals with the convention:
188///
189/// - `t_i` is normalized variable `2*(i-1)` (0-indexed); 1-indexed literal `2i-1`.
190/// - `f_i` is normalized variable `2*(i-1) + 1` (0-indexed); 1-indexed literal `2i`.
191///
192/// For each source variable `i` in `1..=source_num_vars` and each padded
193/// dummy variable, two exactly-one clauses are appended.
194fn normalize(source: &KSatisfiability<K3>) -> (usize, Vec<Vec<i32>>) {
195 let s = source.num_vars();
196 // Padded source-variable count `s_pad` so that `2 * s_pad` is a
197 // power of two.
198 let s_pad = next_power_of_two_at_least(s.max(1));
199 let n = 2 * s_pad;
200
201 let t_lit = |i_one_indexed: usize| -> i32 { (2 * i_one_indexed - 1) as i32 };
202 let f_lit = |i_one_indexed: usize| -> i32 { (2 * i_one_indexed) as i32 };
203
204 let mut clauses: Vec<Vec<i32>> = Vec::new();
205
206 // 1. Translate source clauses: x_i -> t_i, ¬x_i -> f_i.
207 // Both replacements use positive normalized literals; the
208 // exactly-one clauses below tie t_i and f_i to opposite truth
209 // values in any satisfying assignment.
210 for clause in source.clauses() {
211 let mut translated: Vec<i32> = Vec::with_capacity(clause.literals.len());
212 for &lit in &clause.literals {
213 let var = lit.unsigned_abs() as usize; // 1-indexed source var
214 if lit > 0 {
215 translated.push(t_lit(var));
216 } else {
217 translated.push(f_lit(var));
218 }
219 }
220 clauses.push(translated);
221 }
222
223 // 2. Exactly-one clauses for each (real or dummy) normalized pair.
224 // (t_i ∨ f_i ∨ f_i) and (¬t_i ∨ ¬f_i ∨ ¬f_i).
225 for i in 1..=s_pad {
226 let t = t_lit(i);
227 let f = f_lit(i);
228 clauses.push(vec![t, f, f]);
229 clauses.push(vec![-t, -f, -f]);
230 }
231
232 (n, clauses)
233}
234
235/// Compute `k_f = 4*ell + 2*ceil(log2 m) + 6` for the normalized formula.
236fn free_edge_budget(ell: usize, m: usize) -> usize {
237 4 * ell + 2 * ceil_log2(m) + 6
238}
239
240// Overhead expressions are upper bounds in terms of source counts.
241// After normalization, `n ≤ 4·num_vars` (next power of two of `2·num_vars`)
242// and `m ≤ num_clauses + n ≤ num_clauses + 4·num_vars`. With
243// `ell = log2 n ≤ 2 + log2(num_vars)` we use the coarser bound
244// `ell ≤ num_vars` and `ceil(log2 m) ≤ num_clauses + 4·num_vars`,
245// giving the polynomial bounds below. Edges are bounded by
246// `partition_size^2` which is `O((num_vars + num_clauses)^2)`.
247#[reduction(
248 overhead = {
249 num_vertices = "32 * num_vars + 24 * num_clauses + 100",
250 num_edges = "(32 * num_vars + 24 * num_clauses + 100) * (32 * num_vars + 24 * num_clauses + 100)",
251 rank = "10 * num_vars + 4 * num_clauses + 20",
252 }
253)]
254impl ReduceTo<BicliqueCover> for KSatisfiability<K3> {
255 type Result = ReductionKSatisfiabilityToBicliqueCover;
256
257 fn reduce_to(&self) -> Self::Result {
258 // ---------------- Stage 1: normalize ----------------
259 let source_num_vars = self.num_vars();
260 let (n, normalized_clauses) = normalize(self);
261 let ell = ceil_log2(n).max(1); // n = 2^ell; ell >= 1
262 let m = normalized_clauses.len();
263 let k_f = free_edge_budget(ell, m);
264 let rank = k_f + 2 * ell + 2;
265
266 // ---------------- Stage 2: assemble vertex layout ----------------
267 // Bipartite-local block offsets (same on left and right partitions).
268 let h_offset = 0usize;
269 let p_offset = h_offset + n;
270 let s_offset = p_offset + 3 * m;
271 let q_offset = s_offset + 3 * ell;
272 let y_offset = q_offset + 2;
273 let partition_size = y_offset + k_f;
274
275 // Coordinate helpers (bipartite-local).
276 let h_left = |i: usize| -> usize { h_offset + i };
277 let h_right = |i: usize| -> usize { h_offset + i };
278 // P_i has rows a in {0,1,2}; i in 0..m.
279 let p_left = |i: usize, a: usize| -> usize { p_offset + 3 * i + a };
280 let p_right = |i: usize, a: usize| -> usize { p_offset + 3 * i + a };
281 // S_j has rows a in {0,1,2}; j in 0..ell.
282 let s_left = |j: usize, a: usize| -> usize { s_offset + 3 * j + a };
283 let s_right = |j: usize, a: usize| -> usize { s_offset + 3 * j + a };
284 // Q has rows t in {0,1}.
285 let q_left = |t: usize| -> usize { q_offset + t };
286 let q_right = |t: usize| -> usize { q_offset + t };
287 // Y has rows r in 0..k_f.
288 let y_left = |r: usize| -> usize { y_offset + r };
289 let y_right = |r: usize| -> usize { y_offset + r };
290
291 // Edge list (bipartite-local).
292 let mut edges: BTreeSet<(usize, usize)> = BTreeSet::new();
293 let mut add_edge = |u: usize, v: usize| {
294 edges.insert((u, v));
295 };
296
297 // ---------------- Important edges ----------------
298 // 3. Crown H_n: h_i^u h_j^v for all i != j.
299 for i in 0..n {
300 for j in 0..n {
301 if i != j {
302 add_edge(h_left(i), h_right(j));
303 }
304 }
305 }
306
307 // 4. Clause matchings P_i: p_{i,a}^u p_{i,a}^v for a in {0,1,2}.
308 for i in 0..m {
309 for a in 0..3 {
310 add_edge(p_left(i, a), p_right(i, a));
311 }
312 }
313
314 // 5. Domino gadgets S_j: 7 important edges per domino.
315 // (s1,s1), (s1,s2), (s2,s1), (s2,s2), (s2,s3), (s3,s2), (s3,s3)
316 // using 0-indexed rows.
317 let domino_pattern = [(0, 0), (0, 1), (1, 0), (1, 1), (1, 2), (2, 1), (2, 2)];
318 for j in 0..ell {
319 for &(a, b) in &domino_pattern {
320 add_edge(s_left(j, a), s_right(j, b));
321 }
322 }
323
324 // 6. Guard Q: q_t^u q_t^v for t in {0,1}.
325 for t in 0..2 {
326 add_edge(q_left(t), q_right(t));
327 }
328
329 // 7. Important H-S cross edges: s_{j,1}^u h_i^v and s_{j,1}^v h_i^u
330 // for all j in [ell], i in [n] (using 0-indexed rows; row 1 is s_j2).
331 for j in 0..ell {
332 for i in 0..n {
333 add_edge(s_left(j, 1), h_right(i));
334 add_edge(h_left(i), s_right(j, 1));
335 }
336 }
337
338 // ---------------- Free edges ----------------
339 // 8. Free H-S: s_{j,0}^u h_i^v, s_{j,2}^u h_i^v, h_i^u s_{j,0}^v,
340 // h_i^u s_{j,2}^v.
341 for j in 0..ell {
342 for i in 0..n {
343 add_edge(s_left(j, 0), h_right(i));
344 add_edge(s_left(j, 2), h_right(i));
345 add_edge(h_left(i), s_right(j, 0));
346 add_edge(h_left(i), s_right(j, 2));
347 }
348 }
349
350 // 9. Free P-P: U(P_i) x V(P_j) for all i != j.
351 for i in 0..m {
352 for j in 0..m {
353 if i == j {
354 continue;
355 }
356 for a in 0..3 {
357 for b in 0..3 {
358 add_edge(p_left(i, a), p_right(j, b));
359 }
360 }
361 }
362 }
363
364 // 10. Free P-Q: U(Q) x V(P_i) and U(P_i) x V(Q) for all i.
365 for i in 0..m {
366 for a in 0..3 {
367 for t in 0..2 {
368 add_edge(q_left(t), p_right(i, a));
369 add_edge(p_left(i, a), q_right(t));
370 }
371 }
372 }
373
374 // 11. Free H-P: for each literal edge in P_i:
375 // - add p_{i,a}^u h_j^v unless C_i^a is positive literal x_j
376 // - add p_{i,a}^v h_j^u unless C_i^a is negative literal ¬x_j
377 // (1-indexed literal lit -> normalized var index var = |lit|;
378 // 0-indexed var_idx = var - 1.)
379 for (i, clause) in normalized_clauses.iter().enumerate() {
380 for (a, &lit) in clause.iter().enumerate() {
381 let var_one_indexed = lit.unsigned_abs() as usize;
382 let var_zero_indexed = var_one_indexed - 1;
383 let is_positive = lit > 0;
384 for j in 0..n {
385 // p_{i,a}^u → h_j^v unless literal is +x_{j+1}.
386 if !is_positive || j != var_zero_indexed {
387 add_edge(p_left(i, a), h_right(j));
388 }
389 // p_{i,a}^v ← h_j^u unless literal is -x_{j+1}.
390 if is_positive || j != var_zero_indexed {
391 add_edge(h_left(j), p_right(i, a));
392 }
393 }
394 }
395 }
396
397 // 12. Free S_1-P: {s_{1,0}^u, s_{1,1}^u} connect to all V(P_i),
398 // {s_{1,0}^v, s_{1,1}^v} connect to all U(P_i).
399 for i in 0..m {
400 for a in 0..3 {
401 add_edge(s_left(0, 0), p_right(i, a));
402 add_edge(s_left(0, 1), p_right(i, a));
403 add_edge(p_left(i, a), s_right(0, 0));
404 add_edge(p_left(i, a), s_right(0, 1));
405 }
406 }
407
408 // 13. Y matching edges and bisimplicial connections.
409 //
410 // Lemma 16 lists `k_f` free-edge bicliques. To make the
411 // paper's accounting check at the construction site, we
412 // enumerate one canonical set of free-edge bicliques B_r^f
413 // and add bisimplicial edges (y_r^u, V(B_r^f)) and
414 // (U(B_r^f), y_r^v). The exact membership of each B_r^f is
415 // irrelevant for soundness — any choice of `k_f` bicliques
416 // that jointly cover the non-Y free edges works.
417 let free_bicliques = enumerate_free_bicliques(
418 n,
419 m,
420 ell,
421 &normalized_clauses,
422 &h_left,
423 &h_right,
424 &p_left,
425 &p_right,
426 &s_left,
427 &s_right,
428 &q_left,
429 &q_right,
430 );
431 debug_assert_eq!(
432 free_bicliques.len(),
433 k_f,
434 "free-edge biclique enumeration must match k_f"
435 );
436 for (r, biclique) in free_bicliques.iter().enumerate() {
437 let yu = y_left(r);
438 let yv = y_right(r);
439 add_edge(yu, yv);
440 for &v_right in &biclique.right {
441 add_edge(yu, v_right);
442 }
443 for &u_left in &biclique.left {
444 add_edge(u_left, yv);
445 }
446 }
447
448 // ---------------- Assemble target ----------------
449 let edges_vec: Vec<(usize, usize)> = edges.into_iter().collect();
450 let bipartite = BipartiteGraph::new(partition_size, partition_size, edges_vec);
451 let target = BicliqueCover::new(bipartite, rank);
452
453 ReductionKSatisfiabilityToBicliqueCover {
454 target,
455 source_num_vars,
456 normalized_n: n,
457 s1_left_offset: s_offset,
458 s1_right_offset: s_offset,
459 y_left_offset: y_offset,
460 y_right_offset: y_offset,
461 k_f,
462 }
463 }
464}
465
466/// A free-edge biclique listed by Lemma 16 of the paper. Vertices are
467/// expressed in bipartite-local indices.
468#[derive(Debug, Default)]
469struct FreeBiclique {
470 left: Vec<usize>,
471 right: Vec<usize>,
472}
473
474/// Enumerate the `k_f = 4*ell + 2*ceil(log2 m) + 6` free-edge bicliques
475/// from Lemma 16, in the following order:
476///
477/// - 2 H–S bicliques.
478/// - `2*ceil(log2 m)` P–P bicliques (binary-encoded clause indices).
479/// - 2 P–Q bicliques.
480/// - `4*ell` H–P bicliques (bit-wise selection over variable indices).
481/// - 2 S_1–P bicliques.
482///
483/// The exact biclique sets are unused for solution extraction — only
484/// the count matters at construction time. Membership is provided so
485/// the Y bisimplicial wiring is well-defined.
486#[allow(clippy::too_many_arguments, clippy::type_complexity)]
487fn enumerate_free_bicliques(
488 n: usize,
489 m: usize,
490 ell: usize,
491 normalized_clauses: &[Vec<i32>],
492 h_left: &dyn Fn(usize) -> usize,
493 h_right: &dyn Fn(usize) -> usize,
494 p_left: &dyn Fn(usize, usize) -> usize,
495 p_right: &dyn Fn(usize, usize) -> usize,
496 s_left: &dyn Fn(usize, usize) -> usize,
497 s_right: &dyn Fn(usize, usize) -> usize,
498 q_left: &dyn Fn(usize) -> usize,
499 q_right: &dyn Fn(usize) -> usize,
500) -> Vec<FreeBiclique> {
501 let mut out: Vec<FreeBiclique> = Vec::new();
502
503 // (a) H–S: 2 bicliques.
504 // B1 = (∪_j {s_{j,0}^u, s_{j,2}^u}, {h_i^v : i in [n]})
505 // B2 = ({h_i^u : i in [n]}, ∪_j {s_{j,0}^v, s_{j,2}^v})
506 {
507 let mut b = FreeBiclique::default();
508 for j in 0..ell {
509 b.left.push(s_left(j, 0));
510 b.left.push(s_left(j, 2));
511 }
512 for i in 0..n {
513 b.right.push(h_right(i));
514 }
515 out.push(b);
516 }
517 {
518 let mut b = FreeBiclique::default();
519 for i in 0..n {
520 b.left.push(h_left(i));
521 }
522 for j in 0..ell {
523 b.right.push(s_right(j, 0));
524 b.right.push(s_right(j, 2));
525 }
526 out.push(b);
527 }
528
529 // (b) P–P: 2 * ceil(log2 m) bicliques.
530 // For each bit b in 0..ceil_log2_m:
531 // B_b^+ = (∪_{i : bit_b(i)=1} U(P_i), ∪_{j : bit_b(j)=0} V(P_j))
532 // B_b^- = (∪_{i : bit_b(i)=0} U(P_i), ∪_{j : bit_b(j)=1} V(P_j))
533 // Each pair covers all (U(P_i), V(P_j)) with i != j.
534 let bits_m = ceil_log2(m);
535 for bit in 0..bits_m {
536 for invert in [false, true] {
537 let mut b = FreeBiclique::default();
538 for i in 0..m {
539 let has_bit = (i >> bit) & 1 == 1;
540 if has_bit != invert {
541 for a in 0..3 {
542 b.left.push(p_left(i, a));
543 }
544 }
545 }
546 for j in 0..m {
547 let has_bit = (j >> bit) & 1 == 1;
548 if has_bit == invert {
549 for a in 0..3 {
550 b.right.push(p_right(j, a));
551 }
552 }
553 }
554 out.push(b);
555 }
556 }
557
558 // (c) P–Q: 2 bicliques.
559 {
560 let mut b = FreeBiclique::default();
561 for t in 0..2 {
562 b.left.push(q_left(t));
563 }
564 for i in 0..m {
565 for a in 0..3 {
566 b.right.push(p_right(i, a));
567 }
568 }
569 out.push(b);
570 }
571 {
572 let mut b = FreeBiclique::default();
573 for i in 0..m {
574 for a in 0..3 {
575 b.left.push(p_left(i, a));
576 }
577 }
578 for t in 0..2 {
579 b.right.push(q_right(t));
580 }
581 out.push(b);
582 }
583
584 // (d) H–P: 4 * ell bicliques. For each bit b in 0..ell and each
585 // invert in {false, true}, two bicliques (one "left to right",
586 // one "right to left"). Each covers all (p^u, h^v) and
587 // (h^u, p^v) pairs whose variable index differs at bit b from
588 // the literal's omitted variable.
589 //
590 // B_b^{u, invert} = ({p_{i,a}^u : positive lit -> var_idx has bit b != invert,
591 // or negative lit (no constraint here, always include)},
592 // {h_j^v : bit_b(j) = invert})
593 // B_b^{v, invert} = ({h_j^u : bit_b(j) = invert},
594 // {p_{i,a}^v : negative lit -> var_idx has bit b != invert,
595 // or positive lit (no constraint)})
596 //
597 // Because we cannot include (p_{i,a}^u, h_{var_idx}^v) for a
598 // positive literal, the biclique excludes that p vertex on
599 // the matching bit. The union over the 2*ell bicliques (one
600 // per (bit, invert)) covers all required (p^u, h^v) free edges.
601 for bit in 0..ell {
602 for invert in [false, true] {
603 // B_b^{u, invert}
604 let mut b_u = FreeBiclique::default();
605 for j in 0..n {
606 let has_bit = (j >> bit) & 1 == 1;
607 if has_bit == invert {
608 b_u.right.push(h_right(j));
609 }
610 }
611 for (i, clause) in normalized_clauses.iter().enumerate() {
612 for (a, &lit) in clause.iter().enumerate() {
613 let var_idx = lit.unsigned_abs() as usize - 1;
614 let is_positive = lit > 0;
615 // p_{i,a}^u h_j^v omitted only when positive literal
616 // hits j == var_idx. Include p_{i,a}^u in B_u iff
617 // for every j in this biclique's right side
618 // (bit_bit(j) == invert), edge exists. That happens
619 // iff *not* (is_positive && bit_bit(var_idx) == invert).
620 let var_bit_matches = ((var_idx >> bit) & 1 == 1) == invert;
621 let include = !(is_positive && var_bit_matches);
622 if include {
623 b_u.left.push(p_left(i, a));
624 }
625 }
626 }
627 out.push(b_u);
628
629 // B_b^{v, invert}
630 let mut b_v = FreeBiclique::default();
631 for j in 0..n {
632 let has_bit = (j >> bit) & 1 == 1;
633 if has_bit == invert {
634 b_v.left.push(h_left(j));
635 }
636 }
637 for (i, clause) in normalized_clauses.iter().enumerate() {
638 for (a, &lit) in clause.iter().enumerate() {
639 let var_idx = lit.unsigned_abs() as usize - 1;
640 let is_positive = lit > 0;
641 let var_bit_matches = ((var_idx >> bit) & 1 == 1) == invert;
642 let include = is_positive || !var_bit_matches;
643 if include {
644 b_v.right.push(p_right(i, a));
645 }
646 }
647 }
648 out.push(b_v);
649 }
650 }
651
652 // (e) S_1–P: 2 bicliques.
653 {
654 let mut b = FreeBiclique::default();
655 b.left.push(s_left(0, 0));
656 b.left.push(s_left(0, 1));
657 for i in 0..m {
658 for a in 0..3 {
659 b.right.push(p_right(i, a));
660 }
661 }
662 out.push(b);
663 }
664 {
665 let mut b = FreeBiclique::default();
666 for i in 0..m {
667 for a in 0..3 {
668 b.left.push(p_left(i, a));
669 }
670 }
671 b.right.push(s_right(0, 0));
672 b.right.push(s_right(0, 1));
673 out.push(b);
674 }
675
676 out
677}
678
679/// Build a forward witness for the smallest canonical case: 1 source
680/// variable, 1 source clause. After normalization the formula has
681/// `n = 2`, `ell = 1`, `m = 3` clauses, `k_f = 14`, and rank `= 18`.
682///
683/// The witness is a vertex-major BicliqueCover configuration with
684/// `4` important-edge bicliques (`B_1`, `B̄_1`, `B_1^g`, `B_2^g`)
685/// followed by `14` free-edge bicliques `B_r^f ∪ {y_r^u, y_r^v}`
686/// that each absorb the matching edge `y_r^u y_r^v` and the
687/// bisimplicial wiring around it.
688///
689/// The construction follows the paper section by section:
690///
691/// - Assignment `t_1 = true` (`h_0^u ∈ B_1`, `h_1^v ∈ B_1`);
692/// `f_1 = false` (`h_1^u ∈ B̄_1`, `h_0^v ∈ B̄_1`).
693/// - Selected satisfied literal per clause: slot 0 for `C_0`, slot 0
694/// for `C_1`, slot 1 for `C_2`. The remaining two literal edges
695/// per clause are absorbed into the two guard bicliques.
696/// - The single domino `S_0` is covered by the duplex pair
697/// `B_1 = ({s_{0,0}^u, s_{0,1}^u}, {s_{0,0}^v, s_{0,1}^v})` and
698/// `B̄_1 = ({s_{0,1}^u, s_{0,2}^u}, {s_{0,1}^v, s_{0,2}^v})`.
699/// - `B_1` additionally absorbs the selected literal edges and the
700/// crown edges of the satisfying assignment.
701/// - The two guard bicliques each cover one `Q` edge and one of the
702/// two non-selected literal edges per clause; cross-pairs are P-P
703/// and P-Q free edges.
704#[cfg(feature = "example-db")]
705fn forward_witness_single_variable_single_clause(source: &KSatisfiability<K3>) -> Vec<usize> {
706 use crate::traits::Problem;
707
708 let reduction = ReduceTo::<BicliqueCover>::reduce_to(source);
709 let target = reduction.target_problem();
710 let k = target.k();
711 let left_size = target.left_size();
712 let num_vertices = target.num_vertices();
713 let mut config = vec![0usize; num_vertices * k];
714 let _ = target.dims(); // ensure dims matches num_vertices * k
715
716 // Bipartite-local helpers, mirroring `reduce_to`.
717 let n = reduction.normalized_n;
718 let ell = ceil_log2(n).max(1);
719 let m = 3usize; // hard-coded for the canonical case
720 let k_f = free_edge_budget(ell, m);
721 let h_offset = 0usize;
722 let p_offset = h_offset + n;
723 let s_offset = p_offset + 3 * m;
724 let q_offset = s_offset + 3 * ell;
725 let y_offset = q_offset + 2;
726
727 // Unified-vertex coordinates.
728 let h_left = |i: usize| h_offset + i;
729 let h_right_u = |i: usize| left_size + h_offset + i;
730 let p_left = |i: usize, a: usize| p_offset + 3 * i + a;
731 let p_right_u = |i: usize, a: usize| left_size + p_offset + 3 * i + a;
732 let s_left = |j: usize, a: usize| s_offset + 3 * j + a;
733 let s_right_u = |j: usize, a: usize| left_size + s_offset + 3 * j + a;
734 let q_left = |t: usize| q_offset + t;
735 let q_right_u = |t: usize| left_size + q_offset + t;
736 let y_left = |r: usize| y_offset + r;
737 let y_right_u = |r: usize| left_size + y_offset + r;
738
739 let mark = |cfg: &mut [usize], vertex: usize, biclique: usize| {
740 cfg[vertex * k + biclique] = 1;
741 };
742
743 // Biclique 0: B_1 — important.
744 // Left: {h_0^u, s_{0,0}^u, s_{0,1}^u, p_{0,0}^u, p_{1,0}^u, p_{2,1}^u}.
745 // Right: {h_1^v, s_{0,0}^v, s_{0,1}^v, p_{0,0}^v, p_{1,0}^v, p_{2,1}^v}.
746 for v in [
747 h_left(0),
748 s_left(0, 0),
749 s_left(0, 1),
750 p_left(0, 0),
751 p_left(1, 0),
752 p_left(2, 1),
753 ] {
754 mark(&mut config, v, 0);
755 }
756 for v in [
757 h_right_u(1),
758 s_right_u(0, 0),
759 s_right_u(0, 1),
760 p_right_u(0, 0),
761 p_right_u(1, 0),
762 p_right_u(2, 1),
763 ] {
764 mark(&mut config, v, 0);
765 }
766
767 // Biclique 1: B̄_1 — important.
768 // Left: {h_1^u, s_{0,1}^u, s_{0,2}^u}; Right: {h_0^v, s_{0,1}^v, s_{0,2}^v}.
769 for v in [h_left(1), s_left(0, 1), s_left(0, 2)] {
770 mark(&mut config, v, 1);
771 }
772 for v in [h_right_u(0), s_right_u(0, 1), s_right_u(0, 2)] {
773 mark(&mut config, v, 1);
774 }
775
776 // Biclique 2: B_1^g — guard #1. Covers q_0 + non-selected slot for
777 // each clause.
778 // C_0 non-selected slot for B_1^g: 1; C_1: 1; C_2: 0.
779 for v in [q_left(0), p_left(0, 1), p_left(1, 1), p_left(2, 0)] {
780 mark(&mut config, v, 2);
781 }
782 for v in [
783 q_right_u(0),
784 p_right_u(0, 1),
785 p_right_u(1, 1),
786 p_right_u(2, 0),
787 ] {
788 mark(&mut config, v, 2);
789 }
790
791 // Biclique 3: B_2^g — guard #2. Covers q_1 + last non-selected slot.
792 // C_0 leftover slot: 2; C_1: 2; C_2: 2.
793 for v in [q_left(1), p_left(0, 2), p_left(1, 2), p_left(2, 2)] {
794 mark(&mut config, v, 3);
795 }
796 for v in [
797 q_right_u(1),
798 p_right_u(0, 2),
799 p_right_u(1, 2),
800 p_right_u(2, 2),
801 ] {
802 mark(&mut config, v, 3);
803 }
804
805 // Bicliques 4..(4+k_f): free-edge bicliques B_r^f ∪ {y_r^u, y_r^v}.
806 let (_, normalized_clauses) = normalize(source);
807 let free = enumerate_free_bicliques(
808 n,
809 m,
810 ell,
811 &normalized_clauses,
812 &|i| h_offset + i,
813 &|i| h_offset + i,
814 &|i, a| p_offset + 3 * i + a,
815 &|i, a| p_offset + 3 * i + a,
816 &|j, a| s_offset + 3 * j + a,
817 &|j, a| s_offset + 3 * j + a,
818 &|t| q_offset + t,
819 &|t| q_offset + t,
820 );
821 assert_eq!(free.len(), k_f);
822 for (r, biclique) in free.iter().enumerate() {
823 let slot = 4 + r;
824 // Left: U(B_r^f) ∪ {y_r^u}.
825 for &lv in &biclique.left {
826 mark(&mut config, lv, slot);
827 }
828 mark(&mut config, y_left(r), slot);
829 // Right: V(B_r^f) ∪ {y_r^v}.
830 for &rv in &biclique.right {
831 mark(&mut config, left_size + rv, slot);
832 }
833 mark(&mut config, y_right_u(r), slot);
834 }
835
836 config
837}
838
839/// Canonical example for the KSatisfiability/K3 → BicliqueCover rule.
840///
841/// Uses the smallest possible source: one variable `x_1` and one clause
842/// `(x_1 ∨ x_1 ∨ x_1)`. After normalization the gadget has rank `18`
843/// and ~`1188` binary variables, so solving the target by brute force
844/// is out of reach. The witness is constructed by hand, following the
845/// paper's Lemma 16/17 free-edge decomposition and a direct
846/// `(B_1, B̄_1)` duplex on the single domino `S_0`.
847#[cfg(feature = "example-db")]
848pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
849 use crate::export::SolutionPair;
850
851 vec![crate::example_db::specs::RuleExampleSpec {
852 id: "ksatisfiability_to_bicliquecover",
853 build: || {
854 let source = KSatisfiability::<K3>::new(1, vec![CNFClause::new(vec![1, 1, 1])]);
855 let target_config = forward_witness_single_variable_single_clause(&source);
856 crate::example_db::specs::rule_example_with_witness::<_, BicliqueCover>(
857 source,
858 SolutionPair {
859 source_config: vec![1usize], // x_1 = true
860 target_config,
861 },
862 )
863 },
864 }]
865}
866
867#[cfg(test)]
868#[path = "../unit_tests/rules/ksatisfiability_bicliquecover.rs"]
869mod tests;