Boolean Satisfiability Problem (SAT)

The Boolean satisfiability problem (SAT) is to determine whether there exists an assignment of truth values to Boolean variables that makes a given Boolean formula in conjunctive normal form (CNF) evaluate to True. A CNF formula is a conjunction (AND) of clauses, where each clause is a disjunction (OR) of literals. A literal is either a variable $x_i$ (positive literal) or its negation $\lnot x_i$ (negative literal).

For example, the following is a 3-SAT instance with 5 variables $x_0,x_1,x_2,x_3,x_4$ and 6 clauses:

\[(x_0 \lor x_1 \lor x_2) \land (\lnot x_0 \lor x_3 \lor x_4) \land (x_1 \lor \lnot x_2 \lor \lnot x_3) \land (\lnot x_1 \lor \lnot x_3 \lor x_4) \land (\lnot x_0 \lor \lnot x_1 \lor \lnot x_2) \land (x_0 \lor x_1 \lor \lnot x_4)\]

A satisfying assignment must make every clause True, i.e., at least one literal in each clause must be True.

HUBO Formulation

We use the convention True = 0 and False = 1 for binary variables. Under this convention:

  • A positive literal $x_i$ is False when $x_i = 1$.
  • A negative literal $\lnot x_i$ is False when $x_i = 0$, i.e., when $\tilde{x}_i = 1$ (where $\tilde{x}_i$ denotes the negated literal in PyQBPP).

A clause is violated (all its literals are False) exactly when the product of the “False indicator” for each literal equals 1. For a clause $C_k$, we define the penalty:

\[p_k = \prod_{\ell \in C_k} f(\ell)\]

where $f(\ell) = x_i$ if $\ell$ is the positive literal $x_i$, and $f(\ell) = \tilde{x}_i$ if $\ell$ is the negative literal $\lnot x_i$. This product equals 1 if and only if the clause is violated.

The total constraint expression is:

\[\text{constraint} = \sum_{k} p_k\]

This expression achieves the minimum value 0 if and only if all clauses are satisfied. Note that the constraint is naturally a HUBO (higher-order unconstrained binary optimization) expression, since each clause with $m$ literals produces a term of degree $m$.

PyQBPP Formulation

The following PyQBPP program solves the 3-SAT instance described above:

import pyqbpp as qbpp

# 5 Boolean variables
x = qbpp.var("x", 5)

# Convention: True=0, False=1
# Positive literal x_i: False when x_i=1 -> contribute x[i]
# Negative literal ~x_j: False when x_j=0 -> contribute ~x[j]
# Product = 1 iff all literals in the clause are False (violated)

# Clause 0: (x0 OR x1 OR x2)
#   violated when x0=False AND x1=False AND x2=False
#   penalty = x[0] * x[1] * x[2]
c0 = x[0] * x[1] * x[2]

# Clause 1: (~x0 OR x3 OR x4)
#   violated when x0=True AND x3=False AND x4=False
#   penalty = ~x[0] * x[3] * x[4]
c1 = ~x[0] * x[3] * x[4]

# Clause 2: (x1 OR ~x2 OR ~x3)
#   violated when x1=False AND x2=True AND x3=True
#   penalty = x[1] * ~x[2] * ~x[3]
c2 = x[1] * ~x[2] * ~x[3]

# Clause 3: (~x1 OR ~x3 OR x4)
#   violated when x1=True AND x3=True AND x4=False
#   penalty = ~x[1] * ~x[3] * x[4]
c3 = ~x[1] * ~x[3] * x[4]

# Clause 4: (~x0 OR ~x1 OR ~x2)
#   violated when x0=True AND x1=True AND x2=True
#   penalty = ~x[0] * ~x[1] * ~x[2]
c4 = ~x[0] * ~x[1] * ~x[2]

# Clause 5: (x0 OR x1 OR ~x4)
#   violated when x0=False AND x1=False AND x4=True
#   penalty = x[0] * x[1] * ~x[4]
c5 = x[0] * x[1] * ~x[4]

# Total constraint: sum of clause penalties
constraint = c0 + c1 + c2 + c3 + c4 + c5

constraint.simplify_as_binary()
solver = qbpp.EasySolver(constraint)
sol = solver.search({"target_energy": 0})

# Print result
print(f"Energy = {sol.energy}")
print("Assignment (True=0, False=1):")
for i in range(5):
    val = sol(x[i])
    print(f"  x[{i}] = {val} ({'True' if val == 0 else 'False'})")

# Verify each clause
print("Clause penalties:")
for i, c in enumerate([c0, c1, c2, c3, c4, c5]):
    print(f"  c{i} = {sol(c)}")
print(f"Violated clauses = {sol(constraint)}")

In this program, we define 5 binary variables and construct the penalty expression for each clause. For a positive literal $x_i$, we use x[i], which equals 1 when the literal is False. For a negative literal $\lnot x_i$, we use ~x[i], which equals 1 when the literal is False (i.e., when $x_i$ is True, meaning $x_i = 0$). PyQBPP natively supports negated literals ~x[i], so there is no need to manually replace them with 1 - x[i].

The product of these terms for a clause equals 1 only when all literals in the clause are False, i.e., when the clause is violated. The total constraint is the sum of all clause penalties, and it achieves 0 if and only if all clauses are satisfied.

We call simplify_as_binary() to apply the idempotent rule $x_i^2 = x_i$ and simplify the expression, then solve with EasySolver targeting energy 0.

Output

Energy = 0
Assignment (True=0, False=1):
  x[0] = 0 (True)
  x[1] = 1 (False)
  x[2] = 0 (True)
  x[3] = 1 (False)
  x[4] = 0 (True)
Clause penalties:
  c0 = 0
  c1 = 0
  c2 = 0
  c3 = 0
  c4 = 0
  c5 = 0
Violated clauses = 0

The solver finds a satisfying assignment with energy 0, meaning all clauses are satisfied. Note that the actual assignment may vary across runs, as the solver is stochastic.


Back to top

Page last modified: 2026.04.04.