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