2-CNF Boolean Satisfiability Problem
Problem
A Boolean formula can be said to be satisfiable if there is a way to assign truth values to each variable such that the entire formula evaluates to true.
For example, suppose we have the following formula, where the symbol ¬ is used to denote negation:
(¬c OR b) AND (b OR c) AND (¬b OR c) AND (¬c OR ¬a)
One way to satisfy this formula would be to let a = False, b = True, and c = True.
This type of formula, with AND statements joining tuples containing exactly one OR, is known as 2-CNF.
Given a 2-CNF formula, find a way to assign truth values to satisfy it, or return False if this is impossible.
Examples
Example 1
Input: [['¬c', 'b'], ['b', 'c'], ['¬b', 'c'], ['¬c', '¬a']]
Output: {'a': False, 'b': True, 'c': True}
Explanation:
- Clause 1: (¬c OR b) = (¬True OR True) = (False OR True) = True
- Clause 2: (b OR c) = (True OR True) = True
- Clause 3: (¬b OR c) = (¬True OR True) = (False OR True) = True
- Clause 4: (¬c OR ¬a) = (¬True OR ¬False) = (False OR True) = True
All clauses are satisfied.
Example 2
Input: [['a', 'b'], ['¬a', '¬b'], ['a', '¬b'], ['¬a', 'b']]
Output: False
Explanation:
This is unsatisfiable. The clauses form contradictions:
- If a=True: from clause 2, b must be False; from clause 3, b must be False; from clause 4, b must be True (contradiction)
- If a=False: from clause 1, b must be True; from clause 3 contradicts this
Example 3
Input: [['a', 'b'], ['¬a', 'c'], ['¬b', '¬c']]
Output: {'a': True, 'b': True, 'c': False}
Explanation:
- Clause 1: (a OR b) = (True OR True) = True
- Clause 2: (¬a OR c) = (¬True OR False) = (False OR False) = False...
Wait, let me recalculate: c should be True for this to work.
Actually: {'a': False, 'b': True, 'c': True} works:
- Clause 1: (False OR True) = True
- Clause 2: (¬False OR True) = (True OR True) = True
- Clause 3: (¬True OR ¬True) = (False OR False) = False
This needs a=True, b=True, c=False: (True OR True), (False OR False), (False OR True) = True, False, True - still not working.
Let me try a=False, b=True, c=True: (False OR True), (True OR True), (False OR False) = True, True, False.
Actually a=True, b=False, c=False: (True OR False), (False OR False), (True OR True) = True, False, True - still issues.
Let me use a=False, b=True, c=False: (False OR True), (True OR False), (False OR True) = True, True, True. This works!
Example 4
Input: [['x'], ['¬x']]
Output: False
Explanation:
Impossible to satisfy since x must be both True and False simultaneously.
Note: This is not valid 2-CNF since clauses should have exactly 2 literals, but shows unsatisfiable case.
Example 5
Input: [['p', 'q'], ['¬p', 'r'], ['¬q', 'r'], ['¬r', 's'], ['¬s', 'p']]
Output: {'p': True, 'q': True, 'r': True, 's': True}
Explanation:
- Clause 1: (True OR True) = True
- Clause 2: (False OR True) = True
- Clause 3: (False OR True) = True
- Clause 4: (False OR True) = True
- Clause 5: (False OR True) = True
All clauses satisfied with this assignment.
Solution
Method 1 - Implication Graph with SCC Algorithm
Intuition
A 2-CNF formula can be converted into an implication graph where each clause (A OR B) becomes two implications: ¬A → B and ¬B → A. The formula is satisfiable if and only if no variable and its negation appear in the same strongly connected component (SCC). We can use Kosaraju's algorithm to find SCCs and then assign truth values based on topological ordering.
Approach
- Convert 2-CNF clauses to implication graph
- For each clause (A OR B), add edges ¬A → B and ¬B → A
- Find strongly connected components using Kosaraju's algorithm
- Check if any variable x and ¬x are in the same SCC (if yes, unsatisfiable)
- If satisfiable, assign truth values using reverse topological order of SCCs
- For each SCC in reverse topological order, assign values to satisfy implications
- Return the satisfying assignment or False if unsatisfiable
Code
C++
class Solution {
private:
unordered_map<string, vector<string>> graph, revGraph;
unordered_map<string, bool> visited;
vector<string> finishOrder;
unordered_map<string, int> sccId;
int sccCount;
public:
unordered_map<string, bool> solve2CNF(vector<vector<string>>& clauses) {
// Build implication graph
buildGraph(clauses);
// Find SCCs using Kosaraju's algorithm
findSCCs();
// Check satisfiability
set<string> variables = extractVariables(clauses);
for (const string& var : variables) {
if (sccId[var] == sccId["¬" + var]) {
return {}; // Unsatisfiable
}
}
// Assign truth values
return assignValues(variables);
}
private:
void buildGraph(vector<vector<string>>& clauses) {
for (auto& clause : clauses) {
string a = clause[0], b = clause[1];
string notA = negate(a), notB = negate(b);
// (A OR B) => (¬A → B) and (¬B → A)
graph[notA].push_back(b);
graph[notB].push_back(a);
revGraph[b].push_back(notA);
revGraph[a].push_back(notB);
}
}
string negate(const string& lit) {
if (lit[0] == '¬') return lit.substr(1);
return "¬" + lit;
}
void findSCCs() {
// First DFS to get finish order
for (auto& [node, _] : graph) {
if (!visited[node]) {
dfs1(node);
}
}
// Second DFS on reverse graph
visited.clear();
sccCount = 0;
reverse(finishOrder.begin(), finishOrder.end());
for (const string& node : finishOrder) {
if (!visited[node]) {
dfs2(node, sccCount++);
}
}
}
void dfs1(const string& node) {
visited[node] = true;
for (const string& neighbor : graph[node]) {
if (!visited[neighbor]) {
dfs1(neighbor);
}
}
finishOrder.push_back(node);
}
void dfs2(const string& node, int id) {
visited[node] = true;
sccId[node] = id;
for (const string& neighbor : revGraph[node]) {
if (!visited[neighbor]) {
dfs2(neighbor, id);
}
}
}
set<string> extractVariables(vector<vector<string>>& clauses) {
set<string> vars;
for (auto& clause : clauses) {
for (const string& lit : clause) {
string var = (lit[0] == '¬') ? lit.substr(1) : lit;
vars.insert(var);
}
}
return vars;
}
unordered_map<string, bool> assignValues(const set<string>& variables) {
unordered_map<string, bool> assignment;
vector<pair<int, string>> sccOrder;
for (const string& var : variables) {
sccOrder.push_back({sccId[var], var});
sccOrder.push_back({sccId["¬" + var], "¬" + var});
}
sort(sccOrder.rbegin(), sccOrder.rend()); // Reverse topological order
for (auto& [id, lit] : sccOrder) {
string var = (lit[0] == '¬') ? lit.substr(1) : lit;
if (assignment.find(var) == assignment.end()) {
bool value = (lit[0] != '¬');
assignment[var] = value;
}
}
return assignment;
}
};
Go
type TwoCNFSolver struct {
graph map[string][]string
revGraph map[string][]string
visited map[string]bool
finishOrder []string
sccId map[string]int
sccCount int
}
func solve2CNF(clauses [][]string) map[string]bool {
solver := &TwoCNFSolver{
graph: make(map[string][]string),
revGraph: make(map[string][]string),
visited: make(map[string]bool),
sccId: make(map[string]int),
}
// Build implication graph
solver.buildGraph(clauses)
// Find SCCs
solver.findSCCs()
// Check satisfiability
variables := extractVariables(clauses)
for _, variable := range variables {
if solver.sccId[variable] == solver.sccId[negate(variable)] {
return nil // Unsatisfiable
}
}
// Assign truth values
return solver.assignValues(variables)
}
func (s *TwoCNFSolver) buildGraph(clauses [][]string) {
for _, clause := range clauses {
a, b := clause[0], clause[1]
notA, notB := negate(a), negate(b)
// (A OR B) => (¬A → B) and (¬B → A)
s.graph[notA] = append(s.graph[notA], b)
s.graph[notB] = append(s.graph[notB], a)
s.revGraph[b] = append(s.revGraph[b], notA)
s.revGraph[a] = append(s.revGraph[a], notB)
}
}
func negate(lit string) string {
if strings.HasPrefix(lit, "¬") {
return lit[1:]
}
return "¬" + lit
}
func (s *TwoCNFSolver) findSCCs() {
// First DFS to get finish order
for node := range s.graph {
if !s.visited[node] {
s.dfs1(node)
}
}
// Second DFS on reverse graph
s.visited = make(map[string]bool)
s.sccCount = 0
// Reverse finish order
for i, j := 0, len(s.finishOrder)-1; i < j; i, j = i+1, j-1 {
s.finishOrder[i], s.finishOrder[j] = s.finishOrder[j], s.finishOrder[i]
}
for _, node := range s.finishOrder {
if !s.visited[node] {
s.dfs2(node, s.sccCount)
s.sccCount++
}
}
}
func (s *TwoCNFSolver) dfs1(node string) {
s.visited[node] = true
for _, neighbor := range s.graph[node] {
if !s.visited[neighbor] {
s.dfs1(neighbor)
}
}
s.finishOrder = append(s.finishOrder, node)
}
func (s *TwoCNFSolver) dfs2(node string, id int) {
s.visited[node] = true
s.sccId[node] = id
for _, neighbor := range s.revGraph[node] {
if !s.visited[neighbor] {
s.dfs2(neighbor, id)
}
}
}
func extractVariables(clauses [][]string) []string {
varSet := make(map[string]bool)
for _, clause := range clauses {
for _, lit := range clause {
var variable string
if strings.HasPrefix(lit, "¬") {
variable = lit[1:]
} else {
variable = lit
}
varSet[variable] = true
}
}
var variables []string
for variable := range varSet {
variables = append(variables, variable)
}
return variables
}
func (s *TwoCNFSolver) assignValues(variables []string) map[string]bool {
assignment := make(map[string]bool)
type sccLit struct {
id int
lit string
}
var sccOrder []sccLit
for _, variable := range variables {
sccOrder = append(sccOrder, sccLit{s.sccId[variable], variable})
sccOrder = append(sccOrder, sccLit{s.sccId["¬"+variable], "¬" + variable})
}
// Sort in reverse topological order
sort.Slice(sccOrder, func(i, j int) bool {
return sccOrder[i].id > sccOrder[j].id
})
for _, item := range sccOrder {
lit := item.lit
var variable string
if strings.HasPrefix(lit, "¬") {
variable = lit[1:]
} else {
variable = lit
}
if _, exists := assignment[variable]; !exists {
value := !strings.HasPrefix(lit, "¬")
assignment[variable] = value
}
}
return assignment
}
Java
class Solution {
private Map<String, List<String>> graph = new HashMap<>();
private Map<String, List<String>> revGraph = new HashMap<>();
private Set<String> visited = new HashSet<>();
private List<String> finishOrder = new ArrayList<>();
private Map<String, Integer> sccId = new HashMap<>();
private int sccCount = 0;
public Map<String, Boolean> solve2CNF(List<List<String>> clauses) {
// Build implication graph
buildGraph(clauses);
// Find SCCs using Kosaraju's algorithm
findSCCs();
// Check satisfiability
Set<String> variables = extractVariables(clauses);
for (String var : variables) {
if (sccId.get(var).equals(sccId.get("¬" + var))) {
return null; // Unsatisfiable
}
}
// Assign truth values
return assignValues(variables);
}
private void buildGraph(List<List<String>> clauses) {
for (List<String> clause : clauses) {
String a = clause.get(0), b = clause.get(1);
String notA = negate(a), notB = negate(b);
// (A OR B) => (¬A → B) and (¬B → A)
graph.computeIfAbsent(notA, k -> new ArrayList<>()).add(b);
graph.computeIfAbsent(notB, k -> new ArrayList<>()).add(a);
revGraph.computeIfAbsent(b, k -> new ArrayList<>()).add(notA);
revGraph.computeIfAbsent(a, k -> new ArrayList<>()).add(notB);
}
}
private String negate(String lit) {
if (lit.startsWith("¬")) return lit.substring(1);
return "¬" + lit;
}
private void findSCCs() {
// First DFS to get finish order
for (String node : graph.keySet()) {
if (!visited.contains(node)) {
dfs1(node);
}
}
// Second DFS on reverse graph
visited.clear();
sccCount = 0;
Collections.reverse(finishOrder);
for (String node : finishOrder) {
if (!visited.contains(node)) {
dfs2(node, sccCount++);
}
}
}
private void dfs1(String node) {
visited.add(node);
for (String neighbor : graph.getOrDefault(node, new ArrayList<>())) {
if (!visited.contains(neighbor)) {
dfs1(neighbor);
}
}
finishOrder.add(node);
}
private void dfs2(String node, int id) {
visited.add(node);
sccId.put(node, id);
for (String neighbor : revGraph.getOrDefault(node, new ArrayList<>())) {
if (!visited.contains(neighbor)) {
dfs2(neighbor, id);
}
}
}
private Set<String> extractVariables(List<List<String>> clauses) {
Set<String> vars = new HashSet<>();
for (List<String> clause : clauses) {
for (String lit : clause) {
String var = lit.startsWith("¬") ? lit.substring(1) : lit;
vars.add(var);
}
}
return vars;
}
private Map<String, Boolean> assignValues(Set<String> variables) {
Map<String, Boolean> assignment = new HashMap<>();
List<Pair<Integer, String>> sccOrder = new ArrayList<>();
for (String var : variables) {
sccOrder.add(new Pair<>(sccId.get(var), var));
sccOrder.add(new Pair<>(sccId.get("¬" + var), "¬" + var));
}
sccOrder.sort((a, b) -> b.getKey().compareTo(a.getKey())); // Reverse order
for (Pair<Integer, String> item : sccOrder) {
String lit = item.getValue();
String var = lit.startsWith("¬") ? lit.substring(1) : lit;
if (!assignment.containsKey(var)) {
boolean value = !lit.startsWith("¬");
assignment.put(var, value);
}
}
return assignment;
}
private static class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
}
Python
from collections import defaultdict, deque
from typing import List, Dict, Set, Optional
class Solution:
def __init__(self):
self.graph = defaultdict(list)
self.rev_graph = defaultdict(list)
self.visited = set()
self.finish_order = []
self.scc_id = {}
self.scc_count = 0
def solve_2cnf(self, clauses: List[List[str]]) -> Optional[Dict[str, bool]]:
# Build implication graph
self._build_graph(clauses)
# Find SCCs using Kosaraju's algorithm
self._find_sccs()
# Check satisfiability
variables = self._extract_variables(clauses)
for var in variables:
if self.scc_id[var] == self.scc_id[f"¬{var}"]:
return None # Unsatisfiable
# Assign truth values
return self._assign_values(variables)
def _build_graph(self, clauses: List[List[str]]) -> None:
for clause in clauses:
a, b = clause[0], clause[1]
not_a, not_b = self._negate(a), self._negate(b)
# (A OR B) => (¬A → B) and (¬B → A)
self.graph[not_a].append(b)
self.graph[not_b].append(a)
self.rev_graph[b].append(not_a)
self.rev_graph[a].append(not_b)
def _negate(self, lit: str) -> str:
if lit.startswith('¬'):
return lit[1:]
return f"¬{lit}"
def _find_sccs(self) -> None:
# First DFS to get finish order
for node in self.graph:
if node not in self.visited:
self._dfs1(node)
# Second DFS on reverse graph
self.visited.clear()
self.scc_count = 0
self.finish_order.reverse()
for node in self.finish_order:
if node not in self.visited:
self._dfs2(node, self.scc_count)
self.scc_count += 1
def _dfs1(self, node: str) -> None:
self.visited.add(node)
for neighbor in self.graph[node]:
if neighbor not in self.visited:
self._dfs1(neighbor)
self.finish_order.append(node)
def _dfs2(self, node: str, scc_id: int) -> None:
self.visited.add(node)
self.scc_id[node] = scc_id
for neighbor in self.rev_graph[node]:
if neighbor not in self.visited:
self._dfs2(neighbor, scc_id)
def _extract_variables(self, clauses: List[List[str]]) -> Set[str]:
variables = set()
for clause in clauses:
for lit in clause:
var = lit[1:] if lit.startswith('¬') else lit
variables.add(var)
return variables
def _assign_values(self, variables: Set[str]) -> Dict[str, bool]:
assignment = {}
scc_order = []
for var in variables:
scc_order.append((self.scc_id[var], var))
scc_order.append((self.scc_id[f"¬{var}"], f"¬{var}"))
scc_order.sort(reverse=True) # Reverse topological order
for scc_id, lit in scc_order:
var = lit[1:] if lit.startswith('¬') else lit
if var not in assignment:
value = not lit.startswith('¬')
assignment[var] = value
return assignment
Complexity
- ⏰ Time complexity:
O(V + E), where V is number of literals and E is number of implications. For n variables and m clauses, this is O(n + m) - 🧺 Space complexity:
O(V + E), for storing the implication graph and SCC information
Method 2 - Unit Propagation with Backtracking
Intuition
We can solve 2-CNF using unit propagation and backtracking. When we assign a value to a variable, we propagate the implications to simplify the formula. If we reach a contradiction, we backtrack. For 2-CNF, this approach is less efficient than SCC but provides insight into general SAT solving.
Approach
- Start with unassigned variables
- Choose an unassigned variable and try both truth values
- For each assignment, propagate implications using unit propagation
- If contradiction found, backtrack and try the other value
- If all variables assigned without contradiction, return the assignment
- If both values lead to contradiction, the formula is unsatisfiable
Code
Python
class Solution:
def solve_2cnf_backtrack(self, clauses: List[List[str]]) -> Optional[Dict[str, bool]]:
variables = self._extract_variables(clauses)
assignment = {}
def is_satisfied() -> bool:
for clause in clauses:
clause_value = False
for lit in clause:
var = lit[1:] if lit.startswith('¬') else lit
if var in assignment:
lit_value = assignment[var] if not lit.startswith('¬') else not assignment[var]
clause_value = clause_value or lit_value
if not clause_value:
return False
return True
def has_contradiction() -> bool:
for clause in clauses:
all_false = True
for lit in clause:
var = lit[1:] if lit.startswith('¬') else lit
if var not in assignment:
all_false = False
break
lit_value = assignment[var] if not lit.startswith('¬') else not assignment[var]
if lit_value:
all_false = False
break
if all_false:
return True
return False
def backtrack() -> bool:
if len(assignment) == len(variables):
return is_satisfied()
# Choose next unassigned variable
unassigned = [v for v in variables if v not in assignment]
if not unassigned:
return is_satisfied()
var = unassigned[0]
# Try True
assignment[var] = True
if not has_contradiction():
if backtrack():
return True
del assignment[var]
# Try False
assignment[var] = False
if not has_contradiction():
if backtrack():
return True
del assignment[var]
return False
if backtrack():
return assignment
return None
def _extract_variables(self, clauses: List[List[str]]) -> Set[str]:
variables = set()
for clause in clauses:
for lit in clause:
var = lit[1:] if lit.startswith('¬') else lit
variables.add(var)
return variables
Complexity
- ⏰ Time complexity:
O(2^N), where N is the number of variables in worst case, though often much better due to early termination - 🧺 Space complexity:
O(N), for the recursion stack and assignment storage
Notes
- Method 1 (SCC-based) is the optimal approach for 2-CNF with linear time complexity
- The implication graph captures the logical structure of 2-CNF formulas
- Kosaraju's algorithm finds strongly connected components efficiently
- If a variable and its negation are in the same SCC, the formula is unsatisfiable
- Method 2 shows how general SAT solvers work but is exponential for arbitrary formulas
- 2-CNF is a special case where polynomial-time algorithms exist
- The problem is important in computational complexity theory as it sits on the boundary between P and NP-complete problems
- Applications include circuit design, automated reasoning, and constraint satisfaction