| # Adapted from mypy (mypy/build.py) under the MIT license. | |
| from collections.abc import Iterable, Iterator, Set | |
| def strongly_connected_components( | |
| vertices: Set[str], edges: dict[str, Set[str]] | |
| ) -> Iterator[Set[str]]: | |
| """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-strongly-connected-components-of-a-directed-graph/. | |
| """ | |
| identified: set[str] = set() | |
| stack: list[str] = [] | |
| index: dict[str, int] = {} | |
| boundaries: list[int] = [] | |
| def dfs(v: str) -> Iterator[set[str]]: | |
| 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 find_cycles_in_scc( | |
| graph: dict[str, Set[str]], scc: Set[str], start: str | |
| ) -> Iterable[list[str]]: | |
| """Find cycles in SCC emanating from start. | |
| Yields lists of the form ['A', 'B', 'C', 'A'], which means there's | |
| a path from A -> B -> C -> A. The first item is always the start | |
| argument, but the last item may be another element, e.g. ['A', | |
| 'B', 'C', 'B'] means there's a path from A to B and there's a | |
| cycle from B to C and back. | |
| """ | |
| # Basic input checks. | |
| assert start in scc, (start, scc) | |
| assert scc <= graph.keys(), scc - graph.keys() | |
| # Reduce the graph to nodes in the SCC. | |
| graph = {src: {dst for dst in dsts if dst in scc} for src, dsts in graph.items() if src in scc} | |
| assert start in graph | |
| # Recursive helper that yields cycles. | |
| def dfs(node: str, path: list[str]) -> Iterator[list[str]]: | |
| if node in path: | |
| yield path + [node] | |
| return | |
| path = path + [node] # TODO: Make this not quadratic. | |
| for child in graph[node]: | |
| yield from dfs(child, path) | |
| yield from dfs(start, []) |