Skip to content

Commit accbb66

Browse files
authored
Fix #143: [Model] NAESatisfiability (#717)
* Add plan for #143: [Model] NAESatisfiability * Implement #143: [Model] NAESatisfiability * chore: remove plan file after implementation
1 parent fc1cdf8 commit accbb66

8 files changed

Lines changed: 399 additions & 3 deletions

File tree

docs/paper/reductions.typ

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"Knapsack": [Knapsack],
9393
"PartiallyOrderedKnapsack": [Partially Ordered Knapsack],
9494
"Satisfiability": [SAT],
95+
"NAESatisfiability": [NAE-SAT],
9596
"KSatisfiability": [$k$-SAT],
9697
"CircuitSAT": [CircuitSAT],
9798
"ConjunctiveQueryFoldability": [Conjunctive Query Foldability],
@@ -2396,6 +2397,29 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
23962397
]
23972398
}
23982399

2400+
#{
2401+
let x = load-model-example("NAESatisfiability")
2402+
let n = x.instance.num_vars
2403+
let m = x.instance.clauses.len()
2404+
let clauses = x.instance.clauses
2405+
let sol = (config: x.optimal_config, metric: x.optimal_value)
2406+
let assign = sol.config
2407+
let complement = assign.map(v => 1 - v)
2408+
let fmt-lit(l) = if l > 0 { $x_#l$ } else { $not x_#(-l)$ }
2409+
let fmt-clause(c) = $paren.l #c.literals.map(fmt-lit).join($or$) paren.r$
2410+
let eval-lit(l) = if l > 0 { assign.at(l - 1) } else { 1 - assign.at(-l - 1) }
2411+
let clause-values(c) = c.literals.map(l => str(eval-lit(l)))
2412+
[
2413+
#problem-def("NAESatisfiability")[
2414+
Given a CNF formula $phi = and.big_(j=1)^m C_j$ with $m$ clauses over $n$ Boolean variables, where each clause $C_j = or.big_i ell_(j i)$ is a disjunction of literals, find an assignment $bold(x) in {0, 1}^n$ such that every clause contains at least one true literal and at least one false literal.
2415+
][
2416+
Not-All-Equal Satisfiability (NAE-SAT) is a canonical variant in Schaefer's dichotomy theorem @schaefer1978. Unlike ordinary SAT, each clause forbids the all-true and all-false patterns, giving the problem a complement symmetry: if an assignment is NAE-satisfying, then flipping every bit is also NAE-satisfying. This makes NAE-SAT a natural intermediate for cut and partition reductions such as Max-Cut. A straightforward exact algorithm enumerates all $2^n$ assignments; complement symmetry can halve the search space in practice by fixing one variable, but the asymptotic worst-case bound remains $O^*(2^n)$.
2417+
2418+
*Example.* Consider $phi = #clauses.map(fmt-clause).join($and$)$ with $n = #n$ variables and $m = #m$ clauses. The assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#assign.map(v => str(v)).join(", "))$ is NAE-satisfying because each clause evaluates to a tuple containing both $0$ and $1$: #clauses.enumerate().map(((j, c)) => $C_#(j + 1) = paren.l #clause-values(c).join(", ") paren.r$).join(", "). The complementary assignment $(#range(n).map(i => $x_#(i + 1)$).join(",")) = (#complement.map(v => str(v)).join(", "))$ is therefore also NAE-satisfying, illustrating the paired-solution structure characteristic of NAE-SAT.
2419+
]
2420+
]
2421+
}
2422+
23992423
#{
24002424
let x = load-model-example("KSatisfiability")
24012425
let n = x.instance.num_vars

problemreductions-cli/src/cli.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@ Flags by problem type:
219219
MaxCut, MaxMatching, TSP --graph, --edge-weights
220220
ShortestWeightConstrainedPath --graph, --edge-lengths, --edge-weights, --source-vertex, --target-vertex, --length-bound, --weight-bound
221221
MaximalIS --graph, --weights
222-
SAT, KSAT --num-vars, --clauses [--k]
222+
SAT, NAESAT --num-vars, --clauses
223+
KSAT --num-vars, --clauses [--k]
223224
QUBO --matrix
224225
SpinGlass --graph, --couplings, --fields
225226
KColoring --graph, --k

problemreductions-cli/src/commands/create.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
413413
"--graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5"
414414
}
415415
"Satisfiability" => "--num-vars 3 --clauses \"1,2;-1,3\"",
416+
"NAESatisfiability" => "--num-vars 3 --clauses \"1,2,-3;-1,2,3\"",
416417
"QuantifiedBooleanFormulas" => {
417418
"--num-vars 3 --clauses \"1,2;-1,3\" --quantifiers \"E,A,E\""
418419
}
@@ -1389,6 +1390,19 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
13891390
resolved_variant.clone(),
13901391
)
13911392
}
1393+
"NAESatisfiability" => {
1394+
let num_vars = args.num_vars.ok_or_else(|| {
1395+
anyhow::anyhow!(
1396+
"NAESatisfiability requires --num-vars\n\n\
1397+
Usage: pred create NAESAT --num-vars 3 --clauses \"1,2,-3;-1,2,3\""
1398+
)
1399+
})?;
1400+
let clauses = parse_clauses(args)?;
1401+
(
1402+
ser(NAESatisfiability::try_new(num_vars, clauses).map_err(anyhow::Error::msg)?)?,
1403+
resolved_variant.clone(),
1404+
)
1405+
}
13921406
"KSatisfiability" => {
13931407
let num_vars = args.num_vars.ok_or_else(|| {
13941408
anyhow::anyhow!(

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ pub mod prelude {
4444
// Problem types
4545
pub use crate::models::algebraic::{QuadraticAssignment, BMF, QUBO};
4646
pub use crate::models::formula::{
47-
CNFClause, CircuitSAT, KSatisfiability, QuantifiedBooleanFormulas, Satisfiability,
47+
CNFClause, CircuitSAT, KSatisfiability, NAESatisfiability, QuantifiedBooleanFormulas,
48+
Satisfiability,
4849
};
4950
pub use crate::models::graph::{
5051
BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation,

src/models/formula/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,28 @@
22
//!
33
//! Problems whose input is a boolean formula or circuit:
44
//! - [`Satisfiability`]: Boolean satisfiability (SAT) with CNF clauses
5+
//! - [`NAESatisfiability`]: Not-All-Equal satisfiability with CNF clauses
56
//! - [`KSatisfiability`]: K-SAT where each clause has exactly K literals
67
//! - [`CircuitSAT`]: Boolean circuit satisfiability
78
//! - [`QuantifiedBooleanFormulas`]: Quantified Boolean Formulas (QBF) — PSPACE-complete
89
910
pub(crate) mod circuit;
1011
pub(crate) mod ksat;
12+
pub(crate) mod nae_satisfiability;
1113
pub(crate) mod qbf;
1214
pub(crate) mod sat;
1315

1416
pub use circuit::{Assignment, BooleanExpr, BooleanOp, Circuit, CircuitSAT};
1517
pub use ksat::KSatisfiability;
18+
pub use nae_satisfiability::NAESatisfiability;
1619
pub use qbf::{QuantifiedBooleanFormulas, Quantifier};
1720
pub use sat::{CNFClause, Satisfiability};
1821

1922
#[cfg(feature = "example-db")]
2023
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
2124
let mut specs = Vec::new();
2225
specs.extend(sat::canonical_model_example_specs());
26+
specs.extend(nae_satisfiability::canonical_model_example_specs());
2327
specs.extend(ksat::canonical_model_example_specs());
2428
specs.extend(circuit::canonical_model_example_specs());
2529
specs.extend(qbf::canonical_model_example_specs());
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
//! Not-All-Equal Boolean Satisfiability (NAE-SAT) problem implementation.
2+
//!
3+
//! NAE-SAT asks whether a CNF formula has an assignment such that each clause
4+
//! contains at least one true literal and at least one false literal.
5+
6+
use crate::registry::{FieldInfo, ProblemSchemaEntry};
7+
use crate::traits::{Problem, SatisfactionProblem};
8+
use serde::{Deserialize, Serialize};
9+
10+
use super::CNFClause;
11+
12+
inventory::submit! {
13+
ProblemSchemaEntry {
14+
name: "NAESatisfiability",
15+
display_name: "Not-All-Equal Satisfiability",
16+
aliases: &["NAESAT"],
17+
dimensions: &[],
18+
module_path: module_path!(),
19+
description: "Find an assignment where every CNF clause has both a true and a false literal",
20+
fields: &[
21+
FieldInfo { name: "num_vars", type_name: "usize", description: "Number of Boolean variables" },
22+
FieldInfo { name: "clauses", type_name: "Vec<CNFClause>", description: "Clauses in conjunctive normal form with at least two literals each" },
23+
],
24+
}
25+
}
26+
27+
/// Not-All-Equal Boolean Satisfiability (NAE-SAT) in CNF form.
28+
///
29+
/// Given a Boolean formula in conjunctive normal form (CNF), determine whether
30+
/// there exists an assignment such that every clause contains at least one
31+
/// true literal and at least one false literal.
32+
#[derive(Debug, Clone, Serialize, Deserialize)]
33+
#[serde(try_from = "NAESatisfiabilityDef")]
34+
pub struct NAESatisfiability {
35+
/// Number of variables.
36+
num_vars: usize,
37+
/// Clauses in CNF, each with at least two literals.
38+
clauses: Vec<CNFClause>,
39+
}
40+
41+
impl NAESatisfiability {
42+
/// Create a new NAE-SAT problem.
43+
///
44+
/// # Panics
45+
/// Panics if any clause has fewer than two literals.
46+
pub fn new(num_vars: usize, clauses: Vec<CNFClause>) -> Self {
47+
Self::try_new(num_vars, clauses).unwrap_or_else(|err| panic!("{err}"))
48+
}
49+
50+
/// Create a new NAE-SAT problem, returning an error instead of panicking
51+
/// when a clause has fewer than two literals.
52+
pub fn try_new(num_vars: usize, clauses: Vec<CNFClause>) -> Result<Self, String> {
53+
validate_clause_lengths(&clauses)?;
54+
Ok(Self { num_vars, clauses })
55+
}
56+
57+
/// Get the number of variables.
58+
pub fn num_vars(&self) -> usize {
59+
self.num_vars
60+
}
61+
62+
/// Get the number of clauses.
63+
pub fn num_clauses(&self) -> usize {
64+
self.clauses.len()
65+
}
66+
67+
/// Get the total number of literal occurrences across all clauses.
68+
pub fn num_literals(&self) -> usize {
69+
self.clauses.iter().map(|c| c.len()).sum()
70+
}
71+
72+
/// Get the clauses.
73+
pub fn clauses(&self) -> &[CNFClause] {
74+
&self.clauses
75+
}
76+
77+
/// Get a specific clause.
78+
pub fn get_clause(&self, index: usize) -> Option<&CNFClause> {
79+
self.clauses.get(index)
80+
}
81+
82+
/// Count how many clauses satisfy the NAE condition under an assignment.
83+
pub fn count_nae_satisfied(&self, assignment: &[bool]) -> usize {
84+
self.clauses
85+
.iter()
86+
.filter(|clause| Self::clause_is_nae_satisfied(clause, assignment))
87+
.count()
88+
}
89+
90+
/// Check whether all clauses satisfy the NAE condition under an assignment.
91+
pub fn is_nae_satisfying(&self, assignment: &[bool]) -> bool {
92+
self.clauses
93+
.iter()
94+
.all(|clause| Self::clause_is_nae_satisfied(clause, assignment))
95+
}
96+
97+
/// Check if a solution (config) is valid.
98+
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
99+
self.evaluate(config)
100+
}
101+
102+
fn config_to_assignment(config: &[usize]) -> Vec<bool> {
103+
config.iter().map(|&v| v == 1).collect()
104+
}
105+
106+
fn literal_value(lit: i32, assignment: &[bool]) -> bool {
107+
let var = lit.unsigned_abs() as usize - 1;
108+
let value = assignment.get(var).copied().unwrap_or(false);
109+
if lit > 0 {
110+
value
111+
} else {
112+
!value
113+
}
114+
}
115+
116+
fn clause_is_nae_satisfied(clause: &CNFClause, assignment: &[bool]) -> bool {
117+
let mut has_true = false;
118+
let mut has_false = false;
119+
120+
for &lit in &clause.literals {
121+
if Self::literal_value(lit, assignment) {
122+
has_true = true;
123+
} else {
124+
has_false = true;
125+
}
126+
127+
if has_true && has_false {
128+
return true;
129+
}
130+
}
131+
132+
false
133+
}
134+
}
135+
136+
impl Problem for NAESatisfiability {
137+
const NAME: &'static str = "NAESatisfiability";
138+
type Metric = bool;
139+
140+
fn dims(&self) -> Vec<usize> {
141+
vec![2; self.num_vars]
142+
}
143+
144+
fn evaluate(&self, config: &[usize]) -> bool {
145+
let assignment = Self::config_to_assignment(config);
146+
self.is_nae_satisfying(&assignment)
147+
}
148+
149+
fn variant() -> Vec<(&'static str, &'static str)> {
150+
crate::variant_params![]
151+
}
152+
}
153+
154+
impl SatisfactionProblem for NAESatisfiability {}
155+
156+
crate::declare_variants! {
157+
default sat NAESatisfiability => "2^num_variables",
158+
}
159+
160+
#[derive(Debug, Clone, Deserialize)]
161+
struct NAESatisfiabilityDef {
162+
num_vars: usize,
163+
clauses: Vec<CNFClause>,
164+
}
165+
166+
impl TryFrom<NAESatisfiabilityDef> for NAESatisfiability {
167+
type Error = String;
168+
169+
fn try_from(value: NAESatisfiabilityDef) -> Result<Self, Self::Error> {
170+
Self::try_new(value.num_vars, value.clauses)
171+
}
172+
}
173+
174+
fn validate_clause_lengths(clauses: &[CNFClause]) -> Result<(), String> {
175+
for (index, clause) in clauses.iter().enumerate() {
176+
if clause.len() < 2 {
177+
return Err(format!(
178+
"Clause {} has {} literals, expected at least 2",
179+
index,
180+
clause.len()
181+
));
182+
}
183+
}
184+
Ok(())
185+
}
186+
187+
#[cfg(feature = "example-db")]
188+
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
189+
vec![crate::example_db::specs::ModelExampleSpec {
190+
id: "nae_satisfiability",
191+
instance: Box::new(NAESatisfiability::new(
192+
5,
193+
vec![
194+
CNFClause::new(vec![1, 2, -3]),
195+
CNFClause::new(vec![-1, 3, 4]),
196+
CNFClause::new(vec![2, -4, 5]),
197+
CNFClause::new(vec![-2, 3, -5]),
198+
CNFClause::new(vec![1, -3, 5]),
199+
],
200+
)),
201+
optimal_config: vec![0, 0, 0, 1, 1],
202+
optimal_value: serde_json::json!(true),
203+
}]
204+
}
205+
206+
#[cfg(test)]
207+
#[path = "../../unit_tests/models/formula/nae_satisfiability.rs"]
208+
mod tests;

src/models/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ pub use algebraic::{
1414
QuadraticAssignment, BMF, ILP, QUBO,
1515
};
1616
pub use formula::{
17-
CNFClause, CircuitSAT, KSatisfiability, QuantifiedBooleanFormulas, Quantifier, Satisfiability,
17+
CNFClause, CircuitSAT, KSatisfiability, NAESatisfiability, QuantifiedBooleanFormulas,
18+
Quantifier, Satisfiability,
1819
};
1920
pub use graph::{
2021
BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation,

0 commit comments

Comments
 (0)