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;