2 SAT Problem
1. What is SAT Problem
Boolean Satisfiability or simply SAT is the problem of determining if a Boolean formula is satisfiable or unsatisfiable.
- Satisfiable : If the Boolean variables can be assigned values such that the formula turns out to be TRUE, then we say that the formula is satisfiable.
- Unsatisfiable : If it is not possible to assign such values, then we say that the formula is unsatisfiable.
Examples:
- $F=A\land \bar{B}$ is satisfiable, because A = TRUE and B = FALSE makes F = TRUE.
- $F = A \land \bar{A}$ is unsatisfiable, because
- if A = True, $\bar{A}$ = False, they and together, F = False
- if A = False, $\bar{A}$ = True, they and together, F = False
Symbol explanation:
- A is one term, it can be True or False
- $\bar{A}$ is not A, it is opposite to A’s Boolean value
- $\land$ is and gate
- $\lor$ is or gate
2. What is Conjunctive Normal Form (CNF)
To understand Problem of 2-Satisfiability better, first let us see what is Conjunctive Normal Form (CNF)
CNF : CNF is a conjunction (AND) of clauses, where every clause is a disjunction (OR).
Example:
$$
F= (A_1 \lor B_1 \lor C_1 \lor …) \land (A_2 \lor B_2\lor C_2 \lor …) \land (A_3 \lor B_3\lor C_3 \lor …) \land …… \land (A_m \lor B_m\lor C_m \lor …)
$$
3. What is 2-SAT problem
Thus, Problem of 2-Satisfiability can be stated as:
Given CNF with each clause having only 2 terms, is it possible to assign such values to the variables so that the CNF is TRUE?
Example:
$$
F= (x_1 \lor x_2 ) \land (x_2 \lor \bar{x}_1) \land (\bar{x}_1 \lor \bar{x}_2)
$$The given expression is satisfiable.
for x1 = FALSE, x2 = TRUE
$$
F= (x_1 \lor x_2 ) \land (x_2 \lor \bar{x}_1)\land (x_1 \lor \bar{x}_2) \land (\bar{x}_1 \lor \bar{x}_2)
$$The given expression is unsatisfiable.
for all possible combinations of x1 and x2
4. How to solve the 2-SAT problem4
Prepare:
For the CNF value to come TRUE, value of every clause should be TRUE.
Let one of the clause be ($A\lor B$) = True
- if A=0, B must be 1, ($\bar{A} \Rightarrow B $)
- if B=0, A must be 1, ($\bar{B} \Rightarrow A $)
Then we create an Implication Graph which has 2 edges for every clause of the CNF.
($A\lor B$) expressed in Graph as $edge(\bar{A} \Rightarrow B )$ , $edge(\bar{B} \Rightarrow A )$
So for a Boolean formula with m
clauses, we have 2 edges for every clause in Graph. So 2m
edges in total.
Case:
Case 1: If $edge(X\rightarrow \bar{X})$ exists in the graph
It means if X=True, $\bar{X}$ = True
But if X = False, can not decide $\bar{X}$
Case 2: If $edge(\bar{X}\rightarrow X)$ exists in the graph
It means if $\bar{X}$ =True, X = True
But if $\bar{X}$ = False, can not decide X
Case 3: If $edge(\bar{X}\rightarrow X) \space and \space edge(X\rightarrow \bar{X})$ exists in the graph at same time
One edge requires X to be TRUE and the other one requires X to be FALSE.
Thus, there is no possible assignment in such a case.
CONCLUSION: If any two variables $X$ and $\bar{X}$ are on a cycle i.e. $edge(\bar{X}\rightarrow X) \space and \space edge(X\rightarrow \bar{X})$ both exists, then the CNF is unsatisfiable. Otherwise, there is a possible assignment and the CNF is satisfiable.
CONCLUSION FROM IMPLEMENTATION POINT OF VIEW:
If both $X$ and $\bar{X}$ lie in the same SCC (Strongly Connected Component), the CNF is unsatisfiable.
Strongly Connected Component:
A Strongly Connected Component of a directed graph has nodes such that every node can be reach from every another node in that SCC.
3 SCCs here
5. Code for solve 2-SAT problem implement by Python
Total code
import time
test_data = [ ]
with open('2satBIG.cnf', 'r') as f:
for line in f.readlines():
test_data.append(line.strip()) # 把末尾的'\n'删掉
A = []
B = []
n = 0
m = 0
for rec in test_data:
rec=rec.strip()
junk_list = []
substrs = rec.split()
if substrs[0] == 'p' and substrs[1] == 'cnf':
n=int(substrs[2])
m=int(substrs[3])
## record has to start with a number or a minus sign
if (rec[0].isdigit()) or (rec[0].startswith("-")):
substrs = rec.split()
try:
if len(substrs) == 3:
for num in range(len(substrs)):
num_int = int(substrs[num])
if num == 0:
A.append(num_int)
elif num ==1:
B.append(num_int)
elif len(substrs) == 2:
for num in range(len(substrs)):
num_int = int(substrs[num])
if num == 0:
A.append(num_int)
B.append(num_int)
except:
print("error converting", rec)
MAX = 1000000
adj = []
adjInv = []
for i in range(0, MAX + 1):
adj.append([])
adjInv.append([])
visited = [False] * MAX
visitedInv = [False] * MAX
s = []
scc = [0] * MAX
count = 1
def add_edge(a, b):
adj[a].append(b)
def add_edge_inv(a, b):
adjInv[b].append(a)
def dfs_first(u):
if visited[u]:
return
visited[u] = True
for i in range(0, len(adj[u])):
dfs_first(adj[u][i])
s.append(u)
# print("stack: " + str(s))
def dfs_second(u):
if visitedInv[u]:
return
visitedInv[u] = True
for i in range(0, len(adjInv[u])):
dfs_second(adjInv[u][i])
scc[u] = count
def is_2_sat(n, m, a, b):
global count
for i in range(0, m):
if a[i] > 0 and b[i] > 0:
add_edge(a[i] + n, b[i])
add_edge_inv(a[i] + n, b[i])
add_edge(b[i] + n, a[i])
add_edge_inv(b[i] + n, a[i])
elif a[i] > 0 > b[i]:
add_edge(a[i] + n, n - b[i])
add_edge_inv(a[i] + n, n - b[i])
add_edge(-b[i], a[i])
add_edge_inv(-b[i], a[i])
elif a[i] < 0 < b[i]:
add_edge(-a[i], b[i])
add_edge_inv(-a[i], b[i])
add_edge(b[i] + n, n - a[i])
add_edge_inv(b[i] + n, n - a[i])
else:
add_edge(-a[i], n - b[i])
add_edge_inv(-a[i], n - b[i])
add_edge(-b[i], n - a[i])
add_edge_inv(-b[i], n - a[i])
for i in range(1, 2 * n + 1):
if not visited[i]:
dfs_first(i)
while len(s) != 0:
top = s.pop()
if not visitedInv[top]:
dfs_second(top)
count += 1
# print("scc: " + str(scc[0:20]))
for i in range(1, n + 1):
if scc[i] == scc[i + n]:
print("The given expression is unsatisfiable")
return
print("The given expression is satisfiable")
for i in range(1, n + 1):
if scc[i] > scc[i + n]:
pass
# print("x" + str(i) + ": True")
else:
pass
# print("x" + str(i) + ": False")
if __name__ == '__main__':
start = time.time()
# for i in range(100):
is_2_sat(n, m, A, B)
end = time.time()
print(end-start)
Part 1, read CNF file
test_data = [ ]
with open('2satBIG.cnf', 'r') as f:
for line in f.readlines():
test_data.append(line.strip()) # 把末尾的'\n'删掉
A = []
B = []
n = 0
m = 0
for rec in test_data:
rec=rec.strip()
junk_list = []
substrs = rec.split()
if substrs[0] == 'p' and substrs[1] == 'cnf':
n=int(substrs[2])
m=int(substrs[3])
## record has to start with a number or a minus sign
if (rec[0].isdigit()) or (rec[0].startswith("-")):
substrs = rec.split()
try:
if len(substrs) == 3:
for num in range(len(substrs)):
num_int = int(substrs[num])
if num == 0:
A.append(num_int)
elif num ==1:
B.append(num_int)
elif len(substrs) == 2:
for num in range(len(substrs)):
num_int = int(substrs[num])
if num == 0:
A.append(num_int)
B.append(num_int)
except:
print("error converting", rec)
Part 2, check 2 SAT-Problem
MAX = 1000000
adj = []
adjInv = []
for i in range(0, MAX + 1):
adj.append([])
adjInv.append([])
visited = [False] * MAX
visitedInv = [False] * MAX
s = []
scc = [0] * MAX
count = 1
def add_edge(a, b):
adj[a].append(b)
def add_edge_inv(a, b):
adjInv[b].append(a)
def dfs_first(u):
if visited[u]:
return
visited[u] = True
for i in range(0, len(adj[u])):
dfs_first(adj[u][i])
s.append(u)
def dfs_second(u):
if visitedInv[u]:
return
visitedInv[u] = True
for i in range(0, len(adjInv[u])):
dfs_second(adjInv[u][i])
scc[u] = count
# m number of clauses
# n number of terms
# A all terms in first position
# B all terms in second position
def is_2_sat(n, m, a, b):
global count
for i in range(0, m):
if a[i] > 0 and b[i] > 0:
add_edge(a[i] + n, b[i])
add_edge_inv(a[i] + n, b[i])
add_edge(b[i] + n, a[i])
add_edge_inv(b[i] + n, a[i])
elif a[i] > 0 > b[i]:
add_edge(a[i] + n, n - b[i])
add_edge_inv(a[i] + n, n - b[i])
add_edge(-b[i], a[i])
add_edge_inv(-b[i], a[i])
elif a[i] < 0 < b[i]:
add_edge(-a[i], b[i])
add_edge_inv(-a[i], b[i])
add_edge(b[i] + n, n - a[i])
add_edge_inv(b[i] + n, n - a[i])
else:
add_edge(-a[i], n - b[i])
add_edge_inv(-a[i], n - b[i])
add_edge(-b[i], n - a[i])
add_edge_inv(-b[i], n - a[i])
for i in range(1, 2 * n + 1):
if not visited[i]:
dfs_first(i)
while len(s) != 0:
top = s.pop()
if not visitedInv[top]:
dfs_second(top)
count += 1
for i in range(1, n + 1):
if scc[i] == scc[i + n]:
print("The given expression is unsatisfiable")
return
print("The given expression is satisfiable")
if __name__ == '__main__':
start = time.time()
# for i in range(100):
is_2_sat(n, m, A, B)
end = time.time()
print(end-start)
6. Explanation for Algo
Step 1:
The after reading the CNF file it will return us four parameters
- m number of clauses
- n number of terms
- A all terms in first position
- B all terms in second position
Step 2:
scc = [0] * MAX
This is used for checking the SCC problem.
Store the count number in the corresponding position in SCC.
Step 3:
Based on the input terms’ sign, we will decide how we set up the edges.
for i in range(0, m):
if a[i] > 0 and b[i] > 0:
add_edge(a[i] + n, b[i])
add_edge_inv(a[i] + n, b[i])
add_edge(b[i] + n, a[i])
add_edge_inv(b[i] + n, a[i])
elif a[i] > 0 > b[i]:
add_edge(a[i] + n, n - b[i])
add_edge_inv(a[i] + n, n - b[i])
add_edge(-b[i], a[i])
add_edge_inv(-b[i], a[i])
elif a[i] < 0 < b[i]:
add_edge(-a[i], b[i])
add_edge_inv(-a[i], b[i])
add_edge(b[i] + n, n - a[i])
add_edge_inv(b[i] + n, n - a[i])
else:
add_edge(-a[i], n - b[i])
add_edge_inv(-a[i], n - b[i])
add_edge(-b[i], n - a[i])
add_edge_inv(-b[i], n - a[i])
Step 4:
execute the DFS
changing the count number
for i in range(1, 2 * n + 1):
if not visited[i]:
dfs_first(i)
while len(s) != 0:
top = s.pop()
if not visitedInv[top]:
dfs_second(top)
count += 1
Step 5:
Whether there is both $X$ and $\bar{X}$ lie in the same SCC condition. Return results.
for i in range(1, n + 1):
if scc[i] == scc[i + n]:
print("The given expression is unsatisfiable")
return
print("The given expression is satisfiable")