Skip to main content

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;