There is a directed graph of n
nodes with each node labeled from 0
to n - 1
. The graph is represented by a 0-indexed 2D integer array graph
where graph[i]
is an integer array of nodes adjacent to node i
, meaning there is an edge from node i
to each node in graph[i]
.
A node is a terminal node if there are no outgoing edges. A node is a safe node if every possible path starting from that node leads to a terminal node (or another safe node).
Return an array containing all the safe nodes of the graph. The answer should be sorted in ascending order.
Example 1:
graph = [[1,2],[2,3],[5],[0],[5],[],[]]
Output: [2,4,5,6]
Example 2:
graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]]
Output: [4]
Could you provide a solution to identify and return all the safe nodes in a given directed graph?
When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The goal is to find which locations are 'safe', meaning there's no way to get stuck in a cycle. The brute force approach is basically to explore every possible path from each location to see where it leads.
Here's how the algorithm would work step-by-step:
def find_eventual_safe_states_brute_force(graph):
number_of_nodes = len(graph)
safe_nodes = []
for start_node in range(number_of_nodes):
if is_node_safe(graph, start_node, set()):
safe_nodes.append(start_node)
return sorted(safe_nodes)
def is_node_safe(graph, current_node, visited_nodes):
if current_node in visited_nodes:
return False
if not graph[current_node]:
return True
visited_nodes.add(current_node)
# Iterate through all outgoing edges
for neighbor_node in graph[current_node]:
# If the neighbor is not safe, current is also not safe
if not is_node_safe(graph, neighbor_node, visited_nodes.copy()):
return False
# If all outgoing edges are safe, current is also safe
return True
The core idea is to find the 'bad' nodes first: the ones that will definitely lead to an endless loop. Then, we mark all nodes that can reach these 'bad' nodes as also 'bad'. Finally, any node not marked as 'bad' is 'safe'.
Here's how the algorithm would work step-by-step:
def eventualSafeNodes(graph):
number_of_nodes = len(graph)
node_states = [0] * number_of_nodes # 0: unvisited, 1: visiting, 2: safe
safe_nodes = []
def depthFirstSearch(node):
if node_states[node] == 1:
return False # Cycle detected
if node_states[node] == 2:
return True # Already marked as safe
node_states[node] = 1 # Mark as currently visiting
for neighbor in graph[node]:
if not depthFirstSearch(neighbor):
return False
node_states[node] = 2 # Mark as safe after visiting all neighbors
return True
# Iterate through all nodes to find safe nodes.
for node in range(number_of_nodes):
if depthFirstSearch(node):
safe_nodes.append(node)
# Return a sorted list of safe nodes.
return sorted(safe_nodes)
Case | How to Handle |
---|---|
Empty graph (no nodes) | Return an empty list as there are no states to evaluate. |
Graph with only one node and no outgoing edges | The single node is a safe state, add it to the result. |
Graph with a single node and a self-loop | This node is not a safe state and should not be included in the result. |
Graph with a cycle of nodes that cannot reach a terminal node | These nodes are not safe and the algorithm must correctly identify cycles. |
Graph where all nodes are safe states | The algorithm should return all nodes in sorted order. |
Graph where no nodes are safe states (all part of cycles leading to non-terminal nodes) | The algorithm should return an empty list. |
Graph with a large number of nodes and edges, potentially leading to stack overflow with recursive DFS implementations | Implement iteratively with explicit stack, or ensure recursion depth is managed to prevent overflow. |
Graph contains nodes with very high out-degrees | The solution should efficiently iterate over neighbors to determine safety. |