Dominator tree in Python
Tree in which each vertex's parent is its immediate dominator in a flow graph.
- File
dominator_tree.py, 139 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dominator-tree/. The authors are listed in prov/GK1MNC.jsonl.
def _immediate_dominators(n: int, edges: list[tuple[int, int]], root: int) -> list[int]:
"""Immediate dominators by the simple Lengauer-Tarjan algorithm, O(m log n).
Returns idom with idom[root] = -1 and idom[v] = -1 for vertices that the
root cannot reach. Iterative, so long paths do not hit the recursion limit.
"""
succ: list[list[int]] = [[] for _ in range(n)]
pred: list[list[int]] = [[] for _ in range(n)]
for a, b in edges:
if not (0 <= a < n and 0 <= b < n):
raise ValueError("edge endpoint out of range")
succ[a].append(b)
pred[b].append(a)
dfn = [-1] * n
vertex = [root]
parent = [-1]
dfn[root] = 0
stack = [(root, 0)]
while stack:
v, i = stack[-1]
if i < len(succ[v]):
stack[-1] = (v, i + 1)
w = succ[v][i]
if dfn[w] == -1:
dfn[w] = len(vertex)
vertex.append(w)
parent.append(dfn[v])
stack.append((w, 0))
else:
stack.pop()
k = len(vertex)
semi = list(range(k))
idom = [0] * k
ancestor = [-1] * k
label = list(range(k))
bucket: list[list[int]] = [[] for _ in range(k)]
def evaluate(v: int) -> int:
if ancestor[v] == -1:
return v
path = []
x = v
while ancestor[ancestor[x]] != -1:
path.append(x)
x = ancestor[x]
for x in reversed(path):
a = ancestor[x]
if semi[label[a]] < semi[label[x]]:
label[x] = label[a]
ancestor[x] = ancestor[a]
return label[v]
for w in range(k - 1, 0, -1):
for u0 in pred[vertex[w]]:
if dfn[u0] == -1:
continue
u = evaluate(dfn[u0])
if semi[u] < semi[w]:
semi[w] = semi[u]
bucket[semi[w]].append(w)
p = parent[w]
ancestor[w] = p
for v in bucket[p]:
u = evaluate(v)
idom[v] = u if semi[u] < semi[v] else p
bucket[p] = []
for w in range(1, k):
if idom[w] != semi[w]:
idom[w] = idom[idom[w]]
result = [-1] * n
for w in range(1, k):
result[vertex[w]] = vertex[idom[w]]
return result
class DominatorTree:
"""The dominator tree of a flow graph.
The graph is directed and unweighted, with vertices 0 .. n-1 (n >= 1),
given by a list of edges (a, b) meaning a -> b, and a start vertex root.
Vertices the root cannot reach are not in the tree. Building takes
O(m log n) time; dominates(a, b) takes O(1) time.
"""
def __init__(self, n: int, edges: list[tuple[int, int]], root: int = 0) -> None:
if n < 1 or not 0 <= root < n:
raise ValueError("a flow graph needs n >= 1 vertices and a root in range")
self.n = n
self.root = root
self.idom = _immediate_dominators(n, edges, root) # @line L1
children: list[list[int]] = [[] for _ in range(n)]
self.reachable = [False] * n
self.reachable[root] = True
for v in range(n): # @line L2
if self.idom[v] != -1:
children[self.idom[v]].append(v)
self.reachable[v] = True
self.children = children
self.tin = [-1] * n
self.tout = [-1] * n
self.depth = [-1] * n
t = 0 # @line L3
self.depth[root] = 0
stack = [(root, True)]
while stack: # @line L4
v, entering = stack.pop() # @line L5
if entering: # @line L6
self.tin[v] = t
t += 1
stack.append((v, False))
for c in reversed(children[v]):
self.depth[c] = self.depth[v] + 1
stack.append((c, True))
else:
self.tout[v] = t # @line L7
def dominates(self, a: int, b: int) -> bool:
"""Return True if every path from the root to b passes through a."""
if not (0 <= a < self.n and 0 <= b < self.n):
raise IndexError("vertex out of range")
if not (self.reachable[a] and self.reachable[b]): # @line L8
return False
return self.tin[a] <= self.tin[b] and self.tout[b] <= self.tout[a] # @line L9
def dominators(self, v: int) -> list[int]:
"""Return the dominators of v, from v up to the root."""
if not 0 <= v < self.n:
raise IndexError("vertex out of range")
if not self.reachable[v]:
return []
out = []
while v != -1: # @line L10
out.append(v)
v = self.idom[v]
return out # @line L11
test_dominator_tree.py 126 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/dominator-tree/. The authors are listed in prov/GK1MNC.jsonl.
import random
import pytest
from dominator_tree import DominatorTree
EDGES = [(0, 1), (1, 2), (2, 3), (2, 4), (3, 4), (3, 7), (4, 5), (5, 6), (6, 3), (7, 8), (8, 6)]
def reach(n: int, edges: list[tuple[int, int]], root: int, banned: int) -> set[int]:
succ: list[list[int]] = [[] for _ in range(n)]
for a, b in edges:
succ[a].append(b)
if banned == root:
return set()
seen = {root}
stack = [root]
while stack:
v = stack.pop()
for w in succ[v]:
if w != banned and w not in seen:
seen.add(w)
stack.append(w)
return seen
def brute_dominators(n: int, edges: list[tuple[int, int]], root: int) -> dict[int, set[int]]:
full = reach(n, edges, root, -1)
dom = {v: {v} for v in full}
for d in range(n):
if d in full:
r = reach(n, edges, root, d)
for v in full:
if v not in r:
dom[v].add(d)
return dom
def test_page_example() -> None:
t = DominatorTree(9, EDGES, 0)
assert t.idom == [-1, 0, 1, 2, 2, 4, 2, 3, 7]
assert t.tin == [0, 1, 2, 3, 6, 7, 8, 4, 5]
assert t.tout == [9, 9, 9, 6, 8, 8, 9, 6, 6]
assert t.dominates(3, 8)
assert not t.dominates(4, 6)
assert not t.dominates(3, 6)
assert t.dominates(2, 6)
assert t.dominates(6, 6)
assert t.dominators(8) == [8, 7, 3, 2, 1, 0]
assert t.dominators(6) == [6, 2, 1, 0]
def test_single_vertex() -> None:
t = DominatorTree(1, [], 0)
assert t.idom == [-1]
assert t.dominates(0, 0)
assert t.dominators(0) == [0]
def test_star_and_diamond() -> None:
n = 7
edges = [(0, v) for v in range(1, n)] + [(1, 2), (2, 3), (5, 4)]
t = DominatorTree(n, edges, 0)
assert t.idom == [-1] + [0] * (n - 1)
t = DominatorTree(4, [(0, 1), (0, 2), (1, 3), (2, 3)], 0)
assert t.idom == [-1, 0, 0, 0]
assert not t.dominates(1, 3)
def test_unreachable_and_loops() -> None:
t = DominatorTree(5, [(0, 1), (1, 1), (1, 0), (3, 4), (4, 1)], 0)
assert t.idom == [-1, 0, -1, -1, -1]
assert t.reachable == [True, True, False, False, False]
assert not t.dominates(0, 3)
assert t.dominators(4) == []
def test_long_path() -> None:
n = 100_000
edges = [(i, i + 1) for i in range(n - 1)]
t = DominatorTree(n, edges, 0)
assert t.idom == [-1] + list(range(n - 1))
assert t.dominates(10, n - 1)
assert not t.dominates(n - 1, 10)
edges2 = edges + [(i, i + 2) for i in range(n - 2)]
t = DominatorTree(n, edges2, 0)
# a path with a bypass edge around every vertex: nothing but the root dominates
assert all(t.idom[v] == 0 for v in range(1, n))
# a long cycle back to the root
t = DominatorTree(n, edges + [(n - 1, 0)], 0)
assert t.idom[n - 1] == n - 2
def test_bad_input() -> None:
with pytest.raises(ValueError):
DominatorTree(0, [], 0)
with pytest.raises(ValueError):
DominatorTree(2, [(0, 2)], 0)
t = DominatorTree(2, [(0, 1)], 0)
with pytest.raises(IndexError):
t.dominates(0, 5)
def test_random_against_brute_force() -> None:
rng = random.Random(13)
for _ in range(600):
n = rng.randint(1, 12)
m = rng.randint(0, 3 * n)
edges = [(rng.randrange(n), rng.randrange(n)) for _ in range(m)]
root = rng.randrange(n)
t = DominatorTree(n, edges, root)
dom = brute_dominators(n, edges, root)
for v in range(n):
if v not in dom:
assert t.idom[v] == -1 and not t.reachable[v]
continue
assert set(t.dominators(v)) == dom[v]
if v != root:
strict = dom[v] - {v}
assert t.idom[v] in strict
assert all(d in dom[t.idom[v]] for d in strict)
for a in range(n):
assert t.dominates(a, v) == (a in dom[v])
What the next level needs
- the tests did not run in this build