AlgoVerse

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.

Read the full explanation ↓
Input:
formula="(a&~b)|(~a&c…"PDF export

(a ∧ ¬b) ∨ (¬a ∧ c) ∨ ¬c; three variables and 8 rows, 7 of them true (only all three true makes it false).

  1. class Node:
  2. def __init__(self, key):
  3. self.key = key
  4. self.left = None
  5. self.right = None
  6. def satisfiable(formula):
  7. root = parse(formula)
  8. if root is None:
  9. return None
  10. names = []
  11. collect(root, names)
  12. names.sort()
  13. n = len(names)
  14. rows = [names + ["value"]]
  15. models = 0
  16. for m in range(2 ** n):
  17. env = {}
  18. for i in range(n):
  19. env[names[i]] = (m >> (n - 1 - i)) % 2 == 1
  20. value = evaluate(root, env)
  21. row = []
  22. for name in names:
  23. row.append("T" if env[name] else "F")
  24. row.append("T" if value else "F")
  25. rows.append(row)
  26. if value:
  27. models = models + 1
  28. return models
  29. def evaluate(node, env):
  30. # Postorder: both children first, then the operator at this node.
  31. if node.key in env:
  32. v = env[node.key]
  33. return v
  34. right = evaluate(node.right, env)
  35. if node.key == "¬":
  36. v = not right
  37. return v
  38. left = evaluate(node.left, env)
  39. if node.key == "∧":
  40. v = left & right
  41. else:
  42. v = left | right
  43. return v
  44. def collect(node, names):
  45. if node is None:
  46. return
  47. if node.key.isalpha() and node.key not in names:
  48. names.append(node.key)
  49. collect(node.left, names)
  50. collect(node.right, names)
  51. def parse(text):
  52. # | is or, & is and, ~ is not; ~ binds tightest and | loosest.
  53. toks = []
  54. for c in text:
  55. if c != " ":
  56. toks.append(c)
  57. pos = [0]
  58. root = parse_or(toks, pos)
  59. if pos[0] != len(toks):
  60. return None
  61. return root
  62. def parse_or(toks, pos):
  63. left = parse_and(toks, pos)
  64. while left is not None and pos[0] < len(toks) and toks[pos[0]] == "|":
  65. pos[0] += 1
  66. left = join("∨", left, parse_and(toks, pos))
  67. return left
  68. def parse_and(toks, pos):
  69. left = parse_not(toks, pos)
  70. while left is not None and pos[0] < len(toks) and toks[pos[0]] == "&":
  71. pos[0] += 1
  72. left = join("∧", left, parse_not(toks, pos))
  73. return left
  74. def parse_not(toks, pos):
  75. if pos[0] >= len(toks):
  76. return None
  77. c = toks[pos[0]]
  78. pos[0] += 1
  79. if c == "~":
  80. return join("¬", None, parse_not(toks, pos))
  81. if c == "(":
  82. inner = parse_or(toks, pos)
  83. if inner is None or pos[0] >= len(toks) or toks[pos[0]] != ")":
  84. return None
  85. pos[0] += 1
  86. return inner
  87. if c.isalpha():
  88. return Node(c)
  89. return None
  90. def join(op, left, right):
  91. if right is None:
  92. return None
  93. node = Node(op)
  94. node.left = left
  95. node.right = right
  96. return node

Invariant: Each node's value is computed only after its children's (postorder).

Start

∅

models—

The table is still empty.

Recursion tree

Filled orange = running call · Orange outline = still on the stack · Grey = finished

Local variables

formula'(a&~b)|(~a&c)|~c'

Orange = changed in this step

Call stack

  1. satisfiable()line 8
Step 1 of 1,115
Black = row · Orange = key stepparsed: 0/1 · variable: 0/40 · negate: 0/24 · conj: 0/16 · disj: 0/16 · row: 0/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.