72 lines
2.0 KiB
Python
72 lines
2.0 KiB
Python
"""Deterministic topological sort (Kahn's algorithm) with cycle detection.
|
|
|
|
Nodes are arbitrary hashable values. Input is an iterable of (node, predecessors)
|
|
pairs where predecessors are the nodes that must come before node. Ordering
|
|
among independent nodes is stable by input order of first appearance (earliest
|
|
discovered zero-indegree / ready node wins).
|
|
|
|
Returns a list of nodes in topological order. Raises CycleError if the graph
|
|
contains a cycle, listing the remaining (unsorted) nodes.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import heapq
|
|
from collections import OrderedDict
|
|
|
|
|
|
class CycleError(Exception):
|
|
def __init__(self, remaining):
|
|
self.remaining = tuple(remaining)
|
|
super().__init__("cycle detected among: %r" % (self.remaining,))
|
|
|
|
|
|
def topo_sort(edges):
|
|
order = OrderedDict() # node -> discovery index
|
|
preds = {}
|
|
succs = {}
|
|
indeg = {}
|
|
|
|
idx = 0
|
|
def discover(n):
|
|
nonlocal idx
|
|
if n not in order:
|
|
order[n] = idx
|
|
idx += 1
|
|
preds[n] = set()
|
|
succs[n] = []
|
|
indeg[n] = 0
|
|
|
|
for node, predecessors in edges:
|
|
discover(node)
|
|
for p in predecessors:
|
|
discover(p)
|
|
if p not in preds[node]:
|
|
preds[node].add(p)
|
|
succs[p].append(node)
|
|
indeg[node] += 1
|
|
|
|
# Kahn with a min-heap keyed by discovery index for global stability.
|
|
heap = [(order[n], n) for n in order if indeg[n] == 0]
|
|
heapq.heapify(heap)
|
|
result = []
|
|
while heap:
|
|
_, n = heapq.heappop(heap)
|
|
result.append(n)
|
|
for s in succs[n]:
|
|
indeg[s] -= 1
|
|
if indeg[s] == 0:
|
|
heapq.heappush(heap, (order[s], s))
|
|
|
|
if len(result) != len(order):
|
|
remaining = [n for n in order if n not in set(result)]
|
|
raise CycleError(remaining)
|
|
return result
|
|
|
|
|
|
def has_cycle(edges):
|
|
try:
|
|
topo_sort(edges)
|
|
return False
|
|
except CycleError:
|
|
return True
|