Find Eventual Safe States
Description
You are given a directed graph with n nodes, labeled from 0 to n - 1. The graph is represented by a 0-indexed 2D integer array graph, where graph[i] is a list of all nodes that node i has a directed edge pointing to. In other words, there is an edge from node i to each node in graph[i].
Two key definitions:
-
Terminal Node: A node that has no outgoing edges (its adjacency list
graph[i]is empty). Think of it as a dead end — once you reach it, you stop. -
Safe Node: A node where every possible path starting from it eventually leads to a terminal node (or another safe node). This means no matter which outgoing edge you follow at each step, you will always eventually arrive at a terminal node. You can never get stuck in an infinite loop.
Your task is to find all safe nodes and return them in sorted ascending order.
Key insight: A node is unsafe if and only if it can reach a cycle. If any path from a node leads into a cycle, that path will loop forever and never reach a terminal node, making the starting node unsafe. So this problem is really asking: which nodes cannot reach any cycle?

Examples
Example 1
Input: graph = [[1,2],[2,3],[5],[0],[5],[],[]]
Output: [2,4,5,6]
Explanation: The graph has 7 nodes (0-6):
- Node 5 and Node 6 have no outgoing edges — they are terminal nodes, so they are trivially safe.
- Node 2 only points to node 5 (a terminal). Every path from 2 leads to 5. So node 2 is safe.
- Node 4 only points to node 5 (a terminal). Every path from 4 leads to 5. So node 4 is safe.
- Nodes 0, 1, 3 form a cycle: 0→1→3→0. Starting from node 0, you could go 0→1→3→0→1→3→... looping forever. Nodes 0, 1, and 3 are all unsafe.
Notice that node 1 also has an edge to node 2 (which is safe), but that doesn't matter — because node 1 ALSO has a path (through 3→0→1→...) that leads to a cycle. For a node to be safe, every path must lead to a terminal, not just some.
Example 2
Input: graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]]
Output: [4]
Explanation: The graph has 5 nodes:
- Node 4 has no outgoing edges — it is the only terminal node.
- Node 3 points to [0, 4]. The path 3→4 reaches a terminal, but the path 3→0→1→... can reach the cycle 0→1→1 (self-loop at 1) or 0→3→0→... (another cycle). Since not ALL paths from 3 are safe, node 3 is unsafe.
- Node 1 has a self-loop (1→1), which is a cycle of length 1. Node 1 is unsafe.
- Nodes 0 and 2 can reach node 1 or node 3's cycles, so they are also unsafe.
- Only node 4 is safe.
Example 3
Input: graph = [[],[0,2,3,4],[3],[4],[]]
Output: [0,1,2,3,4]
Explanation: The graph has 5 nodes:
- Nodes 0 and 4 have no outgoing edges — they are terminal nodes.
- Node 3 → [4] (terminal). Safe.
- Node 2 → [3] → [4] (terminal). Safe.
- Node 1 → [0, 2, 3, 4]. Every neighbor is either terminal or safe. So node 1 is safe.
There are no cycles in this graph. When a directed graph has no cycles (a DAG — Directed Acyclic Graph), every node is safe because all paths must eventually reach a terminal node.
Constraints
- n == graph.length
- 1 ≤ n ≤ 10^4
- 0 ≤ graph[i].length ≤ n
- 0 ≤ graph[i][j] ≤ n - 1
- graph[i] is sorted in strictly increasing order
- The graph may contain self-loops
- The number of edges in the graph will be in the range [1, 4 × 10^4]
Editorial
Brute Force
Intuition
The most direct way to check if a node is safe: start at that node, and explore every possible path from it. If all paths eventually reach a terminal node, the node is safe. If any path leads back to a node already on the current path (forming a cycle), the node is unsafe.
Imagine you are at the entrance of a maze. You want to know: "No matter which turns I take, will I always reach an exit?" The brute force answer is to try every combination of turns. If even one sequence of turns leads you in a circle, you declare the entrance unsafe.
Concretely, for each node, we run a Depth-First Search (DFS) with a path-visited set that tracks which nodes are on our current path. If DFS from a node revisits any node on the current path, we have found a cycle and the starting node is unsafe.
The critical problem with this approach is that we check each node independently, without remembering results from previous checks. If node 0's DFS already determined that node 2 leads to a terminal, we forget that fact when later checking node 1 (which also goes through node 2). This leads to massive redundant computation.
Step-by-Step Explanation
Let's trace with graph = [[1,2],[2,3],[5],[0],[5],[],[]].
Check node 0:
- Start DFS from 0. pathVisited = {0}
- Visit neighbor 1. pathVisited = {0, 1}
- Visit neighbor 2 (of node 1). pathVisited = {0, 1, 2}
- Visit neighbor 5 (of node 2). pathVisited = {0, 1, 2, 5}
- Node 5 has no neighbors → terminal → this path is safe. Backtrack.
- Back at node 1, visit neighbor 3. pathVisited = {0, 1, 3}
- Visit neighbor 0 (of node 3). But 0 is ALREADY in pathVisited! Cycle detected: 0→1→3→0
- Node 0 is unsafe. ❌
Check node 1:
- Start fresh DFS from 1. pathVisited = {1}
- Visit neighbor 2. pathVisited = {1, 2}
- Visit neighbor 5 (of node 2). Terminal → safe path. Backtrack.
- Visit neighbor 3 (of node 1). pathVisited = {1, 3}
- Visit neighbor 0 (of node 3). pathVisited = {1, 3, 0}
- Visit neighbor 1 (of node 0). But 1 is in pathVisited! Cycle: 1→3→0→1
- Node 1 is unsafe. ❌
- Notice: we re-explored nodes 2 and 5 even though we already knew they were safe from checking node 0. This is the wasted work.
Check node 2:
- Start fresh DFS from 2. pathVisited = {2}
- Visit neighbor 5. Terminal → all paths from 2 are safe.
- Node 2 is safe. ✅
Check node 3:
- Start fresh DFS from 3. pathVisited = {3}
- Visit neighbor 0. pathVisited = {3, 0}
- Visit neighbor 1 (of node 0). pathVisited = {3, 0, 1}
- Visit neighbor 3 (of node 1). But 3 is in pathVisited! Cycle: 3→0→1→3
- Node 3 is unsafe. ❌
Check node 4:
- Start fresh DFS from 4. pathVisited = {4}
- Visit neighbor 5. Terminal → safe.
- Node 4 is safe. ✅
Check node 5: Terminal (no neighbors). Trivially safe. ✅
Check node 6: Terminal (no neighbors). Trivially safe. ✅
Result: [2, 4, 5, 6]
We ran 7 independent DFS traversals. Nodes 2, 5 were explored during checks for 0, 1, 2, and 3 — that's 4 redundant explorations of the same subgraph.
Algorithm
- For each node
ifrom 0 to n-1:
a. Create a freshpathVisitedset
b. RunisSafe(graph, i, pathVisited)
c. If safe, addito the result list - In
isSafe(node, pathVisited):
a. Ifnodeis inpathVisited→ cycle detected → return false
b. Ifgraph[node]is empty → terminal → return true
c. AddnodetopathVisited
d. For each neighbor ofnode:- If
isSafe(neighbor, pathVisited)returns false → return false
e. RemovenodefrompathVisited(backtrack)
f. Return true
- If
- Return the result list (already sorted since we iterate 0 to n-1)
Code
class Solution {
public:
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
int n = graph.size();
vector<int> result;
for (int i = 0; i < n; i++) {
vector<bool> inPath(n, false);
if (isSafe(graph, i, inPath)) {
result.push_back(i);
}
}
return result;
}
private:
bool isSafe(vector<vector<int>>& graph, int node, vector<bool>& inPath) {
if (inPath[node]) return false; // cycle detected
if (graph[node].empty()) return true; // terminal node
inPath[node] = true;
for (int neighbor : graph[node]) {
if (!isSafe(graph, neighbor, inPath)) {
inPath[node] = false;
return false;
}
}
inPath[node] = false;
return true;
}
};class Solution:
def eventualSafeNodes(self, graph: list[list[int]]) -> list[int]:
n = len(graph)
result = []
def is_safe(node: int, in_path: set) -> bool:
if node in in_path:
return False # cycle detected
if not graph[node]:
return True # terminal node
in_path.add(node)
for neighbor in graph[node]:
if not is_safe(neighbor, in_path):
in_path.discard(node)
return False
in_path.discard(node)
return True
for i in range(n):
if is_safe(i, set()):
result.append(i)
return resultclass Solution {
public List<Integer> eventualSafeNodes(int[][] graph) {
int n = graph.length;
List<Integer> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
boolean[] inPath = new boolean[n];
if (isSafe(graph, i, inPath)) {
result.add(i);
}
}
return result;
}
private boolean isSafe(int[][] graph, int node, boolean[] inPath) {
if (inPath[node]) return false;
if (graph[node].length == 0) return true;
inPath[node] = true;
for (int neighbor : graph[node]) {
if (!isSafe(graph, neighbor, inPath)) {
inPath[node] = false;
return false;
}
}
inPath[node] = false;
return true;
}
}Complexity Analysis
Time Complexity: O(V × (V + E))
For each of the V nodes, we run a fresh DFS that can visit up to V nodes and traverse up to E edges. Without memoization, the same subgraphs are re-explored for different starting nodes. In pathological cases with many overlapping paths, this can degrade further.
Space Complexity: O(V)
The inPath set (or boolean array) uses O(V) space. The recursion stack can be at most V deep. Each DFS call uses a fresh set, so total space per DFS is O(V).
With n up to 10^4 and edges up to 4 × 10^4, the worst case is approximately 10^4 × (10^4 + 4 × 10^4) = 5 × 10^8 operations — far too slow for typical time limits of 1-2 seconds.
Why This Approach Is Not Efficient
The brute force runs a separate DFS for each node, leading to massive redundant computation:
-
Repeated exploration: When checking node 0, we already determined that nodes 2 and 5 are safe. But when checking node 1 (which also reaches 2 and 5), we re-explore them from scratch. Every node's safety is re-derived independently.
-
No memory between queries: The algorithm "forgets" everything after each node's check. It has no mechanism to say, "I already know node 2 is safe — skip it."
-
Scale problem: With V = 10^4 and E = 4 × 10^4, performing up to 10^4 separate DFS traversals each costing O(V + E) gives ~5 × 10^8 operations. This exceeds the typical 10^8 operation budget for competitive programming.
The key insight for improvement: If we could remember whether a node was already determined to be safe or unsafe, we would never need to re-explore it. A single DFS pass that caches results (memoization) would suffice. This is exactly what the three-color DFS technique provides.
Better Approach - DFS with Three-Color Marking
Intuition
Instead of running independent DFS calls per node, we use a single DFS pass over the entire graph with a clever coloring scheme that simultaneously detects cycles and caches safety results.
Imagine each node starts as a white blank page. When we begin exploring a node, we color it gray ("currently investigating — do not disturb"). When we finish exploring all of its paths and confirm they are all safe, we color it black ("confirmed safe — case closed").
The magic: if during DFS we encounter a gray node, it means we have returned to a node that is currently being investigated — we have found a cycle. The gray node is on the current DFS recursion stack, so we've come full circle. The entire chain of gray nodes involved in this cycle is unsafe.
If we encounter a black node, we already know it is safe, so we can skip it immediately. If we encounter a white node, it hasn't been investigated yet, so we recurse into it.
This three-color scheme achieves two goals simultaneously:
- Cycle detection: Gray nodes on the recursion stack reveal cycles
- Memoization: Black nodes provide cached answers, preventing re-exploration
The result: each node and each edge is processed exactly once, giving O(V + E) time.
Step-by-Step Explanation
Let's trace with graph = [[1,2],[2,3],[5],[0],[5],[],[]].
Color states: White = 0 (unvisited), Gray = 1 (processing), Black = 2 (safe).
Step 1: Initialize all nodes as white. Begin checking node 0.
- Colors: [W, W, W, W, W, W, W]
Step 2: DFS(0). Mark 0 gray. Explore first neighbor: 1.
- Colors: [G, W, W, W, W, W, W]. Recursion stack: [0]
Step 3: DFS(1). Mark 1 gray. Explore first neighbor: 2.
- Colors: [G, G, W, W, W, W, W]. Stack: [0, 1]
Step 4: DFS(2). Mark 2 gray. Explore only neighbor: 5.
- Colors: [G, G, G, W, W, W, W]. Stack: [0, 1, 2]
Step 5: DFS(5). Mark 5 gray. Node 5 has NO neighbors (terminal). Mark 5 black (safe). Return true.
- Colors: [G, G, G, W, W, B, W]. Stack: [0, 1, 2]
Step 6: Back at node 2. Its only neighbor (5) returned true. All paths from 2 are safe. Mark 2 black. Return true.
- Colors: [G, G, B, W, W, B, W]. Stack: [0, 1]
Step 7: Back at node 1. Neighbor 2 was safe. Now explore next neighbor: 3. DFS(3). Mark 3 gray.
- Colors: [G, G, B, G, W, B, W]. Stack: [0, 1, 3]
Step 8: DFS(3) explores neighbor 0. Node 0 is GRAY — it is on the current recursion stack! Cycle detected: 0→1→3→0. Return false.
- Colors: [G, G, B, G, W, B, W]. Cycle: 0→1→3→0
Step 9: Cascade failure. Node 3 returns false (unsafe, stays gray). Node 1 returns false (has unsafe neighbor 3, stays gray). Node 0 returns false (stays gray).
- Colors: [G, G, B, G, W, B, W]. Nodes 0, 1, 3 are gray = unsafe.
Step 10: Move to next white node: 4. DFS(4). Mark 4 gray. Explore neighbor 5.
- Colors: [G, G, B, G, G, B, W]. Stack: [4]
Step 11: Node 5 is already BLACK (safe). Return true immediately — no need to re-explore! Mark 4 black.
- Colors: [G, G, B, G, B, B, W]. Node 4 is safe.
Step 12: Move to next white node: 6. DFS(6). Terminal (no neighbors). Mark 6 black.
- Colors: [G, G, B, G, B, B, B]. Node 6 is safe.
Step 13: All nodes processed. Collect black nodes (safe): [2, 4, 5, 6]. Gray nodes (unsafe): [0, 1, 3].
- Result: [2, 4, 5, 6]
DFS Three-Color Cycle Detection — Watch how gray (processing) nodes reveal cycles and black (safe) nodes prevent re-exploration. Each node is colored exactly once.
Algorithm
- Create a
colorarray of size n, initialized to 0 (white = unvisited) - For each node
ifrom 0 to n-1:
a. Calldfs(i, color)
b. If it returns true, addito result - In
dfs(node, color):
a. Ifcolor[node]is not 0 (already processed):- Return
color[node] == 2(true if black/safe, false if gray/unsafe)
b. Markcolor[node] = 1(gray — processing)
c. For each neighbor of node: - If
dfs(neighbor, color)returns false → return false
d. Markcolor[node] = 2(black — safe)
e. Return true
- Return
- Return the result list
Code
class Solution {
public:
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
int n = graph.size();
vector<int> color(n, 0); // 0 = white, 1 = gray, 2 = black
vector<int> result;
for (int i = 0; i < n; i++) {
if (dfs(graph, i, color)) {
result.push_back(i);
}
}
return result;
}
private:
bool dfs(vector<vector<int>>& graph, int node, vector<int>& color) {
if (color[node] != 0) {
return color[node] == 2; // black = safe, gray = unsafe
}
color[node] = 1; // mark gray (processing)
for (int neighbor : graph[node]) {
if (!dfs(graph, neighbor, color)) {
return false; // unsafe neighbor found
}
}
color[node] = 2; // all neighbors safe → mark black
return true;
}
};class Solution:
def eventualSafeNodes(self, graph: list[list[int]]) -> list[int]:
n = len(graph)
color = [0] * n # 0 = white, 1 = gray, 2 = black
def dfs(node: int) -> bool:
if color[node] != 0:
return color[node] == 2
color[node] = 1 # gray (processing)
for neighbor in graph[node]:
if not dfs(neighbor):
return False
color[node] = 2 # black (safe)
return True
return [i for i in range(n) if dfs(i)]class Solution {
public List<Integer> eventualSafeNodes(int[][] graph) {
int n = graph.length;
int[] color = new int[n]; // 0 = white, 1 = gray, 2 = black
List<Integer> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (dfs(graph, i, color)) {
result.add(i);
}
}
return result;
}
private boolean dfs(int[][] graph, int node, int[] color) {
if (color[node] != 0) {
return color[node] == 2;
}
color[node] = 1; // gray
for (int neighbor : graph[node]) {
if (!dfs(graph, neighbor, color)) {
return false;
}
}
color[node] = 2; // black (safe)
return true;
}
}Complexity Analysis
Time Complexity: O(V + E)
Each node is processed exactly once by the DFS. Once colored gray or black, subsequent calls to dfs(node) return immediately in O(1). For each node, we iterate through its adjacency list (its outgoing edges). Since we visit each node once and each edge once, the total time is O(V + E).
Compared to the brute force's O(V × (V + E)), this is a dramatic improvement. For V = 10^4 and E = 4 × 10^4, we go from ~5 × 10^8 to ~5 × 10^4 operations.
Space Complexity: O(V)
The color array uses O(V) space. The recursion stack in the worst case (a linear chain of nodes) can be O(V) deep. No additional data structures are needed.
Why This Approach Is Not Efficient
The DFS three-color approach is already O(V + E) — the optimal time complexity for this problem, since we must examine every node and edge at least once. However, it has practical drawbacks:
-
Recursion depth risk: The DFS uses the system call stack. For a graph that forms a long chain (e.g., 0→1→2→...→9999), the recursion depth reaches V = 10^4. Python's default recursion limit is 1000, requiring
sys.setrecursionlimit(). Java and C++ can also encounter stack overflow for very deep graphs. -
Function call overhead: Each recursive call creates a new stack frame. For 10^4 nodes, this overhead is noticeable compared to an iterative approach.
-
Harder to reason about: The three-color scheme requires understanding DFS recursion, backtracking, and implicit cycle detection. It is a "thinking backwards from the problem" approach.
-
Not naturally suited to this problem's structure: The problem asks "which nodes can reach only terminal nodes?" — a question that naturally suggests working forward from terminals rather than backward from cycles. A topological sort approach starting from terminal nodes is more intuitive.
An iterative BFS approach on a reverse graph eliminates recursion entirely and solves the problem in a more intuitive direction — propagating safety backward from known-safe terminal nodes.
Optimal Approach - Reverse Graph with Topological Sort (BFS)
Intuition
Instead of asking "can this node reach a cycle?" (which requires exploring forward), let us flip the question: start from nodes we KNOW are safe, and propagate safety backward.
Who is definitely safe? Terminal nodes — they have no outgoing edges, so they trivially cannot reach a cycle.
Now think: if a node's every outgoing edge leads to a safe node, then that node itself must be safe. This gives us a propagation rule:
A node becomes safe when ALL of its outgoing edges have been "resolved" (lead to confirmed safe nodes).
This is exactly the logic of topological sort using Kahn's algorithm, but applied in a novel way:
-
Build a reverse graph: If the original has edge A→B, the reverse graph has B→A. This lets us efficiently find which nodes point TO a given node.
-
Track out-degrees: For each node, count its outgoing edges in the original graph. This represents how many of its neighbors still need to be confirmed safe.
-
Start BFS from terminal nodes (out-degree = 0). They are trivially safe.
-
Propagate safety: When we confirm a node is safe, use the reverse graph to find all nodes that point TO it. Decrease their out-degree by 1 (one more neighbor confirmed safe). When a node's out-degree reaches 0, ALL of its neighbors are confirmed safe, so it becomes safe too — add it to the queue.
-
Cycle nodes are naturally filtered: Nodes in a cycle will always have remaining out-degree > 0, because their cycle edges can never be resolved. They are never added to the queue.
This approach is iterative (no recursion), intuitive (propagate from known-safe nodes), and handles cycles automatically without explicit detection.
Step-by-Step Explanation
Let's trace with graph = [[1,2],[2,3],[5],[0],[5],[],[]].
Step 1: Compute out-degrees and build reverse graph.
- Out-degrees: [2, 2, 1, 1, 1, 0, 0]
- Reverse graph: rg[0]=[3], rg[1]=[0], rg[2]=[0,1], rg[3]=[1], rg[4]=[], rg[5]=[2,4], rg[6]=[]
Step 2: Initialize queue with terminal nodes (out-degree 0): queue = [5, 6]. Mark them safe.
Step 3: Dequeue node 5. Look at reverse graph: rg[5] = [2, 4]. Nodes 2 and 4 point to 5 in the original graph.
Step 4: Decrement out-degree[2]: 1 → 0. Node 2's only outgoing edge (to 5) is now confirmed safe. ALL paths from 2 lead to safe nodes → node 2 is safe! Enqueue 2.
Step 5: Decrement out-degree[4]: 1 → 0. Same reasoning — node 4 is safe. Enqueue 4.
- Queue: [6, 2, 4]
Step 6: Dequeue node 6. rg[6] = []. No node points to 6 in the original graph. Nothing to process.
- Queue: [2, 4]
Step 7: Dequeue node 2. rg[2] = [0, 1]. Nodes 0 and 1 point to 2.
Step 8: Decrement out-degree[0]: 2 → 1. Node 0 still has 1 unresolved outgoing edge (to node 1, which is NOT yet safe). Cannot mark 0 safe.
Step 9: Decrement out-degree[1]: 2 → 1. Node 1 still has 1 unresolved edge (to node 3, NOT yet safe). Cannot mark 1 safe.
- Queue: [4]
Step 10: Dequeue node 4. rg[4] = []. Nothing to process.
- Queue: [] (empty)
Step 11: BFS complete. Check final out-degrees: [1, 1, 0, 1, 0, 0, 0]. Nodes with out-degree 0: {2, 4, 5, 6}.
- Nodes 0, 1, 3 have remaining out-degree > 0 because they form cycle 0→1→3→0. Their mutual dependencies can never be resolved — each waits for the others to become safe, creating a deadlock.
Result: [2, 4, 5, 6]
Reverse Graph BFS — Safety Propagation from Terminals — Watch safety propagate backward from terminal nodes. Out-degrees decrease as neighbors are confirmed safe. Cycle nodes never reach out-degree 0.
Algorithm
- Build reverse graph and compute out-degrees:
- For each edge
i → jin the original graph, addj → ito the reverse graph - Set
outDegree[i] = graph[i].lengthfor each node
- For each edge
- Initialize BFS with terminal nodes:
- Enqueue all nodes where
outDegree[i] == 0
- Enqueue all nodes where
- Process BFS:
- While queue is not empty:
a. Dequeue a safe nodeu
b. For each predecessorvofuin the reverse graph:- Decrement
outDegree[v]by 1 - If
outDegree[v] == 0, enqueuev(all its edges now lead to safe nodes)
- Decrement
- While queue is not empty:
- Collect results:
- Return all nodes where
outDegree[i] == 0, in sorted order
- Return all nodes where
Code
class Solution {
public:
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
int n = graph.size();
vector<vector<int>> reverseGraph(n);
vector<int> outDegree(n, 0);
// Build reverse graph and compute out-degrees
for (int i = 0; i < n; i++) {
outDegree[i] = graph[i].size();
for (int j : graph[i]) {
reverseGraph[j].push_back(i);
}
}
// Initialize BFS with terminal nodes (out-degree 0)
queue<int> q;
for (int i = 0; i < n; i++) {
if (outDegree[i] == 0) {
q.push(i);
}
}
// Propagate safety backward through reverse graph
while (!q.empty()) {
int node = q.front();
q.pop();
for (int pred : reverseGraph[node]) {
outDegree[pred]--;
if (outDegree[pred] == 0) {
q.push(pred);
}
}
}
// Collect all safe nodes (out-degree reduced to 0)
vector<int> result;
for (int i = 0; i < n; i++) {
if (outDegree[i] == 0) {
result.push_back(i);
}
}
return result;
}
};from collections import defaultdict, deque
class Solution:
def eventualSafeNodes(self, graph: list[list[int]]) -> list[int]:
n = len(graph)
reverse_graph = defaultdict(list)
out_degree = [0] * n
# Build reverse graph and compute out-degrees
for i, neighbors in enumerate(graph):
out_degree[i] = len(neighbors)
for j in neighbors:
reverse_graph[j].append(i)
# Initialize BFS with terminal nodes
queue = deque(i for i in range(n) if out_degree[i] == 0)
# Propagate safety backward
while queue:
node = queue.popleft()
for pred in reverse_graph[node]:
out_degree[pred] -= 1
if out_degree[pred] == 0:
queue.append(pred)
# Collect safe nodes (out-degree reached 0)
return [i for i in range(n) if out_degree[i] == 0]class Solution {
public List<Integer> eventualSafeNodes(int[][] graph) {
int n = graph.length;
List<List<Integer>> reverseGraph = new ArrayList<>();
int[] outDegree = new int[n];
for (int i = 0; i < n; i++) {
reverseGraph.add(new ArrayList<>());
}
// Build reverse graph and compute out-degrees
for (int i = 0; i < n; i++) {
outDegree[i] = graph[i].length;
for (int j : graph[i]) {
reverseGraph.get(j).add(i);
}
}
// Initialize BFS with terminal nodes
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (outDegree[i] == 0) {
queue.add(i);
}
}
// Propagate safety backward
while (!queue.isEmpty()) {
int node = queue.poll();
for (int pred : reverseGraph.get(node)) {
outDegree[pred]--;
if (outDegree[pred] == 0) {
queue.add(pred);
}
}
}
// Collect safe nodes
List<Integer> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (outDegree[i] == 0) {
result.add(i);
}
}
return result;
}
}Complexity Analysis
Time Complexity: O(V + E)
- Building the reverse graph: iterate through all V nodes and all E edges → O(V + E)
- Computing out-degrees: O(V)
- BFS initialization (finding terminal nodes): O(V)
- BFS processing: each node is dequeued at most once, and each reverse edge is traversed at most once → O(V + E)
- Collecting results: O(V)
- Total: O(V + E)
Space Complexity: O(V + E)
- Reverse graph stores all E edges (in reversed direction): O(E)
- Out-degree array: O(V)
- BFS queue: at most V nodes → O(V)
- Result list: O(V)
- Total: O(V + E)
Comparison of all three approaches:
| Approach | Time | Space | Recursion | Cycle Handling |
|---|---|---|---|---|
| Brute Force (DFS per node) | O(V × (V+E)) | O(V) | Yes (per node) | Explicit path tracking |
| DFS Three-Color | O(V + E) | O(V) | Yes (single pass) | Implicit via gray detection |
| Reverse Graph BFS (Topo Sort) | O(V + E) | O(V + E) | No | Implicit via out-degree deadlock |
The BFS approach uses slightly more space (O(V + E) vs O(V)) due to the reverse graph, but eliminates recursion entirely, making it more robust for large inputs and cleaner in its reasoning about safety propagation.