AlgoMaster Logo

Find Eventual Safe States

mediumFrequency6 min readUpdated June 23, 2026

Understanding the Problem

We have a directed graph, and we need to find which nodes are "safe." A node is safe if no matter which path you take from it, you always end up at a terminal node, one with zero outgoing edges. If even one path from a node can reach a cycle, that node is unsafe, because following that path can loop forever instead of terminating.

Terminal nodes are safe by definition: there is nowhere to go, so every path (the empty path) terminates. A node is safe when every outgoing edge leads to a safe node, since that guarantees every path from it eventually reaches a terminal.

This reduces to a statement about cycles. The unsafe nodes are exactly those that lie on a cycle or can reach a cycle, and the safe nodes are everything else. So the problem becomes: find all nodes that cannot reach any cycle.

Key Constraints:

  • n <= 10^4 and edges <= 4 * 10^4 -> The graph is sparse. A linear O(V + E) traversal is the target; an O(V * E) approach that recomputes reachability per node would be far slower than needed.
  • The graph may contain self-loops -> A self-loop (a node pointing to itself) makes that node unsafe, since the path from it never leaves the loop. Both approaches below handle this without a special case: the self-loop is a length-one cycle.

Approach 1: DFS with Three-Color Marking

Intuition

Detecting a cycle in a directed graph is the classic use case for DFS with coloring. The standard version uses three colors (or states):

  • WHITE (0): The node has not been visited yet.
  • GRAY (1): The node is currently on the DFS recursion stack, so we are in the middle of exploring paths from it.
  • BLACK (2): The node has been fully processed and its final status is known.

During DFS from a node u, reaching a GRAY node means we have arrived at a node still on the current path, which is a cycle. That makes u and every node on the path back to that GRAY node unsafe. If instead every path from u ends at processed nodes that were all marked safe, then u is safe.

We split the BLACK state into SAFE and UNSAFE so the result of each node is cached. When DFS finishes exploring all neighbors of a node:

  • If any neighbor is UNSAFE or sat on the recursion stack (was GRAY when reached), mark the current node UNSAFE.
  • If every neighbor is SAFE, mark the current node SAFE.

Because the final status is recorded, each node is computed once and reused on later visits, giving a single linear pass over the graph.

Algorithm

  1. Create a color array of size n, initialized to WHITE (0) for all nodes.
  2. For each node i from 0 to n-1, if it hasn't been visited, run DFS on it.
  3. In the DFS for node u:
    • Mark u as GRAY (visiting).
    • For each neighbor v of u:
      • If v is GRAY, we found a cycle. Return false (unsafe).
      • If v is WHITE, recursively DFS on v. If it returns false, return false.
      • If v is already marked SAFE, skip it. If it's marked UNSAFE, return false.
    • If all neighbors are safe, mark u as SAFE (2) and return true.
    • If any neighbor was unsafe, mark u as UNSAFE (3) and return false.
  4. Collect all nodes marked SAFE into the result list.

Example Walkthrough

1Initial graph. Start DFS from node 0, mark it GRAY
0GRAY123546
1/7

Code

The DFS approach is O(V + E), which is optimal. Its one practical drawback is the recursion: on a deep graph (a long chain of nodes), the call stack can grow to O(V) and overflow. The next approach computes the same answer iteratively, reframing safety as a topological-sort problem on the reversed graph.

Approach 2: Topological Sort on Reverse Graph

Intuition

The definition of safety has a recursive shape that maps directly onto topological sorting. A terminal node (no outgoing edges) is safe. A non-terminal node is safe once all of its outgoing neighbors are known to be safe. This is the same dependency structure Kahn's algorithm resolves, except the "dependency" we count down is a node's out-degree rather than its in-degree.

The algorithm tracks each node's out-degree, which starts as graph[i].length. Terminal nodes have out-degree 0 and are safe immediately. When a node is confirmed safe, every node that points to it has one fewer unresolved dependency, so we decrement those predecessors' out-degrees. To find the predecessors of a node efficiently, we build the reverse graph: for each edge u -> v, we store v -> u. Once a predecessor's out-degree reaches 0, all of its neighbors are safe, so it is safe too and joins the queue.

Algorithm

  1. Build the reverse graph: for each edge u -> v in the original graph, add edge v -> u.
  2. Compute the out-degree of each node in the original graph, which is graph[i].length.
  3. Initialize a queue with all terminal nodes (out-degree 0).
  4. BFS: for each node dequeued (confirmed safe), look at its reverse neighbors. Decrement their out-degree. If a node's out-degree reaches 0, add it to the queue.
  5. All nodes that were ever added to the queue are safe. Sort and return them.

Example Walkthrough

1Out-degrees: 0→2, 1→2, 2→1, 3→1, 4→1, 5→0, 6→0. Queue: [5, 6]
01235queue46queue
1/6

Code