Editorial
Core insight
Find Eventual Safe States · Graph Traversal Patterns (DFS & BFS)
Core Insight for Find Eventual Safe States
The key intuition is that we can classify every node into one of three states during a DFS traversal. This is often called the 3-Color DFS technique.
- Unvisited (White/0): The node has not been processed yet.
- Visiting (Gray/1): The node is currently in the recursion stack (we are exploring its descendants). If we encounter a node in this state, we have found a cycle.
- Safe (Black/2): The node and all its descendants have been fully processed, and no cycle was found.
The invariant we maintain is this: A node is safe if and only if all its neighbors are safe.
If we start DFS from a node and encounter a node marked "Visiting" (Gray), we know a cycle exists. Consequently, the current node and all nodes in the current recursion stack are unsafe. If we finish processing a node and all its neighbors return "Safe" (or it has no neighbors), then the node itself is "Safe" (Black).
By persisting these states globally, we avoid re-calculating the safety of a node. If we encounter a "Safe" node, we return true immediately. If we encounter a "Visiting" node (which effectively becomes a marker for "Unsafe" in our final logic), we return false.
Visual Description: Imagine the DFS as a path growing from a source. As we step onto a node, we mark it "Visiting". We then step to its neighbor. If that path eventually loops back to a "Visiting" node, the entire active path is "poisoned" by the cycle. If the path reaches a dead end (terminal node), we backtrack, marking nodes as "Safe" only after verifying all their outgoing paths are safe.

Deep dive
Full breakdown
Full reasoning, pitfalls, and implementation flow
LeetCode 802: Find Eventual Safe States Solution & Explanation
Problem Overview
TL;DR: We perform a Depth-First Search (DFS) with 3-color state tracking (unvisited, visiting, safe) to detect cycles; any node that is part of or leads to a cycle is unsafe, while all others are safe.
The Find Eventual Safe States problem asks us to identify all nodes in a directed graph that are "safe." A node is considered safe if every possible path starting from it eventually leads to a terminal node (a node with no outgoing edges). In simpler terms, a node is safe if it is impossible to get stuck in a loop (cycle) starting from that node. If you start at a safe node and keep moving, you will guaranteed stop eventually.
This is a classic graph problem often appearing in technical interviews, including those at Amazon. It essentially boils down to identifying nodes that are not part of any cycle and do not lead to any cycle.
Brute Force Approach for Find Eventual Safe States
The naive approach is to simulate all possible paths from every single node to see if they terminate. For each node i from 0 to n-1, we perform a traversal (like DFS). If the traversal encounters a node that is currently in the recursion stack, we have found a cycle, meaning the starting node is not safe.
1# Pseudo-code for Brute Force
2def is_safe(node, visited):
3 if node in visited: return False # Cycle detected
4 visited.add(node)
5 for neighbor in graph[node]:
6 if not is_safe(neighbor, visited):
7 return False
8 visited.remove(node)
9 return True
10
11result = []
12for i in range(n):
13 if is_safe(i, set()):
14 result.append(i)
15return resultTime Complexity: . In the worst case, for every node, we might traverse a large portion of the graph. Why it fails: This approach results in a Time Limit Exceeded (TLE) error. The constraints allow up to nodes. Re-traversing the same paths repeatedly for different starting nodes performs highly redundant work. We need a way to remember (memoize) whether a node is safe or unsafe once we process it.
Algorithm Strategy: Graph Traversal Patterns (DFS & BFS)
- State Initialization: Create an array
colorof sizeninitialized to0(Unvisited). - Global Iteration: Iterate through every node
ifrom0ton-1. Ifcolor[i]is0, start a DFS fromi. - DFS Logic (Cycle Detection):
- Check State: If the current node
uis1(Visiting), a cycle is detected; returnfalse(unsafe). Ifuis2(Safe), returntrue. - Mark Visiting: Set
color[u] = 1. - Process Neighbors: Iterate through all neighbors
vofu.- If
color[v] == 2, skip (it's already safe). - If
color[v] == 1ORdfs(v)returnsfalse, thenuleads to a cycle. We leavecolor[u]as1(which we treat as unsafe) and returnfalse.
- If
- Mark Safe: If all neighbors are processed successfully without finding a cycle, mark
color[u] = 2and returntrue.
- Check State: If the current node
- Result Collection: After checking all nodes, collect all indices
iwherecolor[i] == 2into a sorted list.
Execution Flow
Let's trace the algorithm with a simple example: 0 -> 1 -> 0 (Cycle) and 2 -> 1.
- Start Loop:
i = 0.color[0]is0. Calldfs(0). - DFS(0): Mark
color[0] = 1. Neighbor is1. - DFS(1): Mark
color[1] = 1. Neighbor is0. - Check Neighbor 0:
color[0]is1. Cycle detected! Returnfalse. - Back to DFS(1): Received
falsefrom neighbor0. Returnfalse.color[1]remains1. - Back to DFS(0): Received
falsefrom neighbor1. Returnfalse.color[0]remains1. - Loop continues:
i = 1.color[1]is1. It was visited and determined unsafe. Skip or treat as processed. - Loop continues:
i = 2.color[2]is0. Calldfs(2). - DFS(2): Mark
color[2] = 1. Neighbor is1. - Check Neighbor 1:
color[1]is1. This means it is unsafe (either in stack or previously failed). Returnfalse. - Back to DFS(2): Received
falsefrom neighbor1. Returnfalse.color[2]remains1. - Final Result: No nodes marked
2. Result[].
Note: In this logic, state 1 serves double duty as "Visiting" and "Unsafe". State 2 is strictly "Safe".
Proof of Correctness
The correctness relies on the invariant of the 3-color DFS.
- Cycle Detection: A directed graph has a cycle if and only if a DFS encounters a back-edge to a node currently in the recursion stack (state
1). Our algorithm explicitly checks for this. - Propagation: If a node
uhas a neighborvthat is part of a cycle (or leads to one),dfs(v)will returnfalse. Consequently,dfs(u)will also returnfalse, correctly identifyinguas unsafe. - Terminal Nodes: A terminal node has no neighbors. The loop over neighbors completes immediately, the node is marked
2(Safe), andtrueis returned. This forms the base case for safety. - Completeness: By iterating
0ton-1, we ensure every component of the graph is processed. Memoization ensures each node is computed exactly once.
Pattern Reuse Notes
The Graph DFS - Cycle Detection pattern is a fundamental tool for solving directed graph problems involving dependencies or infinite loops.
- LeetCode 207: Course Schedule: This is the exact same pattern. A course schedule is valid if and only if the graph has no cycles.
- LeetCode 210: Course Schedule II: Similar to Course Schedule I, but requires returning the topological sort order if no cycle exists.
- LeetCode 1059: All Paths from Source Lead to Destination: Requires ensuring all paths from a source end at a specific destination, which implicitly requires cycle detection.
Feynman coach · Before you peek at the code
Can you explain the core insight in your own words?
If you can explain it before reading code, you’re far more likely to recall it under interview pressure.
Reference Implementation
C++ Solution for LeetCode 802
1class Solution {
2public:
3 // 0: Unvisited
4 // 1: Visiting (in stack) or Unsafe
5 // 2: Safe
6 bool dfs(int node, vector<vector<int>>& graph, vector<int>& color) {
7 if (color[node] != 0) {
8 return color[node] == 2;
9 }
10
11 color[node] = 1; // Mark as visiting
12
13 for (int neighbor : graph[node]) {
14 if (color[neighbor] == 2) continue; // Safe node, skip
15 if (color[neighbor] == 1 || !dfs(neighbor, graph, color)) {
16 return false; // Cycle detected or leads to unsafe node
17 }
18 }
19
20 color[node] = 2; // Mark as safe
21 return true;
22 }
23
24 vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
25 int n = graph.size();
26 vector<int> color(n, 0);
27 vector<int> safeNodes;
28
29 for (int i = 0; i < n; ++i) {
30 if (dfs(i, graph, color)) {
31 safeNodes.push_back(i);
32 }
33 }
34
35 return safeNodes;
36 }
37};Java Solution for LeetCode 802
1class Solution {
2 // 0: Unvisited, 1: Visiting/Unsafe, 2: Safe
3 public List<Integer> eventualSafeNodes(int[][] graph) {
4 int n = graph.length;
5 int[] color = new int[n];
6 List<Integer> result = new ArrayList<>();
7
8 for (int i = 0; i < n; i++) {
9 if (dfs(i, graph, color)) {
10 result.add(i);
11 }
12 }
13 return result;
14 }
15
16 private boolean dfs(int node, int[][] graph, int[] color) {
17 if (color[node] != 0) {
18 return color[node] == 2;
19 }
20
21 color[node] = 1; // Mark as visiting
22
23 for (int neighbor : graph[node]) {
24 if (color[neighbor] == 2) continue;
25 if (color[neighbor] == 1 || !dfs(neighbor, graph, color)) {
26 return false;
27 }
28 }
29
30 color[node] = 2; // Mark as safe
31 return true;
32 }
33}Python Solution for LeetCode 802
1class Solution:
2 def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]:
3 n = len(graph)
4 # 0: Unvisited
5 # 1: Visiting (in recursion stack) or Unsafe
6 # 2: Safe
7 color = [0] * n
8
9 def dfs(node):
10 if color[node] != 0:
11 return color[node] == 2
12
13 color[node] = 1 # Mark as visiting
14
15 for neighbor in graph[node]:
16 if color[neighbor] == 2:
17 continue
18 if color[neighbor] == 1 or not dfs(neighbor):
19 return False
20
21 color[node] = 2 # Mark as safe
22 return True
23
24 res = []
25 for i in range(n):
26 if dfs(i):
27 res.append(i)
28
29 return res