All Ancestors of a Node in a Directed Acyclic Graph

Updated on 20 May, 2025
All Ancestors of a Node in a Directed Acyclic Graph header image

Problem Statement

In this task, you're given a Directed Acyclic Graph (DAG) constituted of n nodes, labeled from 0 to n-1. You are provided with the description of the DAG in the form of a 2D integer array edges, where each entry edges[i] = [fromi, toi] signifies a unidirectional edge stemming from node fromi leading to node toi.

Your objective is to determine and return a list answer, where for each node i in the DAG, answer[i] contains a sorted list of nodes that are ancestors of node i. In other words, a node u is considered an ancestor of another node v if there exists a directed trail of edges from u to v.

Examples

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 <= 1000
  • 0 <= edges.length <= min(2000, n * (n - 1) / 2)
  • edges[i].length == 2
  • 0 <= fromi, toi <= n - 1
  • fromi != toi
  • There are no duplicate edges.
  • The graph is directed and acyclic.

Approach and Intuition

When solving this problem, think about the direct relationship between nodes in terms of ancestry where a node can influence another through a series of connections (edges). The plan involves:

  1. Graph Representation: Representing the graph using adjacency lists helps in easily traversing from any given node.

  2. Traversing the Graph: For determining the ancestors of each node, we can employ Depth-First Search (DFS) or Breadth-First Search (BFS). DFS is particularly useful in exploring the depths of each potential ancestral chain.

  3. Ancestor Tracking: Using a recursive or iterative approach, you can backtrack from each node up to its ultimate predecessors (origin nodes with no incoming edges), thus marking the reachable ancestors.

  4. Result Compilation: After assessing all possible routes from every node, compile the list of ancestors for each node into a final result list where each sublist contains the ancestors sorted in ascending order.

  • Complexity Consideration: Given n nodes and the edges constraints, the computation needs careful consideration to remain efficient. The graph traversal typically would be in the order of O(V + E) where V is the number of vertices and E is the number of edges. However, discovering all ancestors might appear more complex due to repeated traversals; hence, optimizing with memoization or reducing redundant checks is beneficial.

  • Examples Review: The examples provided in the problem play a critical role in demonstrating simple to complex relationships between nodes and how they extrapolate to ancestor lists. Visualization of the graph structure based on these examples can add clarity to the traversal and ancestor discovery strategies.

Solutions

  • C++
  • Java
  • Python
cpp
class Solution {
public:
    vector<vector<int>> findAncestors(int totalNodes, vector<vector<int>>& relations) {
        // Build adjacency list representation of graph
        vector<vector<int>> adjList(totalNodes);

        // Initialize indegrees and build graph structure from relations
        vector<int> nodeInDegree(totalNodes, 0);
        for (auto& rel : relations) {
            int start = rel[0];
            int end = rel[1];
            adjList[start].push_back(end);
            nodeInDegree[end]++;
        }

        queue<int> zeroIndegreeNodes;
        for (int i = 0; i < nodeInDegree.size(); i++) {
            if (nodeInDegree[i] == 0) {
                zeroIndegreeNodes.push(i);
            }
        }

        // Prepare for topological sorting
        vector<int> topologicalSortResult;
        while (!zeroIndegreeNodes.empty()) {
            int node = zeroIndegreeNodes.front();
            zeroIndegreeNodes.pop();
            topologicalSortResult.push_back(node);

            // Adjust indegrees based on adjacency list
            for (int adjNode : adjList[node]) {
                nodeInDegree[adjNode]--;
                if (nodeInDegree[adjNode] == 0) {
                    zeroIndegreeNodes.push(adjNode);
                }
            }
        }

        // Ancestral tracking using a set for unique ancestors
        vector<vector<int>> ancestors(totalNodes);
        vector<unordered_set<int>> ancestorSets(totalNodes);

        // Process nodes in order determined by topological sort
        for (int node : topologicalSortResult) {
            for (int adjNode : adjList[node]) {
                ancestorSets[adjNode].insert(node);
                ancestorSets[adjNode].insert(
                    ancestorSets[node].begin(),
                    ancestorSets[node].end());
            }
        }

        // Convert ancestral sets into sorted lists
        for (int i = 0; i < ancestors.size(); i++) {
            ancestors[i].assign(ancestorSets[i].begin(), ancestorSets[i].end());
            sort(ancestors[i].begin(), ancestors[i].end());
        }

        return ancestors;
    }
};

The solution provided in C++ aims to find all the ancestors of each node in a Directed Acyclic Graph (DAG). This solution effectively utilizes the graph representation, topological sort, and set data structures to keep track of the ancestors. Here is an outline of how the solution is implemented:

  • Graph Initialization:

    • A graph is represented using an adjacency list, which is constructed based on the given relations between nodes.
    • An array for node indegrees is also maintained.
  • Topological Sorting:

    • Nodes with zero indegree are identified and processed first using a queue. This helps in performing a topological sort.
    • As nodes are processed, indegrees of adjacent nodes are updated. Nodes with updated zero indegree are further added to the queue for processing.
  • Tracking Ancestors:

    • Ancestors for each node are tracked using a vector of unordered sets. As the nodes are processed in topological order:
      • Direct ancestors and the ancestors of its ancestors (propagated through topological sorting) are added to the set.
    • After all nodes are processed, these sets are converted to sorted vectors to represent the ancestors of each node.

This approach ensures that all ancestors of a node are found efficiently leveraging the properties of DAGs and avoiding cycles by design. The process involves building the graph and sorting the nodes which lays the groundwork for a reliable tracking of ancestors using the properties of topological order. The output, a vector of vectors, lists all ancestors for each node in a sorted manner, making it straightforward to read and utilize.

java
class Solution {

    public List<List<Integer>> findAllAncestors(int vertices, int[][] relationships) {
        // Initialize adjacency list
        List<Integer>[] graph = new ArrayList[vertices];
        for (int vertex = 0; vertex < vertices; vertex++) {
            graph[vertex] = new ArrayList<>();
        }

        // Populate the graph and count in-degrees
        int[] inDegrees = new int[vertices];
        for (int[] relation : relationships) {
            int start = relation[0];
            int end = relation[1];
            graph[start].add(end);
            inDegrees[end]++;
        }

        // Queue for processing independent nodes (zero in-degree)
        Queue<Integer> zeroDegreeNodes = new LinkedList<>();
        for (int i = 0; i < vertices; i++) {
            if (inDegrees[i] == 0) {
                zeroDegreeNodes.add(i);
            }
        }

        // Sorting nodes topologically
        List<Integer> sortedNodes = new ArrayList<>();
        while (!zeroDegreeNodes.isEmpty()) {
            int current = zeroDegreeNodes.poll();
            sortedNodes.add(current);
            for (int adjNode : graph[current]) {
                inDegrees[adjNode]--;
                if (inDegrees[adjNode] == 0) {
                    zeroDegreeNodes.add(adjNode);
                }
            }
        }

        // Sets to store ancestors and result list initiation
        List<List<Integer>> ancestors = new ArrayList<>();
        List<Set<Integer>> ancestorSets = new ArrayList<>();
        for (int i = 0; i < vertices; i++) {
            ancestors.add(new ArrayList<>());
            ancestorSets.add(new HashSet<>());
        }

        // Determine each node's ancestors
        for (int node : sortedNodes) {
            for (int adjacent : graph[node]) {
                ancestorSets.get(adjacent).add(node);
                ancestorSets.get(adjacent).addAll(ancestorSets.get(node));
            }
        }

        // Converting set to list for the final output
        for (int i = 0; i < vertices; i++) {
            for (int ancestor : ancestorSets.get(i)) {
                ancestors.get(i).add(ancestor);
            }
        }

        return ancestors;
    }
}

This solution in Java comprehensively addresses the problem of finding all ancestors of each node in a Directed Acyclic Graph (DAG). Here’s a breakdown of how it achieves its goal:

  1. Graph Initialization: It starts by creating an adjacency list representation for the graph and initializing an array to count the in-degrees of each vertex. This setup is essential for managing relationships and dependencies between nodes.

  2. Building the Graph: Using the relationships provided, the code populates the graph and updates the in-degrees for each node. This step is crucial for understanding which nodes depend on which and helps facilitate the subsequent topological sorting.

  3. Topological Sorting: The algorithm implements a topological sort by using a queue to process all nodes with zero in-degree. Nodes are dequeued, processed, and their dependents' in-degrees are decremented. Nodes with decremented in-degrees that drop to zero are then enqueued themselves. This process ensures that nodes are processed only after all their predecessors have been processed.

  4. Tracking Ancestors: The solution introduces a list of sets to track the ancestors of each node efficiently. As each node is processed in the topological order, it propagates its ancestors (along with itself) to its dependent nodes. This step is computed using the adjacency list and the sets of ancestors formulated in the prior steps.

  5. Final Conversion: Converts these sets into lists for each node. This makes the output more usable since the problem specifically requires a list of lists as the output format.

By efficiently combining graph representation, in-degree management, topological sorting, and ancestor propagation, this Java solution effectively computes all ancestors for each node in a DAG. The usage of data structures like lists, sets, and queues ensures that operations remain efficient and the execution remains clear and organized, leading to a well-rounded and correct implementation.

python
class Solution:
    def ancestorFinder(self, total_nodes, connections):
        # Adjacency representation of the graph
        adjacency_structure = [[] for _ in range(total_nodes)]

        # Creating initial node dependencies
        node_indegree = [0] * total_nodes
        for src, dest in connections:
            adjacency_structure[src].append(dest)
            node_indegree[dest] += 1

        # Nodes with no prerequisites
        entry_points = [node for node in range(total_nodes) if node_indegree[node] == 0]

        # Result of nodes in dependency order
        dependency_order = []
        while entry_points:
            active_node = entry_points.pop(0)
            dependency_order.append(active_node)

            # Updating indegrees and managing new possible entry points
            for linked_node in adjacency_structure[active_node]:
                node_indegree[linked_node] -= 1
                if node_indegree[linked_node] == 0:
                    entry_points.append(linked_node)

        # Preparing the ancestor list output
        result_ancestors = [[] for _ in range(total_nodes)]
        ancestor_tracker = [set() for _ in range(total_nodes)]

        # Building the ancestor relationships from ordered nodes
        for processed_node in dependency_order:
            for connected_node in adjacency_structure[processed_node]:
                ancestor_tracker[connected_node].add(processed_node)
                ancestor_tracker[connected_node].update(ancestor_tracker[processed_node])

        # Converting ancestor sets to sorted lists
        for index, ancestors in enumerate(ancestor_tracker):
            result_ancestors[index] = sorted(ancestors)

        return result_ancestors

The given Python code effectively captures the problem statement for finding all ancestors of a node in a Directed Acyclic Graph (DAG). It implements a powerful approach using an adjacency list and degree management for navigating through the graph efficiently.

  • Understand the representation of the graph:

    • The adjacency_structure array is initialized to store the connections (edges) from each node.
    • For each connection defined in the connections list, this structure is populated, and an indegree array recording the number of direct ancestors each node has is also updated.
  • Work with entry points:

    • The entry_points list is managed to track nodes that have no incoming edges (ancestors), starting the process with them.
    • This solution uses a breadth-first strategy to process nodes from entry_points iteratively.
  • Develop dependency order:

    • Nodes are processed in an order derived from their dependencies. A node is moved from entry_points to dependency_order only when all its ancestors have been processed.
  • Track and manage ancestor relationships:

    • An array ancestor_tracker is used to keep track of ancestors for each node dynamically. This set captures all ancestors for a node as the algorithm progresses through each node in dependency_order.
  • Output preparation:

    • The accumulated data in the ancestor_tracker is converted into sorted lists in result_ancestors, making it structured and directly usable.

Apply this solution for efficiently identifying all ancestors of nodes in a DAG using the adjacency list, indegree handling, and systematic node order processing.

Comments

No comments yet.