use std::collections::HashMap;
fn truth_table(n: usize, order: &[usize], f: &dyn Fn(&[bool]) -> bool) -> Vec<u8> {
let mut tt = vec![0u8; 1usize << n];
let mut assign = vec![false; n];
for (idx, slot) in tt.iter_mut().enumerate() {
for (k, &v) in order.iter().enumerate() {
assign[v] = (idx >> (n - 1 - k)) & 1 == 1;
}
*slot = u8::from(f(&assign));
}
tt
}
fn robdd_size(tt: &[u8], n: usize) -> usize {
let mut unique: HashMap<(usize, usize, usize), usize> = HashMap::new();
let mut memo: HashMap<Vec<u8>, usize> = HashMap::new();
let mut next = 2usize; go(tt, n, &mut next, &mut unique, &mut memo);
return unique.len();
fn go(
tt: &[u8],
n: usize,
next: &mut usize,
unique: &mut HashMap<(usize, usize, usize), usize>,
memo: &mut HashMap<Vec<u8>, usize>,
) -> usize {
if tt.iter().all(|&b| b == 0) {
return 0;
}
if tt.iter().all(|&b| b == 1) {
return 1;
}
if let Some(&id) = memo.get(tt) {
return id;
}
let half = tt.len() / 2;
let lo = go(&tt[..half], n, next, unique, memo);
let hi = go(&tt[half..], n, next, unique, memo);
let level = n - tt.len().trailing_zeros() as usize;
let id = if lo == hi {
lo } else {
*unique.entry((level, lo, hi)).or_insert_with(|| {
let i = *next;
*next += 1;
i
})
};
memo.insert(tt.to_vec(), id);
id
}
}
fn binary_walk(tt: &[u8], n: usize, order: &[usize]) -> Option<Vec<bool>> {
if tt.iter().all(|&b| b == 0) {
return None;
}
let mut cur: &[u8] = tt;
let mut assign = vec![false; n];
for k in 0..n {
let half = cur.len() / 2;
let (lo, hi) = cur.split_at(half);
if hi.iter().any(|&b| b == 1) {
assign[order[k]] = true;
cur = hi;
} else {
assign[order[k]] = false;
cur = lo;
}
}
Some(assign)
}
fn parity(a: &[bool]) -> bool {
a.iter().filter(|&&b| b).count() % 2 == 1
}
fn interleaved(a: &[bool]) -> bool {
(0..a.len() / 2).any(|i| a[2 * i] && a[2 * i + 1])
}
fn structureless(a: &[bool]) -> bool {
let mut z = a.iter().enumerate().fold(0u64, |acc, (i, &b)| acc ^ ((b as u64) << (i % 64)));
z = z.wrapping_add(0x9E3779B97F4A7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
(z ^ (z >> 31)) & 1 == 1
}
fn majority(a: &[bool]) -> bool {
a.iter().filter(|&&b| b).count() * 2 >= a.len()
}
fn at_most_one(a: &[bool]) -> bool {
a.iter().filter(|&&b| b).count() <= 1
}
fn random_3sat(n: usize, seed: u64) -> impl Fn(&[bool]) -> bool {
let mut state = seed ^ 0x9E3779B97F4A7C15;
let mut next = move || {
state = state.wrapping_add(0x9E3779B97F4A7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
};
let m = (n as f64 * 1.5) as usize; let mut clauses: Vec<[(usize, bool); 3]> = Vec::with_capacity(m);
while clauses.len() < m {
let mut vs = [0usize; 3];
let mut k = 0;
while k < 3 {
let v = next() as usize % n;
if !vs[..k].contains(&v) {
vs[k] = v;
k += 1;
}
}
clauses.push([(vs[0], next() & 1 == 0), (vs[1], next() & 1 == 0), (vs[2], next() & 1 == 0)]);
}
move |a: &[bool]| clauses.iter().all(|c| c.iter().any(|&(v, pos)| a[v] == pos))
}
fn symmetry_reduced_size(n: usize, f: &dyn Fn(&[bool]) -> bool) -> Option<usize> {
let mut by_weight: Vec<Option<bool>> = vec![None; n + 1];
let mut assign = vec![false; n];
for idx in 0..(1usize << n) {
for (j, slot) in assign.iter_mut().enumerate() {
*slot = (idx >> j) & 1 == 1;
}
let w = assign.iter().filter(|&&b| b).count();
let v = f(&assign);
match by_weight[w] {
None => by_weight[w] = Some(v),
Some(prev) if prev != v => return None, _ => {}
}
}
let vals: Vec<bool> = by_weight.into_iter().map(|o| o.unwrap()).collect();
let transitions = vals.windows(2).filter(|w| w[0] != w[1]).count();
Some(transitions + 2) }
fn main() {
let evens_then_odds = |n: usize| -> Vec<usize> {
(0..n).step_by(2).chain((1..n).step_by(2)).collect()
};
let natural = |n: usize| -> Vec<usize> { (0..n).collect() };
println!("Arrangement size (OBDD nodes) — the cost of the sorted hypercube walk:\n");
println!(" n │ parity │ interleaved(good) │ interleaved(bad) │ structureless │ 2^n");
println!(" ───┼──────────┼───────────────────┼──────────────────┼───────────────┼────────");
for n in [4usize, 6, 8, 10, 12, 14, 16] {
let nat = natural(n);
let bad = evens_then_odds(n);
let par = robdd_size(&truth_table(n, &nat, &parity), n);
let il_good = robdd_size(&truth_table(n, &nat, &interleaved), n);
let il_bad = robdd_size(&truth_table(n, &bad, &interleaved), n);
let rnd = robdd_size(&truth_table(n, &nat, &structureless), n);
println!(
" {n:>3} │ {par:>8} │ {il_good:>17} │ {il_bad:>16} │ {rnd:>13} │ {:>7}",
1usize << n
);
}
let n = 8;
let order = natural(n);
let tt = truth_table(n, &order, &interleaved);
println!(
"\nBinary walk on interleaved(n={n}) [arrangement = {} nodes]:",
robdd_size(&tt, n)
);
if let Some(model) = binary_walk(&tt, n, &order) {
let bits: String = model.iter().map(|&b| if b { '1' } else { '0' }).collect();
println!(
" walked {n} steps → model {bits} (satisfies: {})",
interleaved(&model)
);
}
println!(
"\nReading: the walk is always n steps. The *arrangement* is linear for parity (any order) and\n\
for interleaved IN THE RIGHT ORDER — but exponential for interleaved in the wrong order, and\n\
near-maximal for the structureless function in EVERY order. So the sorted arrangement is real,\n\
and the binary walk is cheap — but a SMALL arrangement exists only when the function has\n\
structure the order can expose (finding the best order is NP-hard), and some functions have no\n\
small arrangement at all. That is the same wall the symmetry and degree views show."
);
println!("\n\nSatisfiable structured families — OBDD (order quotient) vs symmetry-reduced (2nd quotient):\n");
println!(" family │ n │ OBDD │ symmetric? │ reduced │ 2^n");
println!(" ───────────────────────┼─────┼──────┼────────────┼─────────┼────────");
for n in [6usize, 8, 10, 12, 14, 16] {
let nat = natural(n);
let families: [(&str, Box<dyn Fn(&[bool]) -> bool>); 4] = [
("majority (cardinality)", Box::new(majority)),
("at-most-one (PHP col) ", Box::new(at_most_one)),
("parity (GF(2) linear) ", Box::new(parity)),
("random 3-SAT (sat) ", Box::new(random_3sat(n, 0xBEEF ^ n as u64))),
];
for (name, f) in families.iter() {
let obdd = robdd_size(&truth_table(n, &nat, f.as_ref()), n);
let (sym, red) = match symmetry_reduced_size(n, f.as_ref()) {
Some(r) => ("yes", r.to_string()),
None => ("NO", "—".to_string()),
};
println!(" {name}│ {n:>3} │ {obdd:>4} │ {sym:>10} │ {red:>7} │ {:>7}", 1usize << n);
}
println!(" ───────────────────────┼─────┼──────┼────────────┼─────────┼────────");
}
println!(
"\nDoes it hold on 3-SAT? NO — and that row is the proof. The cardinality/covering families are\n\
weight-symmetric, so the SECOND quotient collapses their O(n²) OBDD to O(1) (majority → 3,\n\
at-most-one → 3); parity is already linear. But random 3-SAT is 'symmetric? NO' — its SAT\n\
region is not invariant under variable permutations, so there is nothing to quotient, and its\n\
OBDD does not collapse. The shift (order) + lift (symmetry) get EVERYTHING that HAS structure —\n\
which is exactly the families we crush — and get nothing on arbitrary 3-SAT, which is precisely\n\
the P≠NP boundary: if the symmetry quotient collapsed random 3-SAT too, that would be P=NP."
);
}