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