I was reading about the 2-SAT problem on Wikipedia and I was wondering what the O(n) algorithm looks like in Python.
So far I've only found implementations that either are in other programming languages or that just determine whether an expression has solutions or not, without given the solution itself.
How could the O(n) algorithm for finding the values of variables be written in Python?
Here is an OOP implementation in Python:
import re
class TwoSat:
class Variable:
def __init__(self, name, negated=None):
self.name = name
self.negated = negated or TwoSat.Variable("~" + name, self)
self.implies = set()
self.impliedby = set()
self.component = -1
def disjunction(self, b):
self.negated.implication(b)
b.negated.implication(self)
def implication(self, b):
self.implies.add(b)
b.impliedby.add(self)
def postorder(self, visited):
if self not in visited:
visited.add(self)
for neighbor in self.implies:
yield from neighbor.postorder(visited)
yield self
def setcomponent(self, component):
if self.component == -1:
self.component = component
for neighbor in self.impliedby:
neighbor.setcomponent(component)
def value(self):
diff = self.component - self.negated.component
return diff > 0 if diff else None
### end of class Variable
def __init__(self, s):
self.variables = {}
for a_neg, a_name, b_neg, b_name in re.findall(r"(~)?(\w+).*?(~)?(\w+)", s):
self.getvariable(a_neg, a_name).disjunction(self.getvariable(b_neg, b_name))
def getvariable(self, neg, name):
if name not in self.variables:
self.variables[name] = TwoSat.Variable(name)
self.variables["~" + name] = self.variables[name].negated
a = self.variables[name]
return a.negated if neg else a
def postorder(self):
visited = set()
for startvariable in self.variables.values():
yield from startvariable.postorder(visited)
def setcomponents(self):
for i, variable in enumerate(reversed(list(self.postorder()))):
variable.setcomponent(i)
def issolved(self):
return all(variable.value() is not None for variable in self.variables.values())
def solve(self):
self.setcomponents()
return self.issolved()
def truevariables(self):
if self.issolved():
return [variable.name for variable in self.variables.values() if variable.value()]
def __repr__(self):
return " ∧ ".join(
f"({a.name} → {b.name})"
for a in self.variables.values()
for b in a.implies
)
Here is an example of how this class can be used:
problem = TwoSat("(~a+~b)*(b+~c)*(c+g)*(d+a)*(~f+i)*(~i+~j)*(~h+d)*(~d+~b)*(~f+c)*(h+~i)*(i+~g)")
print(problem)
problem.solve()
print("solution: ", problem.truevariables())
The TwoSat
constructor takes one argument, a string, which should provide the conjugation of disjunction pairs. The syntax rules for this string are:
~
to denote negation.So the above example could also have taken this string representing the same problem:
problem = TwoSat("~a ~b b ~c c g d a ~f i ~i ~j ~h d ~d ~b ~f c h ~i i ~g")
Alternatively, you can use the getvariable
and disjunction
methods to build the expression. Look at the __init__
method how the constructor uses those methods when parsing the string. For example:
problem = TwoSat()
for variable in "abcdefghij":
problem.getvariable(False, variable)
# Define the disjunction ~a + ~b:
problem.variables["a"].negated.disjunction(problem.variables["b"].negated)
# ...etc
The algorithm is the one explained in the 2-satisiability article on Wikipedia, identifying strongly connected components using Kosaraju's algorithm