Facebook Pixel

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 list graph[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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Start with nodes that have out-degree 0 (terminal nodes)
  2. These are definitely safe
  3. For each safe node we find, we can potentially make other nodes safe
  4. 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 node j in the original graph
  • indeg[i] stores the out-degree of node i (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 nodes j that point to i in the original graph
  • We decrement the out-degree of j because one of its outgoing edges (to node i) is confirmed to lead to a safe node
  • If all of j's outgoing edges lead to safe nodes (indeg[j] == 0), then j 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 Evaluator

Example 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 most V 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More