Problem
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.
Examples
Example 1:
graph TD 0 --> 3 0 --> 4 1 --> 3 2 --> 4 2 --> 7 3 --> 5 3 --> 6 3 --> 7 4 --> 6
|
|
Example 2:
graph TD; 0 1 2 1 --> 2 1 --> 3 1 --> 4 2 --> 3 2 --> 4 0 --> 1 0 --> 2 0 --> 3 0 --> 4 3 --> 4
|
|
Solution
Method 1 - DFS
Steps to follow:
- Build the graph with adjacency list representation
- Run the DFS on each node as
parent
, and see which nodes it can reach - If the node is reachable, we add
parent
as to the ancestor list for that node
Here is the video explanation:
Code
|
|
Complexity
- ⏰ Time complexity:
O(n * (n+m))
wheren
is number of nodes andm
is number of edges. It takesO(n)
time to initialize all adjacency lists, andO(m)
time to fill in all adjacency list. So,O(n+m)
for building the graph. For DFS from one node it takesO(n+m)
time as well, but we are doing it forn
nodes, henceO(n * (n+m))
. - 🧺 Space complexity:
O(n+m)
.O(n+m)
for building graph, andO(n)
for using visited set.
Method 2 - Topological Sorting using Kahns Algorithm BFS
We can optimize the given solution for finding the ancestors of nodes in a Directed Acyclic Graph (DAG) by using Depth-First Search (DFS) in combination with memoization to avoid redundant computations. Since the nodes are numbered from 0
to n-1
, we can leverage the properties of the DAG and the topological order.
Approach
- Graph Construction: Builds the adjacency list representation of the graph and computes the in-degrees of each node.
- Topological Sort: Uses Kahn’s algorithm to perform a topological sort of the nodes. This helps us process each node before its descendants.
- Ancestors Calculation: Iterates through the nodes in topological order and populates the ancestor sets for each node. The ancestors of a node
neighbor
are the union of its immediate ancestornode
and all ancestors ofnode
. So, in a way it is DFS with memoization. - Sorting: Converts the ancestor sets to lists and sorts them.
Code
|
|
Complexity
- ⏰ Time complexity:
O(n log n + m)
. For topological sorting it takesO(n+m)
, for ancestor collection it takesO(n)
and for sorting it takesO(n log n)
- 🧺 Space complexity:
O(n + m)
for storing the graph
Dry Run
We will take second graph for this, as it is smaller for debugging purposes.
|
|
graph TD; 0 1 2 1 --> 2 1 --> 3 1 --> 4 2 --> 3 2 --> 4 0 --> 1 0 --> 2 0 --> 3 0 --> 4 3 --> 4
Build the adjacency list
Adjacency list will be:
|
|
Build In-degrees Vector
|
|
Topological Sort
This is the code for that part:
|
|
Since, 0
is the only node with inDegree 0
, we add to queue and start the BFS.
Now, the BFS starts.
Iteration 1
We pop out curr = 0
, and start processing its neighbours - [1, 2, 3, 4]
. We first reduce indegrees of neighbours, because we have processed their ancestor. Now, they are going to be next ancestor for processing.
Now, if any neighbours in-degree is now 0, we add it to the queue for processing. In this case, it will be 1. This is how the indegree will look now:
|
|
Iteration 2
Now, we pop out curr = 1
. We now process all its neighbours, i.e. [2, 3, 4]
.
We reduce their indegree by 1, add the one with indegree = 0
in the queue, i.e. 2.
This is how the indegree will look now:
|
|
Finally
This will go and finally topological order will be [0, 1, 2, 3, 4]
.
Initialize and Populate ancestors
|
|
This is the adjacency list and topological order:
|
|
Iteration 1, node = 0
First node in list is node 0
. So,
- we will get all its neighbours, and add it as an ancestors to all its neighbours
[1, 2, 3, 4]
- Then, we will add all the ancestors of this ancestor to the list of all the ancestors.
Since node 0
has no ancestor, so only 0
wil be added as ancestor to its neighbours.
So, this is how ancestors look like:
|
|
Iteration 2, node = 1
Now, we get node = 1
. We will process its neighbours.
- we will get all its neighbours, and add it as an ancestors to all its neighbours
[2,3,4]
- Then, we will add all the ancestors of this ancestor to the list of all the ancestors. Node
1
ancestor is node0
. If the ancestor is already added, we will just ignore it for addition. In this0
is already present in the ancestor vector, for all the nodes.
This is how the ancestors will be build:
|
|
Finally
Resulting ancestors
set:
|
|
Convert to sorted list
We need the ancestors to be sorted as per the problem, so we sort them and add to answers, and this is how the answer now looks:
|
|