Algorithm Search⌘K

Incremental topological ordering algorithm in Python

Maintains a topological order under edge insertions while detecting cycles.

File
incremental_topological_ordering.py, 66 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
incremental_topological_ordering.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/incremental-topological-ordering/. The authors are listed in prov/D5SS95.jsonl.

"""Incremental topological ordering by local reordering (Pearce and Kelly).

The structure keeps a bijection between the vertices and the positions
0, ..., n - 1 such that every edge goes from a smaller to a larger position.
Inserting an edge (u, v) with pos(u) > pos(v) searches forward from v among
the positions up to pos(u) and backward from u among the positions from
pos(v). If the forward search reaches u, the edge closes a cycle and is
rejected. Otherwise the two visited sets swap places within the positions they
already use. Insertions only; deterministic; O(K log K) time per insertion,
where K counts the visited vertices and their edges.
"""


class IncrementalTopologicalOrder:
    def __init__(self, n: int) -> None:
        self.n = n
        self.out: list[list[int]] = [[] for _ in range(n)]
        self.inn: list[list[int]] = [[] for _ in range(n)]
        self.pos = list(range(n))  # position of each vertex
        self.at = list(range(n))   # vertex at each position

    def add_edge(self, u: int, v: int) -> bool:
        """Insert (u, v); return False, and leave the graph as it was, on a cycle."""
        if u == v:
            return False
        lo, hi = self.pos[v], self.pos[u]
        if hi < lo:  # @line L1
            self.out[u].append(v)
            self.inn[v].append(u)
            return True
        # Forward search from v, inside the window of positions up to pos(u).
        fwd = {v}
        stack = [v]
        while stack:  # @line L2
            x = stack.pop()
            for y in self.out[x]:
                if y == u:
                    return False  # @line L3
                if y not in fwd and self.pos[y] < hi:
                    fwd.add(y)
                    stack.append(y)
        # Backward search from u, inside the window of positions from pos(v).
        bwd = {u}
        stack = [u]
        while stack:  # @line L4
            x = stack.pop()
            for y in self.inn[x]:
                if y not in bwd and self.pos[y] > lo:
                    bwd.add(y)
                    stack.append(y)
        # Reorder: the backward set first, then the forward set, in the old slots.
        b = sorted(bwd, key=self.pos.__getitem__)  # @line L5
        f = sorted(fwd, key=self.pos.__getitem__)
        slots = sorted(self.pos[x] for x in b + f)
        for x, p in zip(b + f, slots):  # @line L6
            self.pos[x] = p
            self.at[p] = x
        self.out[u].append(v)
        self.inn[v].append(u)
        return True

    def order(self) -> list[int]:
        return list(self.at)  # @line L7
test_incremental_topological_ordering.py 68 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/incremental-topological-ordering/. The authors are listed in prov/D5SS95.jsonl.

import random

from incremental_topological_ordering import IncrementalTopologicalOrder


def _reaches(n: int, edges: list[tuple[int, int]], s: int, t: int) -> bool:
    adj: list[list[int]] = [[] for _ in range(n)]
    for a, b in edges:
        adj[a].append(b)
    seen = {s}
    st = [s]
    while st:
        x = st.pop()
        if x == t:
            return True
        for y in adj[x]:
            if y not in seen:
                seen.add(y)
                st.append(y)
    return False


def _valid(g: IncrementalTopologicalOrder, edges) -> None:
    order = g.order()
    assert sorted(order) == list(range(g.n))
    for i, x in enumerate(order):
        assert g.pos[x] == i
    for a, b in edges:
        assert g.pos[a] < g.pos[b]


def test_example() -> None:
    g = IncrementalTopologicalOrder(6)
    for e in [(0, 1), (1, 2), (4, 5), (5, 1)]:
        assert g.add_edge(*e)
    assert g.order() == [0, 4, 5, 3, 1, 2]
    assert g.add_edge(3, 4)
    assert g.order() == [0, 3, 4, 5, 1, 2]
    assert not g.add_edge(2, 3)
    assert g.order() == [0, 3, 4, 5, 1, 2]


def test_random_against_static() -> None:
    rng = random.Random(9)
    for _ in range(500):
        n = rng.randint(1, 9)
        g = IncrementalTopologicalOrder(n)
        edges: list[tuple[int, int]] = []
        for _ in range(rng.randint(0, 30)):
            u, v = rng.randrange(n), rng.randrange(n)
            expect = u != v and not _reaches(n, edges, v, u)
            assert g.add_edge(u, v) == expect
            if expect:
                edges.append((u, v))
            _valid(g, edges)


def test_reversed_chain() -> None:
    n = 1000
    g = IncrementalTopologicalOrder(n)
    edges = [(i + 1, i) for i in range(n - 1)]
    for e in edges:
        assert g.add_edge(*e)
    assert g.order() == list(range(n - 1, -1, -1))
    assert not g.add_edge(0, n - 1)

What the next level needs

  • the tests did not run in this build

Read the full page about Incremental topological ordering algorithm