Skip to main content

Find Eventual Safe States

MEDIUMProblemSolveExternal Links

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:

  1. 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.

  2. 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?

Directed graph with 7 nodes showing safe nodes 2, 4, 5, 6 in green and unsafe nodes 0, 1, 3 in red forming a cycle 0→1→3→0.
Directed graph with 7 nodes showing safe nodes 2, 4, 5, 6 in green and unsafe nodes 0, 1, 3 in red forming a cycle 0→1→3→0.

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

  1. For each node i from 0 to n-1:
    a. Create a fresh pathVisited set
    b. Run isSafe(graph, i, pathVisited)
    c. If safe, add i to the result list
  2. In isSafe(node, pathVisited):
    a. If node is in pathVisited → cycle detected → return false
    b. If graph[node] is empty → terminal → return true
    c. Add node to pathVisited
    d. For each neighbor of node:
    • If isSafe(neighbor, pathVisited) returns false → return false
      e. Remove node from pathVisited (backtrack)
      f. Return true
  3. 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 result
class 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:

  1. 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.

  2. 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."

  3. 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:

  1. Cycle detection: Gray nodes on the recursion stack reveal cycles
  2. 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

  1. Create a color array of size n, initialized to 0 (white = unvisited)
  2. For each node i from 0 to n-1:
    a. Call dfs(i, color)
    b. If it returns true, add i to result
  3. In dfs(node, color):
    a. If color[node] is not 0 (already processed):
    • Return color[node] == 2 (true if black/safe, false if gray/unsafe)
      b. Mark color[node] = 1 (gray — processing)
      c. For each neighbor of node:
    • If dfs(neighbor, color) returns false → return false
      d. Mark color[node] = 2 (black — safe)
      e. Return true
  4. 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:

  1. 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.

  2. Function call overhead: Each recursive call creates a new stack frame. For 10^4 nodes, this overhead is noticeable compared to an iterative approach.

  3. 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.

  4. 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:

  1. 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.

  2. 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.

  3. Start BFS from terminal nodes (out-degree = 0). They are trivially safe.

  4. 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.

  5. 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

  1. Build reverse graph and compute out-degrees:
    • For each edge i → j in the original graph, add j → i to the reverse graph
    • Set outDegree[i] = graph[i].length for each node
  2. Initialize BFS with terminal nodes:
    • Enqueue all nodes where outDegree[i] == 0
  3. Process BFS:
    • While queue is not empty:
      a. Dequeue a safe node u
      b. For each predecessor v of u in the reverse graph:
      • Decrement outDegree[v] by 1
      • If outDegree[v] == 0, enqueue v (all its edges now lead to safe nodes)
  4. Collect results:
    • Return all nodes where outDegree[i] == 0, in sorted order

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:

ApproachTimeSpaceRecursionCycle Handling
Brute Force (DFS per node)O(V × (V+E))O(V)Yes (per node)Explicit path tracking
DFS Three-ColorO(V + E)O(V)Yes (single pass)Implicit via gray detection
Reverse Graph BFS (Topo Sort)O(V + E)O(V + E)NoImplicit 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.