from collections import defaultdict, deque
class Solution:
def getAncestors(self, n, edges):
graph = defaultdict(list)
indegree = [0] * n
for u, v in edges:
graph[u].append(v)
indegree[v] += 1
ancestors = [set() for _ in range(n)]
queue = deque([i for i in range(n) if indegree[i] == 0])
while queue:
node = queue.popleft()
for nei in graph[node]:
ancestors[nei].update(ancestors[node])
ancestors[nei].add(node)
indegree[nei] -= 1
if indegree[nei] == 0:
queue.append(nei)
return [sorted(list(s)) for s in ancestors]
#include <vector>
#include <queue>
#include <set>
using namespace std;
class Solution {
public:
vector<vector<int>> getAncestors(int n, vector<vector<int>>& edges) {
vector<vector<int>> graph(n);
vector<int> indegree(n, 0);
for (auto& e : edges) {
graph[e[0]].push_back(e[1]);
indegree[e[1]]++;
}
vector<set<int>> ancestors(n);
queue<int> q;
for (int i = 0; i < n; ++i)
if (indegree[i] == 0) q.push(i);
while (!q.empty()) {
int node = q.front(); q.pop();
for (int nei : graph[node]) {
ancestors[nei].insert(ancestors[node].begin(), ancestors[node].end());
ancestors[nei].insert(node);
indegree[nei]--;
if (indegree[nei] == 0)
q.push(nei);
}
}
vector<vector<int>> res(n);
for (int i = 0; i < n; ++i)
res[i] = vector<int>(ancestors[i].begin(), ancestors[i].end());
return res;
}
};
import java.util.*;
class Solution {
public List<List<Integer>> getAncestors(int n, int[][] edges) {
List<List<Integer>> graph = new ArrayList<>();
int[] indegree = new int[n];
for (int i = 0; i < n; ++i) graph.add(new ArrayList<>());
for (int[] e : edges) {
graph.get(e[0]).add(e[1]);
indegree[e[1]]++;
}
List<Set<Integer>> ancestors = new ArrayList<>();
for (int i = 0; i < n; ++i) ancestors.add(new HashSet<>());
Queue<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; ++i)
if (indegree[i] == 0) q.offer(i);
while (!q.isEmpty()) {
int node = q.poll();
for (int nei : graph.get(node)) {
ancestors.get(nei).addAll(ancestors.get(node));
ancestors.get(nei).add(node);
indegree[nei]--;
if (indegree[nei] == 0)
q.offer(nei);
}
}
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < n; ++i) {
List<Integer> temp = new ArrayList<>(ancestors.get(i));
Collections.sort(temp);
res.add(temp);
}
return res;
}
}
/**
* @param {number} n
* @param {number[][]} edges
* @return {number[][]}
*/
var getAncestors = function(n, edges) {
const graph = Array.from({length: n}, () => []);
const indegree = Array(n).fill(0);
for (const [u, v] of edges) {
graph[u].push(v);
indegree[v]++;
}
const ancestors = Array.from({length: n}, () => new Set());
const queue = [];
for (let i = 0; i < n; i++) {
if (indegree[i] === 0) queue.push(i);
}
while (queue.length) {
const node = queue.shift();
for (const nei of graph[node]) {
for (const anc of ancestors[node]) ancestors[nei].add(anc);
ancestors[nei].add(node);
indegree[nei]--;
if (indegree[nei] === 0) queue.push(nei);
}
}
return ancestors.map(s => Array.from(s).sort((a, b) => a - b));
};
You are given a directed acyclic graph (DAG) with n
nodes labeled from 0
to n - 1
and a list of directed edges edges
where each edge is represented as [from, to]
.
Your task is to find for every node all of its ancestors—meaning all nodes from which there is a directed path to the given node.
The ancestors for each node should be returned as a sorted list with no duplicates.
Key constraints:
i
-th element is a sorted list of all ancestors of node i
.At first glance, we might consider running a graph traversal (like DFS or BFS) from every node to find its ancestors. However, this approach would be inefficient, especially for large graphs, since it would involve redundant traversals and repeated work.
Instead, we notice that since the graph is acyclic, we can process nodes in topological order. This means that when we visit a node, all its ancestors have already been processed. We can then propagate ancestor information efficiently from each node to its neighbors.
The core insight is to use a combination of topological sorting and set operations to accumulate and propagate ancestor sets as we traverse the graph, ensuring each ancestor is only recorded once per node.
We solve the problem using the following steps:
This design ensures that each ancestor is computed only once per node, and we avoid redundant traversals by leveraging the DAG's properties and topological ordering.
Suppose n = 4
and edges = [[0,1],[1,2],[2,3],[0,2]]
.
Brute-force approach: For each node, run a DFS or BFS to find all ancestors. This can be O(n(n+e))
, where e
is the number of edges, since each traversal could visit much of the graph.
Optimized/topological approach:
O(e)
O(n + e)
O(n)
size, so total set operations are O(n^2)
in the worst case.O(n^2 + e)
time and O(n^2)
space (for storing all ancestor sets).This problem leverages the properties of DAGs and topological order to efficiently compute all ancestors for each node. By propagating ancestor sets in topological order, we avoid redundant work and ensure each ancestor is only added once per node. The solution is both efficient and elegant, demonstrating the power of combining graph traversal strategies with set operations for problems involving reachability and ancestry in directed graphs.