Algorithm Search⌘K

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
dominator_tree.pyRawGitHub
# 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

Read the full page about Dominator tree