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
# 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