802. Find Eventual Safe States
On LeetCode ->Problem¶
Given a directed graph, return all nodes from which every path eventually reaches a node with no outgoing edges. Return them in ascending order.
Key trick¶
Reverse the edges and run a topological process from terminal nodes. Whenever all outgoing edges of a node have been eliminated, that node is safe too.
Trap¶
- An unsafe node does not have to belong to a cycle; reaching any cycle makes it unsafe.
- Because every path must terminate, one outgoing edge leading to an unsafe node makes the source unsafe.
- DFS implementations need distinct states for unvisited, currently visiting, safe, and unsafe; a single
seenset is insufficient for memoization.
Why is it interesting?¶
It converts an all-paths cycle problem into reverse topological sorting: safe nodes are exactly those eliminated by repeatedly removing terminal nodes.
Python solution¶
from collections import deque
class Solution:
def eventualSafeNodes(self, graph: list[list[int]]) -> list[int]:
n = len(graph)
predecessors = [[] for _ in range(n)]
outdegree = [len(neighbors) for neighbors in graph]
# Reverse every edge so safety can propagate from terminal nodes.
for node, neighbors in enumerate(graph):
for nei in neighbors:
predecessors[nei].append(node)
q = deque(node for node in range(n) if outdegree[node] == 0)
safe = [False] * n
while q:
node = q.popleft()
safe[node] = True
# Removing a safe outgoing edge may make its predecessor safe.
for prev in predecessors[node]:
outdegree[prev] -= 1
if outdegree[prev] == 0:
q.append(prev)
# Scanning node labels guarantees ascending order.
return [node for node in range(n) if safe[node]]
Time complexity: \(O(V + E)\).
Space complexity: \(O(V + E)\).
Comment on my solution¶
The main issue is that unsafety discovered recursively is not propagated back to the caller.
For example, when exploring 0 -> 1 and node 1 is found unsafe, the caller never checks safe[1] again after dfs(1) returns, so node 0 can incorrectly remain safe.
Additionally:
seencorrectly represents the current DFS path, but it does not memoize completed nodes.- Unsafe nodes include nodes that can reach cycles, not only nodes inside cycles.
- Starting a fresh DFS from every node can take \(O(V(V + E))\) time.
- Recursive DFS can exceed Python's recursion limit for \(10^4\) nodes.
# Ideas:
# - It seems that "not safe" means contains a cycle
# - So we can try do cycle detection to discard nodes
# - the complement nodes are then safe nodes
# Wrong Answer: 58/113 testcases passed
# graph = [[1,2,3,4],[1,2,3,4],[3,4],[4],[]]
# Output: [0,2,3,4]
# Expected: [2,3,4]
class Solution:
def eventualSafeNodes(self, graph: list[list[int]]) -> list[int]:
n = len(graph)
safe = [True] * n
def dfs(node):
seen.add(node)
for nei in graph[node]:
if nei in seen or not safe[nei]:
safe[node] = False
break
dfs(nei)
seen.remove(node)
for node in range(n):
seen = set()
dfs(node)
res = []
for node in range(n):
if safe[node]:
res.append(node)
return res