Expression trees and satisfiability
Turn a propositional formula into an expression tree and evaluate it for every assignment with a postorder walk; the formula is satisfiable if some row comes out true. 2^n assignments.
Every propositional formula has an expression tree: the leaves are variables and the inner nodes operators (¬ with
one child, ∧ and ∨ with two). The formula's value comes from a postorder walk: the children first, then the
operator.
- Time:
O(2^n · size) - Space:
O(size) - Horowitz–Sahni, chapter 5
(a ∧ ¬b) ∨ (¬a ∧ c) ∨ ¬c; three variables and 8 rows, 7 of them true (only all three true makes it false).
class Node:def __init__(self, key):self.key = keyself.left = Noneself.right = Nonedef satisfiable(formula):root = parse(formula)if root is None:return Nonenames = []collect(root, names)names.sort()n = len(names)rows = [names + ["value"]]models = 0for m in range(2 ** n):env = {}for i in range(n):env[names[i]] = (m >> (n - 1 - i)) % 2 == 1value = evaluate(root, env)row = []for name in names:row.append("T" if env[name] else "F")row.append("T" if value else "F")rows.append(row)if value:models = models + 1return modelsdef evaluate(node, env):# Postorder: both children first, then the operator at this node.if node.key in env:v = env[node.key]return vright = evaluate(node.right, env)if node.key == "¬":v = not rightreturn vleft = evaluate(node.left, env)if node.key == "∧":v = left & rightelse:v = left | rightreturn vdef collect(node, names):if node is None:returnif node.key.isalpha() and node.key not in names:names.append(node.key)collect(node.left, names)collect(node.right, names)def parse(text):# | is or, & is and, ~ is not; ~ binds tightest and | loosest.toks = []for c in text:if c != " ":toks.append(c)pos = [0]root = parse_or(toks, pos)if pos[0] != len(toks):return Nonereturn rootdef parse_or(toks, pos):left = parse_and(toks, pos)while left is not None and pos[0] < len(toks) and toks[pos[0]] == "|":pos[0] += 1left = join("∨", left, parse_and(toks, pos))return leftdef parse_and(toks, pos):left = parse_not(toks, pos)while left is not None and pos[0] < len(toks) and toks[pos[0]] == "&":pos[0] += 1left = join("∧", left, parse_not(toks, pos))return leftdef parse_not(toks, pos):if pos[0] >= len(toks):return Nonec = toks[pos[0]]pos[0] += 1if c == "~":return join("¬", None, parse_not(toks, pos))if c == "(":inner = parse_or(toks, pos)if inner is None or pos[0] >= len(toks) or toks[pos[0]] != ")":return Nonepos[0] += 1return innerif c.isalpha():return Node(c)return Nonedef join(op, left, right):if right is None:return Nonenode = Node(op)node.left = leftnode.right = rightreturn node
Start
∅
The table is still empty.
Recursion tree
Filled orange = running call · Orange outline = still on the stack · Grey = finished
Local variables
Orange = changed in this step
Call stack
- satisfiable()line 8
Explanation
Expression trees
Every propositional formula has an expression tree: the leaves are variables and the inner nodes operators (¬ with
one child, ∧ and ∨ with two). The formula's value comes from a postorder walk: the children first, then the
operator.
Satisfiability
A formula is satisfiable if some assignment of its variables makes it true. The direct method (as in Horowitz–Sahni)
tries all 2^n assignments and evaluates the tree each time: that is the truth table.
This problem (SAT) was the first one proved NP-complete (Cook–Levin); no polynomial algorithm for it is known.
The input
The letters a to d are variables; ~ is not, & and, | or, plus brackets. ~ binds tightest and | loosest.