You are given a positive integer n representing the number of nodes of a Directed Acyclic Graph (DAG). The nodes are numbered from 0 to n - 1 (inclusive).
You are also given a 2D integer array edges, where edges[i] = [fromi, toi] denotes that there is a unidirectional edge from fromi to toi in the graph.
Return a list answer, where answer[i] is the list of ancestors of the ith node, sorted in ascending order.
A node u is an ancestor of another node v if u can reach v via a set of edges.
Example 1:
Input: n = 8, edgeList = [[0,3],[0,4],[1,3],[2,4],[2,7],[3,5],[3,6],[3,7],[4,6]] Output: [[],[],[],[0,1],[0,2],[0,1,3],[0,1,2,3,4],[0,1,2,3]] Explanation: The above diagram represents the input graph. - Nodes 0, 1, and 2 do not have any ancestors. - Node 3 has two ancestors 0 and 1. - Node 4 has two ancestors 0 and 2. - Node 5 has three ancestors 0, 1, and 3. - Node 6 has five ancestors 0, 1, 2, 3, and 4. - Node 7 has four ancestors 0, 1, 2, and 3.
Example 2:
Input: n = 5, edgeList = [[0,1],[0,2],[0,3],[0,4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]] Output: [[],[0],[0,1],[0,1,2],[0,1,2,3]] Explanation: The above diagram represents the input graph. - Node 0 does not have any ancestor. - Node 1 has one ancestor 0. - Node 2 has two ancestors 0 and 1. - Node 3 has three ancestors 0, 1, and 2. - Node 4 has four ancestors 0, 1, 2, and 3.
Constraints:
1 <= n <= 10000 <= edges.length <= min(2000, n * (n - 1) / 2)edges[i].length == 20 <= fromi, toi <= n - 1fromi != toiWhen 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:
To find all ancestors of a specific node, we can explore every possible path in the graph. We essentially walk backwards from the target node, tracking every node we encounter along the way. If we ever revisit a node, we know it is an ancestor.
Here's how the algorithm would work step-by-step:
def all_ancestors_brute_force(number_of_nodes, edges, target_node):
ancestors = set()
queue = [target_node]
while queue:
current_node = queue.pop(0)
for possible_ancestor in range(number_of_nodes):
# Check each node to see if it points to the current node.
if (possible_ancestor, current_node) in edges:
# Ensure ancestor is only added once.
if possible_ancestor not in ancestors:
ancestors.add(possible_ancestor)
# Add the possible ancestor to the queue to explore its ancestors.
queue.append(possible_ancestor)
return sorted(list(ancestors))To efficiently find all ancestors of each node in a directed acyclic graph, we'll perform a traversal from each node upwards. We avoid redundant work by remembering the ancestors we've already found for each node, storing that information to be reused later.
Here's how the algorithm would work step-by-step:
def all_ancestors_of_a_node_in_dag(number_of_nodes, edges):
graph = [[] for _ in range(number_of_nodes)]
for start_node, end_node in edges:
graph[start_node].append(end_node)
all_ancestors = [set() for _ in range(number_of_nodes)]
def depth_first_search(node, current_ancestors):
# If we've already computed the ancestors, reuse them.
if all_ancestors[node]:
return all_ancestors[node]
ancestors = set(current_ancestors)
# Traverse the graph to find ancestors.
for neighbor in graph[node]:
neighbor_ancestors = depth_first_search(neighbor, current_ancestors | {neighbor})
ancestors.update(neighbor_ancestors)
all_ancestors[node] = ancestors
return ancestors
# Perform a DFS from each node.
for node in range(number_of_nodes):
depth_first_search(node, {node})
result = [sorted(list(ancestors)) for ancestors in all_ancestors]
return result| Case | How to Handle |
|---|---|
| Null or empty graph (no nodes or edges) | Return an empty list for each node in the result since there are no ancestors. |
| Single node graph (only one node, no edges) | The single node has no ancestors, so return an empty list for it. |
| Graph with a single edge (e.g., 0 -> 1) | Node 1's ancestor is 0, and node 0 has no ancestors; handle correctly during traversal. |
| Node with no ancestors (root node(s)) | The result for such nodes will be an empty list as no ancestor exists to add. |
| Graph where all nodes are ancestors of a single node. | The algorithm will correctly identify all other nodes as ancestors of the final node. |
| Large graph to check for efficient memory usage and time complexity | Use an efficient traversal algorithm (e.g., DFS or BFS with appropriate data structures) to avoid excessive memory consumption and prevent timeouts. |
| Graph represented with a very large number of nodes, potentially nearing integer limits | Ensure node IDs are stored as data types capable of representing the entire range (e.g., long in Java/C++) to prevent overflow issues. |
| Disconnected graph components | The algorithm should handle disconnected components by correctly identifying ancestors within each component, returning empty lists for nodes in other components when traversing from a specific node. |