Find Eventual Safe States
Problem statement
You get a directed graph with n nodes labelled 0 to n - 1, as a list graph where graph[i] holds the nodes that node i has an edge to.
A node with no outgoing edges is a terminal node. A node is safe if every possible path starting from it eventually ends at a terminal node; in other words, no path from it can get stuck going round a cycle.
Return all safe nodes in ascending order.
Examples
Example 1
Input: graph = [[1, 2], [2], [], [0, 4], [4]]
Output: [0, 1, 2]
Explanation: Node 2 is terminal. Nodes 0 and 1 can only lead to 2. Node 4 points to itself, a cycle, and node 3 can go to 4, so both are unsafe.
Example 2
Input: graph = [[1], [2], [0, 3], []]
Output: [3]
Explanation: 0 -> 1 -> 2 -> 0 is a cycle, so 0, 1 and 2 are unsafe even though 2 also has an edge to the terminal node 3.
Hints
Approach
Peel safe nodes off from the terminals backwards, like a topological sort on the reversed graph.
- Build the reverse graph: for each edge
u -> v, recorduinparents[v]. Letoutdeg[u]be the number of edges leavingu. - Queue every node with
outdeg == 0(the terminals). They are safe. - Pop a safe node
v. For each parentu, one more of its edges is now known to lead somewhere safe, so decrementoutdeg[u]. When it hits zero, every edge fromuleads to a safe node, souis safe: queue it. - Nodes that never reach zero have an edge that leads, directly or indirectly, into a cycle. Return the safe nodes in index order.
Every node is queued at most once and every edge is relaxed once. Nothing recurses, so deep graphs are fine.
O(V + E)Space O(V + E)from collections import deque class Solution: def eventualSafeNodes(self, graph: list[list[int]]) -> list[int]: n = len(graph) parents = [[] for _ in range(n)] outdeg = [0] * n for u in range(n): outdeg[u] = len(graph[u]) for v in graph[u]: parents[v].append(u) queue = deque(u for u in range(n) if outdeg[u] == 0) safe = [False] * n while queue: v = queue.popleft() safe[v] = True for u in parents[v]: outdeg[u] -= 1 if outdeg[u] == 0: # every edge from u leads to a safe node queue.append(u) return [i for i in range(n) if safe[i]]Follow-up questions
- Return the cycles themselves, not just the unsafe nodes.
- Edges are added over time. How would you keep the safe set up to date?
Frequently asked questions
Yes: keep the colours across all starts instead of resetting them. A node that finished black is safe, and a node left grey when a cycle was found stays grey and is unsafe. That memoised three-colour DFS is also O(V + E), but it recurses, which can hit Python's recursion limit on long chains; the reverse-graph queue does not.
It is deadlock and loop analysis. Given a graph of which job waits on which, or which service retries into which, the unsafe nodes are the ones that can end up waiting forever in a cycle. The same check finds redirect loops, circular module imports, or workflow states that can never reach a finished state.