Loading problems...
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.
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.
python
# Pseudo-code for Brute Force
def is_safe(node, visited):
if node in visited: return False # Cycle detected
visited.add(node)
for neighbor in graph[node]:
if not is_safe(neighbor, visited):
return False
visited.remove(node)
return True
result = []
for i in range(n):
if is_safe(i, set()):
result.append(i)
return 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.
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.
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.

Let's trace the algorithm with a simple example: 0 -> 1 -> 0 (Cycle) and 2 -> 1.
i = 0. color[0] is 0. Call dfs(0).color[0] = 1. Neighbor is 1.color[1] = 1. Neighbor is 0.color[0] is 1. Cycle detected! Return false.false from neighbor 0. Return false. color[1] remains 1.false from neighbor 1. Return false. color[0] remains 1.i = 1. color[1] is 1. It was visited and determined unsafe. Skip or treat as processed.i = 2. color[2] is 0. Call dfs(2).color[2] = 1. Neighbor is 1.color[1] is 1. This means it is unsafe (either in stack or previously failed). Return false.false from neighbor 1. Return false. color[2] remains 1.2. Result [].Note: In this logic, state 1 serves double duty as "Visiting" and "Unsafe". State 2 is strictly "Safe".
The correctness relies on the invariant of the 3-color DFS.
1). Our algorithm explicitly checks for this.u has a neighbor v that is part of a cycle (or leads to one), dfs(v) will return false. Consequently, dfs(u) will also return false, correctly identifying u as unsafe.2 (Safe), and true is returned. This forms the base case for safety.0 to n-1, we ensure every component of the graph is processed. Memoization ensures each node is computed exactly once.cppclass Solution { public: // 0: Unvisited // 1: Visiting (in stack) or Unsafe // 2: Safe bool dfs(int node, vector<vector<int>>& graph, vector<int>& color) { if (color[node] != 0) { return color[node] == 2; } color[node] = 1; // Mark as visiting for (int neighbor : graph[node]) { if (color[neighbor] == 2) continue; // Safe node, skip if (color[neighbor] == 1 || !dfs(neighbor, graph, color)) { return false; // Cycle detected or leads to unsafe node } } color[node] = 2; // Mark as safe return true; } vector<int> eventualSafeNodes(vector<vector<int>>& graph) { int n = graph.size(); vector<int> color(n, 0); vector<int> safeNodes; for (int i = 0; i < n; ++i) { if (dfs(i, graph, color)) { safeNodes.push_back(i); } } return safeNodes; } };
javaclass Solution { // 0: Unvisited, 1: Visiting/Unsafe, 2: Safe public List<Integer> eventualSafeNodes(int[][] graph) { int n = graph.length; int[] color = new int[n]; List<Integer> result = new ArrayList<>(); for (int i = 0; i < n; i++) { if (dfs(i, graph, color)) { result.add(i); } } return result; } private boolean dfs(int node, int[][] graph, int[] color) { if (color[node] != 0) { return color[node] == 2; } color[node] = 1; // Mark as visiting for (int neighbor : graph[node]) { if (color[neighbor] == 2) continue; if (color[neighbor] == 1 || !dfs(neighbor, graph, color)) { return false; } } color[node] = 2; // Mark as safe return true; } }
pythonclass Solution: def eventualSafeNodes(self, graph: List[List[int]]) -> List[int]: n = len(graph) # 0: Unvisited # 1: Visiting (in recursion stack) or Unsafe # 2: Safe color = [0] * n def dfs(node): if color[node] != 0: return color[node] == 2 color[node] = 1 # Mark as visiting for neighbor in graph[node]: if color[neighbor] == 2: continue if color[neighbor] == 1 or not dfs(neighbor): return False color[node] = 2 # Mark as safe return True res = [] for i in range(n): if dfs(i): res.append(i) return res
Time Complexity:
result collection takes .Space Complexity:
color array takes space.The Graph DFS - Cycle Detection pattern is a fundamental tool for solving directed graph problems involving dependencies or infinite loops.
Why does my solution get Time Limit Exceeded (TLE)?
visited set that resets for every node iteration.color array). Once a node is determined to be safe or unsafe, that result should be reused for all future queries.Why is my logic failing on disjoint graphs?
0 to n-1 and launch DFS if the node hasn't been visited yet.Why is checking for "visited" not enough?