| """Helpers for manipulations with graphs.""" |
| |
| from __future__ import annotations |
| |
| from typing import AbstractSet, Iterable, Iterator, TypeVar |
| |
| T = TypeVar("T") |
| |
| |
| def strongly_connected_components( |
| vertices: AbstractSet[T], edges: dict[T, list[T]] |
| ) -> Iterator[set[T]]: |
| """Compute Strongly Connected Components of a directed graph. |
| |
| Args: |
| vertices: the labels for the vertices |
| edges: for each vertex, gives the target vertices of its outgoing edges |
| |
| Returns: |
| An iterator yielding strongly connected components, each |
| represented as a set of vertices. Each input vertex will occur |
| exactly once; vertices not part of a SCC are returned as |
| singleton sets. |
| |
| From https://code.activestate.com/recipes/578507/. |
| """ |
| identified: set[T] = set() |
| stack: list[T] = [] |
| index: dict[T, int] = {} |
| boundaries: list[int] = [] |
| |
| def dfs(v: T) -> Iterator[set[T]]: |
| index[v] = len(stack) |
| stack.append(v) |
| boundaries.append(index[v]) |
| |
| for w in edges[v]: |
| if w not in index: |
| yield from dfs(w) |
| elif w not in identified: |
| while index[w] < boundaries[-1]: |
| boundaries.pop() |
| |
| if boundaries[-1] == index[v]: |
| boundaries.pop() |
| scc = set(stack[index[v] :]) |
| del stack[index[v] :] |
| identified.update(scc) |
| yield scc |
| |
| for v in vertices: |
| if v not in index: |
| yield from dfs(v) |
| |
| |
| def prepare_sccs( |
| sccs: list[set[T]], edges: dict[T, list[T]] |
| ) -> dict[AbstractSet[T], set[AbstractSet[T]]]: |
| """Use original edges to organize SCCs in a graph by dependencies between them.""" |
| sccsmap = {v: frozenset(scc) for scc in sccs for v in scc} |
| data: dict[AbstractSet[T], set[AbstractSet[T]]] = {} |
| for scc in sccs: |
| deps: set[AbstractSet[T]] = set() |
| for v in scc: |
| deps.update(sccsmap[x] for x in edges[v]) |
| data[frozenset(scc)] = deps |
| return data |
| |
| |
| def topsort(data: dict[T, set[T]]) -> Iterable[set[T]]: |
| """Topological sort. |
| |
| Args: |
| data: A map from vertices to all vertices that it has an edge |
| connecting it to. NOTE: This data structure |
| is modified in place -- for normalization purposes, |
| self-dependencies are removed and entries representing |
| orphans are added. |
| |
| Returns: |
| An iterator yielding sets of vertices that have an equivalent |
| ordering. |
| |
| Example: |
| Suppose the input has the following structure: |
| |
| {A: {B, C}, B: {D}, C: {D}} |
| |
| This is normalized to: |
| |
| {A: {B, C}, B: {D}, C: {D}, D: {}} |
| |
| The algorithm will yield the following values: |
| |
| {D} |
| {B, C} |
| {A} |
| |
| From https://code.activestate.com/recipes/577413/. |
| """ |
| # TODO: Use a faster algorithm? |
| for k, v in data.items(): |
| v.discard(k) # Ignore self dependencies. |
| for item in set.union(*data.values()) - set(data.keys()): |
| data[item] = set() |
| while True: |
| ready = {item for item, dep in data.items() if not dep} |
| if not ready: |
| break |
| yield ready |
| data = {item: (dep - ready) for item, dep in data.items() if item not in ready} |
| assert not data, f"A cyclic dependency exists amongst {data!r}" |