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;