802. Find Eventual Safe States
Problem Description
You are given a directed graph with n
nodes labeled from 0
to n - 1
. The graph is represented as a 2D array called graph
, where graph[i]
contains a list of all nodes that node i
has edges pointing to.
A terminal node is defined as a node with no outgoing edges (its adjacency list is empty).
A safe node is defined as a node where every possible path starting from it will eventually reach a terminal node. This means no matter which edges you follow from a safe node, you will always end up at a terminal node without getting stuck in a cycle.
Your task is to find all the safe nodes in the graph and return them as a sorted list in ascending order.
For example, if node i
has edges to nodes [j, k]
, then for node i
to be safe, both paths through j
and k
must eventually lead to terminal nodes. If any path from node i
could lead to a cycle or infinite loop, then node i
is not safe.
The solution uses a reverse graph approach with topological sorting. It builds a reverse graph where edges point in the opposite direction, then uses the out-degree (which becomes in-degree in the reverse graph) to identify safe nodes. Starting from terminal nodes (which have out-degree 0), it works backwards to find all nodes that can only reach terminal nodes.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly states we have a directed graph with
n
nodes and edges represented by the adjacency listgraph[i]
.
Is it a tree?
- No: The graph can have cycles and multiple paths between nodes. A tree would have no cycles and exactly one path between any two nodes.
Is the problem related to directed acyclic graphs?
- Yes: While the original graph may have cycles, the problem is essentially about identifying nodes that don't lead to cycles. We're looking for nodes that only lead to terminal nodes (no cycles). The solution approach treats this as a DAG problem by reversing the graph and finding nodes that can reach terminal nodes.
Topological Sort
- Yes: The flowchart leads us to Topological Sort, which is indeed used in the solution. The algorithm uses a modified topological sort on the reversed graph to identify safe nodes.
Conclusion: The flowchart correctly guides us to use Topological Sort for this problem. While DFS could also solve this problem (by detecting cycles), the given solution cleverly uses topological sorting on a reversed graph. It starts from terminal nodes (nodes with out-degree 0) and works backwards to find all nodes that only lead to these terminal nodes, effectively identifying all safe nodes.
Intuition
The key insight is to think about this problem in reverse. Instead of checking if every path from a node leads to a terminal node, we can ask: "Which nodes can only be reached from terminal nodes?"
Consider that a terminal node is trivially safe - it has no outgoing edges, so all paths from it (none) lead to terminal nodes. Now, if a node only points to terminal nodes, it must also be safe. We can extend this reasoning: if a node only points to safe nodes, then it must be safe too.
This suggests working backwards from terminal nodes. However, in the original graph, it's hard to work backwards because we'd need to know which nodes point TO a given node. This is where the reverse graph comes in - by reversing all edges, we can easily track dependencies.
In the reversed graph:
- Terminal nodes in the original graph have in-degree 0 in the reversed graph
- A node is safe if all nodes it depends on (points to in original graph) are safe
This naturally leads to a topological sort approach:
- Start with nodes that have out-degree 0 (terminal nodes)
- These are definitely safe
- For each safe node we find, we can potentially make other nodes safe
- A node becomes safe when all its outgoing edges lead to safe nodes (when its out-degree becomes 0 after removing edges to safe nodes)
The brilliance of this approach is that it avoids cycle detection entirely. Nodes involved in cycles will never have their out-degree reduced to 0, so they naturally get filtered out. Only nodes that can reach terminal nodes without going through cycles will eventually have their out-degree become 0.
Learn more about Depth-First Search, Breadth-First Search, Graph and Topological Sort patterns.
Solution Approach
The implementation uses a reverse graph with topological sorting to identify safe nodes. Here's how the solution works step by step:
1. Build the Reverse Graph and Track Out-degrees
rg = defaultdict(list) # reverse graph
indeg = [0] * len(graph) # actually stores out-degree of original graph
rg[j]
stores all nodes that point to nodej
in the original graphindeg[i]
stores the out-degree of nodei
(number of outgoing edges)
2. Construct Reverse Graph
for i, vs in enumerate(graph):
for j in vs:
rg[j].append(i) # if i->j in original, then j->i in reverse
indeg[i] = len(vs) # count outgoing edges
For each edge i β j
in the original graph, we add edge j β i
in the reverse graph.
3. Initialize Queue with Terminal Nodes
q = deque([i for i, v in enumerate(indeg) if v == 0])
Terminal nodes have out-degree 0, so we start with them as they are guaranteed to be safe.
4. Process Nodes in Topological Order
while q: i = q.popleft() for j in rg[i]: indeg[j] -= 1 if indeg[j] == 0: q.append(j)
- When we process a safe node
i
, we look at all nodesj
that point toi
in the original graph - We decrement the out-degree of
j
because one of its outgoing edges (to nodei
) is confirmed to lead to a safe node - If all of
j
's outgoing edges lead to safe nodes (indeg[j] == 0
), thenj
is also safe
5. Collect Results
return [i for i, v in enumerate(indeg) if v == 0]
After processing, any node with out-degree 0 is safe. Nodes in cycles or leading to cycles will never have their out-degree reduced to 0.
Time Complexity: O(V + E)
where V is the number of nodes and E is the number of edges
Space Complexity: O(V + E)
for storing the reverse graph
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example Graph:
graph = [[1,2],[2,3],[5],[0],[5],[],[]]
This represents:
- Node 0 β [1, 2]
- Node 1 β [2, 3]
- Node 2 β [5]
- Node 3 β [0]
- Node 4 β [5]
- Node 5 β []
- Node 6 β []
Step 1: Build Reverse Graph and Track Out-degrees
First, we create the reverse graph and count out-degrees:
- Original edges: 0β1, 0β2, 1β2, 1β3, 2β5, 3β0, 4β5
- Reverse graph:
- rg[0] = [3] (3 points to 0)
- rg[1] = [0] (0 points to 1)
- rg[2] = [0, 1] (0 and 1 point to 2)
- rg[3] = [1] (1 points to 3)
- rg[5] = [2, 4] (2 and 4 point to 5)
- rg[6] = [] (nothing points to 6)
- Out-degrees: indeg = [2, 2, 1, 1, 1, 0, 0]
Step 2: Initialize Queue with Terminal Nodes
Terminal nodes have out-degree 0:
- Queue = [5, 6] (nodes 5 and 6 have no outgoing edges)
Step 3: Process Nodes in Topological Order
Process node 5:
- Look at rg[5] = [2, 4]
- Decrement indeg[2]: 1 β 0 (node 2 now safe!)
- Decrement indeg[4]: 1 β 0 (node 4 now safe!)
- Add nodes 2 and 4 to queue
- Queue = [6, 2, 4]
Process node 6:
- Look at rg[6] = [] (nothing to process)
- Queue = [2, 4]
Process node 2:
- Look at rg[2] = [0, 1]
- Decrement indeg[0]: 2 β 1
- Decrement indeg[1]: 2 β 1
- Neither reaches 0, so don't add to queue
- Queue = [4]
Process node 4:
- Look at rg[4] = [] (nothing to process)
- Queue = [] (empty)
Step 4: Identify Safe Nodes
After processing, check which nodes have out-degree 0:
- indeg = [1, 1, 0, 1, 0, 0, 0]
- Safe nodes: [2, 4, 5, 6]
Why This Works:
- Nodes 5 and 6 are terminal nodes (trivially safe)
- Node 2 only points to node 5 (a terminal node), so it's safe
- Node 4 only points to node 5 (a terminal node), so it's safe
- Nodes 0, 1, and 3 form a cycle: 0β1β3β0
- Their out-degrees never reach 0 because they depend on each other
- They are correctly identified as unsafe
The algorithm efficiently identifies all safe nodes without explicitly detecting cycles. Nodes in cycles naturally get filtered out because their dependencies can never be fully resolved.
Solution Implementation
1class Solution:
2 def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]:
3 # Build reverse graph: for each node, store which nodes point to it
4 reverse_graph = defaultdict(list)
5
6 # Track out-degree for each node (number of outgoing edges)
7 out_degree = [0] * len(graph)
8
9 # Construct reverse graph and count out-degrees
10 for node, neighbors in enumerate(graph):
11 for neighbor in neighbors:
12 reverse_graph[neighbor].append(node)
13 out_degree[node] = len(neighbors)
14
15 # Initialize queue with terminal nodes (nodes with no outgoing edges)
16 queue = deque([node for node, degree in enumerate(out_degree) if degree == 0])
17
18 # Process nodes in reverse topological order
19 while queue:
20 current_node = queue.popleft()
21
22 # For each node that points to current_node in the original graph
23 for predecessor in reverse_graph[current_node]:
24 out_degree[predecessor] -= 1
25
26 # If predecessor now has no unprocessed outgoing edges, it's safe
27 if out_degree[predecessor] == 0:
28 queue.append(predecessor)
29
30 # Return all nodes with out-degree 0 (eventual safe nodes)
31 return [node for node, degree in enumerate(out_degree) if degree == 0]
32
1class Solution {
2 public List<Integer> eventualSafeNodes(int[][] graph) {
3 int nodeCount = graph.length;
4
5 // Track the out-degree of each node (number of outgoing edges)
6 int[] outDegree = new int[nodeCount];
7
8 // Build reverse graph: reverseGraph[i] contains all nodes that point to node i
9 List<Integer>[] reverseGraph = new List[nodeCount];
10 Arrays.setAll(reverseGraph, index -> new ArrayList<>());
11
12 // Queue for BFS traversal, starting with terminal nodes (out-degree = 0)
13 Deque<Integer> queue = new ArrayDeque<>();
14
15 // Build reverse graph and initialize out-degrees
16 for (int node = 0; node < nodeCount; ++node) {
17 // For each edge from 'node' to 'neighbor'
18 for (int neighbor : graph[node]) {
19 // Add reverse edge: neighbor -> node
20 reverseGraph[neighbor].add(node);
21 }
22
23 // Set out-degree for current node
24 outDegree[node] = graph[node].length;
25
26 // If node is terminal (no outgoing edges), add to queue
27 if (outDegree[node] == 0) {
28 queue.offer(node);
29 }
30 }
31
32 // Process nodes in reverse topological order
33 // Starting from terminal nodes, work backwards
34 while (!queue.isEmpty()) {
35 int safeNode = queue.pollFirst();
36
37 // For each node that points to the current safe node
38 for (int predecessor : reverseGraph[safeNode]) {
39 // Decrement out-degree of predecessor
40 // If it becomes 0, all paths from it lead to terminal nodes
41 if (--outDegree[predecessor] == 0) {
42 queue.offer(predecessor);
43 }
44 }
45 }
46
47 // Collect all safe nodes (nodes with out-degree 0 after processing)
48 List<Integer> safeNodes = new ArrayList<>();
49 for (int node = 0; node < nodeCount; ++node) {
50 // Nodes with out-degree 0 are eventually safe
51 if (outDegree[node] == 0) {
52 safeNodes.add(node);
53 }
54 }
55
56 return safeNodes;
57 }
58}
59
1class Solution {
2public:
3 vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
4 int n = graph.size();
5
6 // Track out-degree for each node (number of outgoing edges)
7 vector<int> outDegree(n);
8
9 // Build reverse graph where edges point in opposite direction
10 vector<vector<int>> reverseGraph(n);
11
12 // Queue for BFS traversal
13 queue<int> bfsQueue;
14
15 // Build reverse graph and initialize out-degrees
16 for (int node = 0; node < n; ++node) {
17 // For each neighbor of current node in original graph
18 for (int neighbor : graph[node]) {
19 // Add reverse edge from neighbor to current node
20 reverseGraph[neighbor].push_back(node);
21 }
22
23 // Set out-degree as number of neighbors in original graph
24 outDegree[node] = graph[node].size();
25
26 // Terminal nodes (out-degree 0) are immediately safe
27 if (outDegree[node] == 0) {
28 bfsQueue.push(node);
29 }
30 }
31
32 // Process nodes in reverse topological order
33 while (!bfsQueue.empty()) {
34 int currentNode = bfsQueue.front();
35 bfsQueue.pop();
36
37 // For each node that points to current node in original graph
38 for (int predecessor : reverseGraph[currentNode]) {
39 // Decrement out-degree and check if it becomes terminal
40 if (--outDegree[predecessor] == 0) {
41 bfsQueue.push(predecessor);
42 }
43 }
44 }
45
46 // Collect all safe nodes (nodes with out-degree 0 after processing)
47 vector<int> safeNodes;
48 for (int node = 0; node < n; ++node) {
49 if (outDegree[node] == 0) {
50 safeNodes.push_back(node);
51 }
52 }
53
54 return safeNodes;
55 }
56};
57
1/**
2 * Find all nodes that eventually lead to terminal nodes (safe nodes)
3 * A node is terminal if it has no outgoing edges
4 * A node is safe if every path starting from that node leads to a terminal node
5 *
6 * @param graph - Adjacency list representation where graph[i] contains all nodes that node i points to
7 * @returns Array of safe node indices in ascending order
8 */
9function eventualSafeNodes(graph: number[][]): number[] {
10 const nodeCount: number = graph.length;
11
12 // Build reverse graph: reverseGraph[i] contains all nodes that point to node i
13 const reverseGraph: number[][] = new Array(nodeCount).fill(0).map(() => new Array<number>());
14
15 // Track out-degree for each node (number of outgoing edges)
16 const outDegree: number[] = new Array(nodeCount).fill(0);
17
18 // Queue for BFS traversal, starting with terminal nodes
19 const queue: number[] = [];
20
21 // Build reverse graph and initialize out-degrees
22 for (let currentNode = 0; currentNode < nodeCount; ++currentNode) {
23 // For each neighbor of the current node
24 for (let neighbor of graph[currentNode]) {
25 // Add current node to the reverse graph of its neighbor
26 reverseGraph[neighbor].push(currentNode);
27 }
28
29 // Set out-degree as the number of outgoing edges
30 outDegree[currentNode] = graph[currentNode].length;
31
32 // If node has no outgoing edges (terminal node), add to queue
33 if (outDegree[currentNode] === 0) {
34 queue.push(currentNode);
35 }
36 }
37
38 // Process nodes in reverse topological order
39 while (queue.length > 0) {
40 const safeNode: number = queue.shift()!;
41
42 // For each node that points to the current safe node
43 for (let predecessor of reverseGraph[safeNode]) {
44 // Decrease out-degree since we found a safe path
45 if (--outDegree[predecessor] === 0) {
46 // If all outgoing paths lead to safe nodes, this node is also safe
47 queue.push(predecessor);
48 }
49 }
50 }
51
52 // Collect all safe nodes (nodes with out-degree 0 after processing)
53 const safeNodes: number[] = [];
54 for (let node = 0; node < nodeCount; ++node) {
55 if (outDegree[node] === 0) {
56 safeNodes.push(node);
57 }
58 }
59
60 return safeNodes;
61}
62
Time and Space Complexity
Time Complexity: O(V + E)
where V
is the number of nodes and E
is the total number of edges in the graph.
- Building the reverse graph requires iterating through all nodes and their edges:
O(V + E)
- Initializing the indegree array takes
O(V)
time - Finding initial nodes with indegree 0 requires
O(V)
time - The BFS/topological sort processes each node at most once and each edge at most once:
O(V + E)
- The final list comprehension to collect safe nodes takes
O(V)
time - Overall:
O(V + E) + O(V) + O(V) + O(V + E) + O(V) = O(V + E)
Space Complexity: O(V + E)
- The reverse graph
rg
stores all edges in reversed direction:O(E)
space - The
indeg
array stores the out-degree for each node:O(V)
space - The queue
q
can contain at mostV
nodes:O(V)
space - The output list contains at most
V
nodes:O(V)
space - Overall:
O(E) + O(V) + O(V) + O(V) = O(V + E)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Out-degree Tracking
Pitfall: A common mistake is thinking that indeg
or out_degree
represents the in-degree of nodes in the reverse graph, when it actually tracks the out-degree of nodes in the original graph.
Why it happens: The variable naming can be confusing since we're building a reverse graph but tracking original out-degrees.
Incorrect approach:
# Wrong: Trying to track in-degrees of reverse graph
indeg = [0] * len(graph)
for node, neighbors in enumerate(graph):
for neighbor in neighbors:
indeg[node] += 1 # This is wrong!
Correct approach:
# Right: Track out-degrees of original graph
out_degree = [0] * len(graph)
for node, neighbors in enumerate(graph):
out_degree[node] = len(neighbors) # Count outgoing edges in original
2. Forgetting to Handle Self-loops Properly
Pitfall: Not recognizing that nodes with self-loops can never be safe nodes, but the algorithm handles this automatically.
Example scenario: If node 2 has an edge to itself graph[2] = [2, 3]
, some might think special handling is needed.
Why the algorithm works: A node with a self-loop will never have its out-degree reduced to 0 because it creates a cycle. The self-loop means the node will always have at least one outgoing edge that doesn't lead to a terminal node.
3. Attempting to Use Standard DFS Without Proper Cycle Detection
Pitfall: Trying to solve this with a simple DFS that doesn't properly track visited states.
Incorrect approach:
def isSafe(node, visited):
if not graph[node]: # terminal node
return True
visited.add(node)
for neighbor in graph[node]:
if neighbor in visited or not isSafe(neighbor, visited):
return False
return True
Problem: This doesn't properly handle the case where different paths from a node might revisit the same nodes without forming a cycle.
Better DFS approach (if not using topological sort):
def isSafe(node, color):
if color[node] != 0: # Already processed
return color[node] == 2
color[node] = 1 # Mark as being processed (gray)
for neighbor in graph[node]:
if not isSafe(neighbor, color):
return False
color[node] = 2 # Mark as safe (black)
return True
# Use three colors: 0 (white/unvisited), 1 (gray/processing), 2 (black/safe)
color = [0] * len(graph)
return [i for i in range(len(graph)) if isSafe(i, color)]
4. Not Recognizing That Order Matters in the Result
Pitfall: Forgetting to return the safe nodes in ascending order.
Wrong:
# If using a set to collect results
safe_nodes = set()
# ... processing ...
return list(safe_nodes) # Order not guaranteed!
Correct:
# The enumerate approach naturally maintains order
return [node for node, degree in enumerate(out_degree) if degree == 0]
5. Incorrectly Initializing the Queue
Pitfall: Adding nodes to the initial queue based on in-degree in the reverse graph rather than out-degree in the original graph.
Wrong:
# Counting in-degrees of reverse graph for initialization
indeg_reverse = [0] * len(graph)
for node, neighbors in enumerate(graph):
for neighbor in neighbors:
indeg_reverse[neighbor] += 1
queue = deque([i for i, v in enumerate(indeg_reverse) if v == 0])
Correct:
# Initialize with terminal nodes (out-degree 0 in original)
queue = deque([node for node, degree in enumerate(out_degree) if degree == 0])
These terminal nodes are the foundation of safety - they guarantee termination of any path, which is why we start our backward traversal from them.
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!