Skip to content

193. All Ancestors of a Node in a Directed Acyclic Graph

πŸ”— LeetCode Problem: 2192. All Ancestors of a Node in a Directed Acyclic Graph
πŸ“Š Difficulty: Medium
🏷️ Topics: Graph, DFS, BFS, Topological Sort, DAG

Problem Statement

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] = [fromα΅’, toα΅’] denotes that there is a unidirectional edge from fromα΅’ to toα΅’ in the graph.

Return a list answer, where answer[i] is the list of ancestors of the iα΅—Κ° 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, edges = [[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:
  Graph visualization:
       0 ────→ 3 ────→ 5
       ↓       ↓ β†˜     
       4 ──→ 6   7 ←── 2
       ↑             β†—
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  Ancestors:
    Node 0: [] (no incoming edges)
    Node 1: [] (no incoming edges)
    Node 2: [] (no incoming edges)
    Node 3: [0,1] (0β†’3, 1β†’3)
    Node 4: [0,2] (0β†’4, 2β†’4)
    Node 5: [0,1,3] (can reach via 0β†’3β†’5, 1β†’3β†’5)
    Node 6: [0,1,2,3,4] (multiple paths!)
    Node 7: [0,1,2,3] (0β†’3β†’7, 1β†’3β†’7, 2β†’7)

Example 2:

Input: n = 5, edges = [[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:
  All nodes eventually lead to node 4.
  Node 4 has ancestors from all other nodes!

Constraints: - 1 <= n <= 1000 - 0 <= edges.length <= min(2000, n * (n - 1) / 2) - edges[i].length == 2 - 0 <= fromα΅’, toα΅’ <= n - 1 - fromα΅’ != toα΅’ - There are no duplicate edges. - The graph is directed and acyclic.


🌳 Visual Understanding - The Complete Picture

The Problem We're Solving πŸ€”

Find ALL ancestors for each node in a DAG: 🎯

Example:
       0 β†’ 1 β†’ 2
           ↓ β†—
           3

Ancestors:
  Node 0: [] (starting node)
  Node 1: [0] (0 can reach 1)
  Node 2: [0,1] (0β†’1β†’2, 1β†’2)
  Node 3: [0,1,2] (0β†’1β†’3, 1β†’3, 2β†’3)

Question: For each node, who can reach it? πŸ‘₯

Building Understanding Through Examples πŸ“š

Example 1: Simple chain

Graph: 0 β†’ 1 β†’ 2 β†’ 3

Ancestors:
  Node 0: []
  Node 1: [0]
  Node 2: [0,1] (0β†’1β†’2, 1β†’2)
  Node 3: [0,1,2] (0β†’1β†’2β†’3, 1β†’2β†’3, 2β†’3)

Pattern: Each node inherits all ancestors from parents!
Example 2: Diamond shape

Graph:
     0
    / \
   1   2
    \ /
     3

Ancestors:
  Node 0: []
  Node 1: [0]
  Node 2: [0]
  Node 3: [0,1,2] (paths: 0β†’1β†’3, 0β†’2β†’3, 0β†’3 implied)

Node 3 has ancestors from BOTH paths! πŸ”€
Example 3: Complex DAG

Graph:
     0 β†’ 1 β†’ 3
     ↓   ↓
     2 β†’ 4

Ancestors:
  Node 0: []
  Node 1: [0]
  Node 2: [0]
  Node 3: [0,1]
  Node 4: [0,1,2] (0β†’2β†’4, 1β†’4, 0β†’1β†’4)

Multiple paths converge! Need to track all! πŸ“Š
Visual: Ancestor Propagation 🌊

Start:      0 (ancestors: {})
            ↓
Level 1:    1 (ancestors: {0})
            ↓
Level 2:    2 (ancestors: {0,1})
            ↓
Level 3:    3 (ancestors: {0,1,2})

Each node inherits parent's ancestors + parent itself! πŸ”‘

The Pattern Emerges πŸ’‘

Key observations: πŸ”

1️⃣ Transitive Reachability 🎯
   If A→B and B→C, then A is ancestor of C
   Ancestors propagate through edges!

2️⃣ DFS or BFS Works 🌊
   From each node, explore all reachable nodes
   OR: For each node, find who can reach it

3️⃣ Topological Sort Order Helps ⚑
   Process nodes in topological order
   Ancestors already computed for parents!

4️⃣ Need Deduplication πŸ”„
   Multiple paths to same node
   Use Set to avoid duplicates

5️⃣ Two Approaches πŸ› οΈ
   Approach 1: For each node, DFS to find reachable
   Approach 2: Topological sort, propagate ancestors

🧠 The Journey From Brute Force to Optimal

Part 1: Understanding the Problem - "Who can reach me?" πŸ€”

πŸ’­ The key question: For each node, who are its ancestors?

Ancestor = a node that can REACH this node through edges

Example:
  Graph: 0 β†’ 1 β†’ 2

  Node 0: Who can reach 0? Nobody! ancestors[0] = []
  Node 1: Who can reach 1? Just 0!  ancestors[1] = [0]
  Node 2: Who can reach 2? 0 and 1! ancestors[2] = [0,1]

Two perspectives:
  1. Forward: From each node, who can I reach? (descendants)
  2. Backward: For each node, who can reach me? (ancestors)

Both work! Let's explore! πŸ”

Part 2: Approach 1 - "Reverse graph + DFS" πŸ’­

πŸ€” Simplest idea: Reverse the graph!

Original graph: A β†’ B means "A is ancestor of B"
Reversed graph: B β†’ A means "B can find A by following edges"

Algorithm:
  1. Reverse all edges
  2. For each node, DFS to find all reachable
  3. Those reachable nodes = ancestors!

Example:
  Original: 0 β†’ 1 β†’ 2
  Reversed: 0 ← 1 ← 2

  DFS from 0 (reversed): reaches nothing β†’ []
  DFS from 1 (reversed): reaches 0 β†’ [0]
  DFS from 2 (reversed): reaches 1, 0 β†’ [0,1]

Perfect! Direct ancestor finding! ✨

Time: O(n Γ— (V + E))
  For each node: one DFS

Very intuitive! ⭐

Part 3: Approach 2 - "BFS with topological sort" πŸ’‘

πŸ€” Smarter approach: Use topological order!

Key insight:
  Process nodes in topological order
  When we process a node, all its parents already done!
  Just inherit ancestors from parents!

Algorithm:
  1. Topological sort (Kahn's BFS)
  2. Process in order:
     For each parent of current node:
       Add all parent's ancestors to current
       Add parent itself to current

Example:
  Graph: 0 β†’ 1 β†’ 2

  Topological order: [0, 1, 2]

  Process 0: No parents β†’ ancestors[0] = []

  Process 1: Parent is 0
    ancestors[1] = ancestors[0] + {0}
                 = {} + {0}
                 = {0} βœ…

  Process 2: Parent is 1
    ancestors[2] = ancestors[1] + {1}
                 = {0} + {1}
                 = {0, 1} βœ…

Elegant propagation! 🌊

Time: O(V + E + total_ancestors)
  Topological sort: O(V + E)
  For each edge, copy ancestors

Often faster in practice! ⚑

Part 4: Comparing Approaches πŸ’­

πŸ€” Which approach to use?

Approach 1: Reverse Graph + DFS
  βœ… Very intuitive
  βœ… Direct: "find who can reach me"
  βœ… Simple to code
  ❌ Might revisit nodes multiple times
  Time: O(n Γ— (V + E))

Approach 2: Topological Sort + BFS Propagation
  βœ… Each node processed once
  βœ… Inherits from parents (elegant!)
  βœ… Often faster in practice
  ❌ Slightly more complex logic
  Time: O(V + E + total_ancestors)

Both correct! Both O(nΒ²) worst case!

For learning:
  - Reverse DFS: Easier to understand ⭐
  - Topological BFS: More elegant 🌟

We'll show both! 🎯

Part 5: The Pattern - "Ancestor propagation in DAG!" 🎯

✨ This is about ANCESTOR PROPAGATION in DAG!

Key concepts:
  βœ… Directed Acyclic Graph (DAG)
  βœ… Transitive relationships (if Aβ†’Bβ†’C, then A ancestor of C)
  βœ… Use topological order to propagate efficiently

Why topological sort helps:
  Parents processed before children
  Children can inherit all parent ancestors
  No redundant computation!

This pattern applies to:
  - Dependency chains
  - Inheritance hierarchies  
  - Transitive closure computation
  - Reachability queries

Two valid approaches:
  1. Reverse + DFS: Intuitive, direct
  2. Topo + BFS: Elegant, efficient

Both teach important graph concepts! 🌟

🎯 Approach 1: Reverse Graph + DFS (Simplest!)

The Key Insight πŸ’‘

Reverse all edges then DFS from each node πŸ”„. In reversed graph, following edges goes from descendants to ancestors πŸ”€. DFS from node collects all its ancestors directly! 🎯 Build reversed adjacency list πŸ—οΈ. For each node, DFS finds all reachable in reversed graph = all ancestors ✨. Use TreeSet for sorted order πŸ“Š. Time: O(nΓ—(V+E)) ⚑, Space: O(nΒ²) πŸ“¦!

Most intuitive approach! ⭐

Complete Implementation

class Solution {
    public List<List<Integer>> getAncestors(int n, int[][] edges) {
        // ═══════════════════════════════════════════════════
        // REVERSE GRAPH + DFS APPROACH πŸ”„
        // ═══════════════════════════════════════════════════
        // Reverse edges: Aβ†’B becomes A←B
        // DFS from each node finds its ancestors directly!
        // ═══════════════════════════════════════════════════

        // ═══════════════════════════════════════════════════
        // BUILD REVERSED ADJACENCY LIST πŸ—οΈ
        // ═══════════════════════════════════════════════════
        List<List<Integer>> reversedGraph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            reversedGraph.add(new ArrayList<>());
        }

        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            // REVERSED: to β†’ from
            reversedGraph.get(to).add(from);
        }

        // ═══════════════════════════════════════════════════
        // FIND ANCESTORS FOR EACH NODE πŸ”
        // ═══════════════════════════════════════════════════
        List<List<Integer>> result = new ArrayList<>();

        for (int node = 0; node < n; node++) {
            // ───────────────────────────────────────────────
            // Use TreeSet for automatic sorting!
            // ───────────────────────────────────────────────
            Set<Integer> ancestors = new TreeSet<>();
            boolean[] visited = new boolean[n];

            // ───────────────────────────────────────────────
            // DFS in reversed graph to find ancestors
            // ───────────────────────────────────────────────
            for (int parent : reversedGraph.get(node)) {
                dfs(parent, reversedGraph, ancestors, visited);
            }

            result.add(new ArrayList<>(ancestors));
        }

        return result;
    }

    // ═══════════════════════════════════════════════════════
    // DFS IN REVERSED GRAPH πŸ”
    // ═══════════════════════════════════════════════════════
    private void dfs(int node, List<List<Integer>> reversedGraph,
                     Set<Integer> ancestors, boolean[] visited) {
        // ───────────────────────────────────────────────────
        // Mark as visited and add to ancestors
        // ───────────────────────────────────────────────────
        visited[node] = true;
        ancestors.add(node);

        // ───────────────────────────────────────────────────
        // Visit all neighbors (parents in original graph)
        // ───────────────────────────────────────────────────
        for (int parent : reversedGraph.get(node)) {
            if (!visited[parent]) {
                dfs(parent, reversedGraph, ancestors, visited);
            }
        }
    }
}

Why This Works 🎯

Reversed edges invert relationships: πŸ”„ - Original: Aβ†’B means "A is ancestor of B" - Reversed: A←B means "follow backward to find A" - DFS backward = find all ancestors!

Direct ancestor collection: 🎯 - Start from node's immediate parents - Recursively find their ancestors - Collect all reachable = all ancestors!

Visited prevents revisiting: βœ… - Each ancestor visited once per DFS - Efficient exploration!

TreeSet gives sorted order: πŸ“Š - Automatic sorting as we add - No final sort needed!


🎯 Approach 2: BFS with Topological Sort (More Elegant!)

The Key Insight πŸ’‘

Use topological sort to process nodes in dependency order 🌊. When processing a node, all parents already done βœ…. Inherit ancestors from parents + add parents themselves! πŸ“ Use Kahn's algorithm for topological order πŸ“Š. For each node, collect ancestors from all parents πŸ‘₯. Elegant propagation! Time: O(V+E+total_ancestors) ⚑, Space: O(nΒ²) πŸ“¦!

Clean and efficient! 🌟

Complete Implementation

class Solution {
    public List<List<Integer>> getAncestors(int n, int[][] edges) {
        // ═══════════════════════════════════════════════════
        // TOPOLOGICAL SORT + ANCESTOR PROPAGATION 🌊
        // ═══════════════════════════════════════════════════
        // Process nodes in topological order
        // Inherit ancestors from parents!
        // ═══════════════════════════════════════════════════

        // ═══════════════════════════════════════════════════
        // BUILD ADJACENCY LIST πŸ—οΈ
        // ═══════════════════════════════════════════════════
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            graph.add(new ArrayList<>());
        }

        int[] inDegree = new int[n];

        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            graph.get(from).add(to);
            inDegree[to]++;
        }

        // ═══════════════════════════════════════════════════
        // INITIALIZE ANCESTOR SETS πŸ“Š
        // ═══════════════════════════════════════════════════
        List<Set<Integer>> ancestorSets = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            ancestorSets.add(new TreeSet<>());
        }

        // ═══════════════════════════════════════════════════
        // KAHN'S ALGORITHM - TOPOLOGICAL SORT 🌊
        // ═══════════════════════════════════════════════════
        Queue<Integer> queue = new LinkedList<>();

        // Find nodes with no incoming edges
        for (int i = 0; i < n; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        while (!queue.isEmpty()) {
            int current = queue.poll();

            // ───────────────────────────────────────────────
            // PROCESS CHILDREN: Inherit ancestors! 🎯
            // ───────────────────────────────────────────────
            for (int child : graph.get(current)) {
                // ───────────────────────────────────────────
                // Child inherits all of current's ancestors
                // ───────────────────────────────────────────
                ancestorSets.get(child).addAll(ancestorSets.get(current));

                // ───────────────────────────────────────────
                // AND current itself is ancestor of child!
                // ───────────────────────────────────────────
                ancestorSets.get(child).add(current);

                // ───────────────────────────────────────────
                // Update in-degree
                // ───────────────────────────────────────────
                inDegree[child]--;
                if (inDegree[child] == 0) {
                    queue.offer(child);
                }
            }
        }

        // ═══════════════════════════════════════════════════
        // CONVERT SETS TO LISTS πŸ“‹
        // ═══════════════════════════════════════════════════
        List<List<Integer>> result = new ArrayList<>();
        for (Set<Integer> ancestorSet : ancestorSets) {
            result.add(new ArrayList<>(ancestorSet));
        }

        return result;
    }
}

Why This Works 🎯

Topological order ensures parents first: πŸ“Š - Parents processed before children - Children can safely inherit from parents - No missing ancestors!

Inheritance propagation: 🌊 - child gets parent's ancestors - child gets parent itself - Transitive: if Aβ†’Bβ†’C, then C gets both A and B!

TreeSet maintains order: πŸ“Š - Sorted automatically - Handles duplicates (from multiple parents)

Kahn's algorithm guarantees order: βœ… - Process zero in-degree first - Always correct topological order!


πŸ” Detailed Dry Run

Let's trace Approach 1 (Reverse Graph + DFS) on a small example:

n = 4
edges = [[0,1],[0,2],[1,3],[2,3]]

╔══════════════════════════════════════════════════════════════╗
β•‘                            SETUP                             β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Build REVERSED graph from edges:
  Original [0,1]: 0β†’1  becomes  Reversed: 1β†’0
  Original [0,2]: 0β†’2  becomes  Reversed: 2β†’0
  Original [1,3]: 1β†’3  becomes  Reversed: 3β†’1
  Original [2,3]: 2β†’3  becomes  Reversed: 3β†’2

Reversed adjacency list:
  0: []        (no parents)
  1: [0]       (0 is parent)
  2: [0]       (0 is parent)
  3: [1, 2]    (1 and 2 are parents)

Original graph:          Reversed graph:
     0                        0
    / \                      ↑ ↑
   1   2                    /   \
    \ /                    1     2
     3                      ↑   ↑
                             \ /
                              3

DFS for Each Node πŸ”

╔═════════════════════════════════════════════════╗
β•‘ πŸ” FIND ANCESTORS OF NODE 0                    β•‘
╠═════════════════════════════════════════════════╣
β•‘ Start from node 0's parents in reversed graph  β•‘
β•‘                                                 β•‘
β•‘ Parents of 0: [] (empty)                       β•‘
β•‘ No parents to explore!                          β•‘
β•‘                                                 β•‘
β•‘ ancestors[0] = {} β†’ []                          β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

╔═════════════════════════════════════════════════╗
β•‘ πŸ” FIND ANCESTORS OF NODE 1                    β•‘
╠═════════════════════════════════════════════════╣
β•‘ Parents of 1: [0]                              β•‘
β•‘                                                 β•‘
β•‘ ───────────────────────────────────────────────║
β•‘ DFS from parent 0:                              β•‘
β•‘ ───────────────────────────────────────────────║
β•‘   visited[0] = true                             β•‘
β•‘   Add 0 to ancestors βœ…                        β•‘
β•‘   ancestors = {0}                               β•‘
β•‘                                                 β•‘
β•‘   Parents of 0: [] (none)                      β•‘
β•‘   Done!                                         β•‘
β•‘                                                 β•‘
β•‘ ancestors[1] = {0} β†’ [0]                        β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

╔═════════════════════════════════════════════════╗
β•‘ πŸ” FIND ANCESTORS OF NODE 2                    β•‘
╠═════════════════════════════════════════════════╣
β•‘ Parents of 2: [0]                              β•‘
β•‘                                                 β•‘
β•‘ ───────────────────────────────────────────────║
β•‘ DFS from parent 0:                              β•‘
β•‘ ───────────────────────────────────────────────║
β•‘   visited[0] = true                             β•‘
β•‘   Add 0 to ancestors βœ…                        β•‘
β•‘   ancestors = {0}                               β•‘
β•‘                                                 β•‘
β•‘   Parents of 0: [] (none)                      β•‘
β•‘   Done!                                         β•‘
β•‘                                                 β•‘
β•‘ ancestors[2] = {0} β†’ [0]                        β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

╔═════════════════════════════════════════════════╗
β•‘ πŸ” FIND ANCESTORS OF NODE 3                    β•‘
╠═════════════════════════════════════════════════╣
β•‘ Parents of 3: [1, 2]                           β•‘
β•‘                                                 β•‘
β•‘ ───────────────────────────────────────────────║
β•‘ DFS from parent 1:                              β•‘
β•‘ ───────────────────────────────────────────────║
β•‘   visited[1] = true                             β•‘
β•‘   Add 1 to ancestors βœ…                        β•‘
β•‘   ancestors = {1}                               β•‘
β•‘                                                 β•‘
β•‘   Parents of 1: [0]                            β•‘
β•‘                                                 β•‘
β•‘   DFS from 0:                                   β•‘
β•‘     visited[0] = true                           β•‘
β•‘     Add 0 to ancestors βœ…                      β•‘
β•‘     ancestors = {0, 1}                          β•‘
β•‘                                                 β•‘
β•‘     Parents of 0: [] (none)                    β•‘
β•‘     Done!                                       β•‘
β•‘                                                 β•‘
β•‘ ───────────────────────────────────────────────║
β•‘ DFS from parent 2:                              β•‘
β•‘ ───────────────────────────────────────────────║
β•‘   visited[2] = true                             β•‘
β•‘   Add 2 to ancestors βœ…                        β•‘
β•‘   ancestors = {0, 1, 2}                         β•‘
β•‘                                                 β•‘
β•‘   Parents of 2: [0]                            β•‘
β•‘                                                 β•‘
β•‘   Try DFS from 0:                               β•‘
β•‘     visited[0] = true (already!)               β•‘
β•‘     Skip! (already explored)                    β•‘
β•‘                                                 β•‘
β•‘ ancestors[3] = {0, 1, 2} β†’ [0, 1, 2]           β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

FINAL RESULT πŸŽ‰

╔═══════════════════════════════════════════════╗
β•‘    πŸŽ‰ FINAL RESULT: [[],[0],[0],[0,1,2]]     β•‘
╠═══════════════════════════════════════════════╣
β•‘                                               β•‘
β•‘  Verification:                                β•‘
β•‘    Node 0: No incoming edges βœ…               β•‘
β•‘    Node 1: 0β†’1 βœ…                             β•‘
β•‘    Node 2: 0β†’2 βœ…                             β•‘
β•‘    Node 3: 0β†’1β†’3, 0β†’2β†’3 βœ…                   β•‘
β•‘           All ancestors: {0, 1, 2} βœ…         β•‘
β•‘                                               β•‘
β•‘  All paths correctly identified! 🎯           β•‘
β•‘                                               β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

πŸ”‘ Key Observations:

  1. Reversed graph simplifies logic: πŸ”„ Following edges goes backward to ancestors

  2. Direct ancestor finding: 🎯 DFS from node directly finds its ancestors

  3. Visited prevents re-exploration: βœ… Node 0 visited once for node 3

  4. TreeSet maintains order: πŸ“Š Ancestors automatically sorted

  5. Much simpler than forward approach: ⭐ No need to track "who reaches whom"


πŸ“Š Complexity Analysis

Time Complexity: O(n Γ— (V + E)) in worst case ⚑

Where:
  n = number of nodes
  V = n (vertices)
  E = number of edges

Analysis:

Approach 1 (Forward DFS):
  For each node (n iterations):
    DFS to find reachable nodes
    DFS visits: O(V + E) per call

  Total: O(n Γ— (V + E))

But with optimization (Set.add() check):
  If ancestor already added, prune DFS
  In practice: much better than worst case!

  Worst case: complete graph
    Every node reaches every other node
    O(n Γ— (V + E)) β‰ˆ O(n Γ— n) = O(nΒ²)

Approach 2 (Reverse DFS):
  Same analysis: O(n Γ— (V + E))

Convert to lists: O(total ancestors)
  In worst case: O(nΒ²) ancestors total

Overall: O(n²) worst case ⚑
For sparse graphs: O(n Γ— E) much better! πŸš€

Space Complexity: O(nΒ²) worst case πŸ“¦

Space usage:

1. Graph (adjacency list): O(V + E) πŸ—οΈ
   n lists, total E edges

2. Ancestor sets: O(total ancestors) πŸ“Š
   Worst case: complete DAG
   Each node has O(n) ancestors
   Total: O(nΒ²)

3. DFS recursion stack: O(V) πŸ”„
   Worst case: path of length n
   O(n) depth

4. Result list: O(nΒ²) πŸ“‹
   Same as ancestor sets

Total: O(E) + O(nΒ²) + O(n) + O(nΒ²) = O(nΒ²) πŸ“¦

Worst case dominates!

Example: n = 1000
  Worst case: 1,000,000 ancestors total
  Significant but manageable! βœ…

Core Pattern: Reachability in DAG 🎯

// Template for finding all reachable nodes
class Solution {
    // Forward DFS approach
    public List<Set<Integer>> findReachable(int n, int[][] edges) {
        List<List<Integer>> graph = buildGraph(n, edges);
        List<Set<Integer>> reachable = new ArrayList<>();

        for (int i = 0; i < n; i++) {
            reachable.add(new TreeSet<>());
        }

        for (int start = 0; start < n; start++) {
            dfs(start, start, graph, reachable);
        }

        return reachable;
    }

    private void dfs(int start, int current, 
                     List<List<Integer>> graph,
                     List<Set<Integer>> reachable) {
        for (int neighbor : graph.get(current)) {
            if (reachable.get(neighbor).add(start)) {
                dfs(start, neighbor, graph, reachable);
            }
        }
    }
}

When to Use This Pattern 🎯

βœ… Find all ancestors/descendants in DAG
βœ… Transitive reachability queries
βœ… Build transitive closure
βœ… Dependency analysis
βœ… Path existence between all pairs

Recognition triggers: πŸ” - "All ancestors" or "all descendants" πŸ‘₯ - Directed Acyclic Graph (DAG) πŸ“Š - Reachability queries 🎯 - Transitive relationships πŸ”„ - Multiple paths to consider 🌊

Key insight: DFS for transitive closure! πŸ’‘

Direct Applications:

  1. Clone Graph (LC 133) - Medium πŸ”„ β€’ What's similar: Graph traversal, DFS/BFS 🌊 β€’ What's different: Clone structure, not find ancestors πŸ“‹ β€’ Key insight: DFS with visited tracking! 🎯

  2. Course Schedule (LC 207) - Medium πŸ“š β€’ What's similar: DAG, dependencies πŸ”„ β€’ What's different: Cycle detection, not ancestors βœ… β€’ Key insight: Topological sort! 🌊

  3. Evaluate Division (LC 399) - Medium βž— β€’ What's similar: Graph reachability, paths 🎯 β€’ What's different: Weighted paths, ratios πŸ“Š β€’ Key insight: DFS with path product! πŸ’‘

  4. Network Delay Time (LC 743) - Medium ⏱️ β€’ What's similar: Reachability, graph traversal 🌊 β€’ What's different: Weighted edges, shortest paths ⚑ β€’ Key insight: Dijkstra's algorithm! 🎯

Pattern Evolution: πŸ“ˆ

Level 1: Basic Graph Traversal 🌊
  β”œβ”€ Clone Graph (LC 133)
  └─ Number of Islands (LC 200)

Level 2: DAG Reachability 🎯 ← HERE
  β”œβ”€ All Ancestors (LC 2192)
  β”œβ”€ Course Schedule (LC 207, 210)
  └─ Topological Sort variations

Level 3: Advanced Reachability 🌟
  β”œβ”€ Evaluate Division (LC 399)
  β”œβ”€ Network Delay Time (LC 743)
  └─ Shortest Path algorithms

πŸ“ Quick Revision Notes

🎯 Core Concept

All Ancestors finds all nodes that can reach each node in DAG using DFS from each node 🌊. For each starting node, DFS explores all reachable descendants 🎯. Add starting node to ancestors of each reachable πŸ“. Use Set to prevent duplicates πŸ”„. Set.add() returns false if already present β†’ prune DFS! ⚑ TreeSet maintains sorted order πŸ“Š. Alternative: reverse graph, DFS finds ancestors directly πŸ”€. Time: O(nΒ²) worst case ⚑, Space: O(nΒ²) πŸ“¦!

⚑ Quick Implementation (Reverse Graph - Simplest!)

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;

public class Solution {
  public List<List<Integer>> getAncestors(int n, int[][] edges) {
    // return bfs(n, edges);
    return dfs(n, edges);
  }

  private List<List<Integer>> dfs(int n, int[][] edges) {
    List<List<Integer>> res = new ArrayList<>();

    // Step 1: create graph
    List<List<Integer>> graph = new ArrayList<>();

    for (int i = 0; i < n; i++) {
      graph.add(new ArrayList<>());
    }

    // GOTCHA: reverse edges while adding to graph which will
    // be easy when we do DFS
    for (int i = 0; i < edges.length; i++) {
      graph.get(edges[i][1]).add(edges[i][0]);
    }

    // Step 2: do DFS for every node from 0 to n - 1
    // For example, 0 -> 1 -> 2 becomes 0 <- 1 <- 2
    // graph => {{1 : 0}, {2 : 1}}
    // Also, keep track of visited, else no way of tracking visited nodes.
    for (int node = 0; node < n; node++) {
      // Ancestors of ith node
      Set<Integer> ancestors = new TreeSet<>();
      boolean[] visited = new boolean[n];

      dfs(node, graph, ancestors, visited);

      // in above example, for 0th node, it skips above
      // loop and comes directly here.
      res.add(new ArrayList<>(ancestors));
    }

    return res;
  }

  private void dfs(int node, List<List<Integer>> graph, Set<Integer> ancestors, boolean[] visited) {
    for (int parent : graph.get(node)) {
      if (visited[parent]) {
        continue;
      }

      visited[parent] = true;
      ancestors.add(parent);

      dfs(parent, graph, ancestors, visited);
    }
  }

  private List<List<Integer>> bfs(int n, int[][] edges) {
    List<List<Integer>> res = new ArrayList<>();
    // GOTCHA - extra to track ancestors. We can actually construct in res itself
    // directly. Set is to just avoid duplicates edge cases.
    List<Set<Integer>> ancestors = new ArrayList<>();

    // Step 1: create graph and indegrees
    List<List<Integer>> graph = new ArrayList<>();
    int[] indegrees = new int[n];

    for (int i = 0; i < n; i++) {
      graph.add(new ArrayList<>());
      indegrees[i] = 0;
      // GOTCHA: this should be a treeset instead of hashset
      // In question, its asked to return in the sorted order.
      ancestors.add(new TreeSet<>());
    }

    for (int i = 0; i < edges.length; i++) {
      graph.get(edges[i][0]).add(edges[i][1]); // directed graph
      indegrees[edges[i][1]]++;
    }

    // Step 2: take 0 indegree elements
    Queue<Integer> queue = new LinkedList<>();
    for (int i = 0; i < n; i++) {
      if (indegrees[i] == 0) {
        queue.offer(i);
      }
    }

    // Step 3: explore all queue (indegree) elements and add ancestors
    while (!queue.isEmpty()) {
      int curr = queue.poll();

      for (int next : graph.get(curr)) {
        // add parents of current parents
        ancestors.get(next).addAll(ancestors.get(curr));

        ancestors.get(next).add(curr); // add current parent

        indegrees[next]--;

        if (indegrees[next] == 0) {
          queue.offer(next);
        }
      }
    }

    // Step 4: Convert to desired format.
    for (Set<Integer> set : ancestors) {
      res.add(new ArrayList<>(set));
    }

    return res;
  }

  public static void main(String[] args) {
    Solution s = new Solution();
    System.out.println(s.getAncestors(8,
        new int[][] { { 0, 3 }, { 0, 4 }, { 1, 3 }, { 2, 4 }, { 2, 7 }, { 3, 5 }, { 3, 6 }, { 3, 7 }, { 4, 6 } }));
    System.out.println(s.getAncestors(5, new int[][] { { 0, 1 }, { 0, 2 }, { 0, 3
    }, { 0, 4 }, { 1, 2 }, { 1, 3 },
        { 1, 4 }, { 2, 3 }, { 2, 4 }, { 3, 4 } }));
  }
}

πŸ”‘ Key Insights

Reversed Graph Simplifies Logic: πŸ”„

The KEY insight: Reverse edges!

Original graph:
  0 β†’ 1 β†’ 2
  "0 is ancestor of 1, 1 is ancestor of 2"

Reversed graph:
  0 ← 1 ← 2
  "Follow edges backward to find ancestors"

For node 2:
  Start from 2
  Follow backward: 2 β†’ 1 β†’ 0
  Collect all reached: {0, 1} βœ…

Why this works:
  Original: "Who can I reach?" (descendants)
  Reversed: "Who can reach me?" (ancestors)

Direct ancestor finding! 🎯

BFS Topological Approach: 🌊

Alternative elegant approach:

Use topological sort (Kahn's algorithm)
Process nodes in dependency order

When processing node:
  All parents already processed βœ…
  Inherit ancestors from parents!

Example:
  0 β†’ 1 β†’ 2

  Process 0: ancestors[0] = {}

  Process 1: 
    Parent 0 processed βœ…
    ancestors[1] = ancestors[0] + {0}
                 = {} + {0} = {0}

  Process 2:
    Parent 1 processed βœ…
    ancestors[2] = ancestors[1] + {1}
                 = {0} + {1} = {0, 1}

Clean inheritance! 🌟

Code:
  for (int child : graph.get(current)) {
    // Child inherits all of current's ancestors
    ancestorSets.get(child).addAll(ancestorSets.get(current));
    // Plus current itself!
    ancestorSets.get(child).add(current);
  }

Beautiful propagation! ✨

Why TreeSet? πŸ“Š

TreeSet automatically:
  βœ… Keeps elements sorted
  βœ… Prevents duplicates
  βœ… No final sorting needed

Example:
  Add in any order: 2, 0, 1
  TreeSet: {0, 1, 2} βœ…

Compare with ArrayList:
  Add in any order: 2, 0, 1
  List: [2, 0, 1]
  Need to sort: Collections.sort() ❌

TreeSet is perfect! 🎯

Two Valid Approaches: βš–οΈ

Approach 1: Reverse Graph + DFS
  βœ… Most intuitive
  βœ… Direct ancestor finding
  βœ… Simple to understand
  Time: O(n Γ— (V + E))

Approach 2: Topological + BFS
  βœ… More elegant
  βœ… Inheritan propagation
  βœ… Each node processed once
  Time: O(V + E + total_ancestors)

Both correct! Both O(nΒ²) worst case!
Choose based on preference! 🎯

πŸŽͺ Memory Aid

"DFS from each, to all we reach" 🌊
"Add ourselves to those we teach" πŸ“
"Set returns false, stop we must" ⚑
"Ancestors found, in Sets we trust" πŸ”‘ ✨

⚠️ Don't Forget

  • Use TreeSet - sorted order automatically! πŸ“Š
  • Check Set.add() return - optimization! ⚑
  • DFS from each node - O(n) iterations! 🌊
  • Prune on false - avoid redundant exploration! πŸ”„
  • Convert Sets to Lists - final step! πŸ“‹
  • Reverse approach works too - same complexity! πŸ”€

You've mastered All Ancestors in DAG - reachability and transitive closure! 🌟🎯✨