Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 60

Topological Sort on a DAG

Recommended resources

Hello! Welcome to the final lesson in our module on Graph Traversal Algorithms.

In our previous lessons, we've used traversals like DFS and BFS to explore graph structures, detect cycles, and find connected components. You'll recall that for finding connected components, we used a "master loop" to ensure we visited every node, even in a disconnected graph. Today, we'll build on these ideas to tackle a fundamental problem for a specific type of graph.

Your learning goal for this lesson is to perform a topological sort on a Directed Acyclic Graph (DAG). This is a powerful algorithm with many real-world applications, from scheduling tasks with dependencies to resolving dependencies in software build systems.

1. What is Topological Sorting?

Imagine you have a list of university courses to take, but some have prerequisites. For example, you must take "Intro to Programming" before "Data Structures." A topological sort would give you a valid sequence in which you can take all the courses.

Formally, a topological sort is a linear ordering of vertices in a Directed Acyclic Graph (DAG) such that for every directed edge from vertex u to vertex v, vertex u comes before vertex v in the ordering.

Topological Sort Definition and Example
This image defines topological sort and shows that a single graph can have multiple valid topological orderings.

The two key requirements are:

  1. Directed Graph: The relationships have a one-way dependency (e.g., A is required for B, not the other way around).
  2. Acyclic Graph (No Cycles): There are no circular dependencies. If "Course A" requires "Course B", and "Course B" requires "Course A", it's impossible to create a valid schedule.

A topological sort visually "unravels" the graph into a straight line, where all arrows point from left to right.

Unsorted vs. Topologically Sorted Graph
This image shows a DAG on the left and one of its valid topological orderings on the right. Notice how all edges in the sorted version point from an earlier node to a later one.

To understand these prerequisites more deeply, let's watch a short, clear explanation.

G-22. Kahn's Algorithm | Topological Sort Algorithm | BFS

The video 'Kahn's Algorithm | Topological Sort Algorithm | BFS' from the take U forward channel provides an excellent introduction to what topological sorting is and why it's only valid for Directed Acyclic Graphs (DAGs).

Watch from the beginning until 03:58. Focus on the definition of a linear ordering and the explanations for why topological sort cannot be applied to undirected graphs or graphs with cycles.

There are two primary algorithms to perform a topological sort. We will cover both:

  1. Kahn's Algorithm, which uses Breadth-First Search (BFS).
  2. A modified Depth-First Search (DFS) approach.

2. Kahn's Algorithm (BFS-based Approach)

Kahn's algorithm is intuitive because it mimics how you might naturally schedule tasks. You start with the tasks that have no prerequisites. Once you complete them, you can then move on to the tasks that only depended on the ones you just finished.

The key concept here is in-degree: the number of incoming edges a node has.

  • A node with an in-degree of 0 has no prerequisites and can be performed first.

The algorithm works as follows:

  1. Compute In-degrees: Calculate the in-degree for every vertex in the graph.
  2. Initialize Queue: Add all vertices with an in-degree of 0 to a queue. These are our starting points.
  3. Process Nodes: While the queue is not empty:
    a. Dequeue a vertex u. Add it to your result list (this is the next task in our sorted order).
    b. For each neighbor v of u, decrement its in-degree by 1 (since we've just "completed" u).
    c. If a neighbor v's in-degree becomes 0, it means all its prerequisites are met. Add v to the queue.
  4. Check for Cycles: After the loop, if the number of vertices in your result list is less than the total number of vertices in the graph, it means there was a cycle. A cycle prevents the in-degrees of some nodes from ever reaching 0.

Let's see this process in action.

G-22. Kahn's Algorithm | Topological Sort Algorithm | BFS

The same video from take U forward provides a fantastic step-by-step walkthrough of Kahn's algorithm, showing how the in-degrees and the queue change over time.

Watch from 03:58 to 10:31. Pay close attention to how the in-degree array is calculated and updated, and how nodes are added to the queue only when their in-degree drops to zero.

Python Implementation of Kahn's Algorithm

Now, let's look at a clean Python implementation. This is a common pattern you'll use in competitive programming.

Topological Sort (With Visualization and Code Examples)

The article 'Topological Sort (With Visualization and Code Examples)' from FinalRoundAI provides a great Python implementation for Kahn's Algorithm. We'll focus on this code.

In the article, find the section titled 'Brute Force Approach' (don't worry about the name, it's referring to the standard Kahn's algorithm). Study the 'Code (Python)' provided there. Notice the use of defaultdict for the adjacency list and deque for the queue, tools we've covered previously.

Here is the code from the article, with added comments to highlight the steps:

from collections import deque, defaultdict

def topological_sort_kahns(vertices, edges):
    # Step 1: Build adjacency list and calculate in-degrees
    graph = defaultdict(list)
    in_degree = {i: 0 for i in range(vertices)}
    
    for src, dest in edges:
        graph[src].append(dest)
        in_degree[dest] += 1
    
    # Step 2: Find all vertices with no incoming edges (in-degree == 0)
    queue = deque()
    for vertex in range(vertices):
        if in_degree[vertex] == 0:
            queue.append(vertex)
    
    result = []
    
    # Step 3: Process vertices from the queue
    while queue:
        current = queue.popleft()
        result.append(current)
        
        # For each neighbor, reduce its in-degree
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            # If in-degree becomes 0, add to queue
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
    
    # Step 4: Check if topological sorting is possible (no cycle)
    if len(result) != vertices:
        return []  # Graph has a cycle
    
    return result

# Example usage
vertices = 6
edges = [(5, 2), (5, 0), (4, 0), (4, 1), (2, 3), (3, 1)]
print("Topological Sort (Kahn's):", topological_sort_kahns(vertices, edges))
# Possible output: [4, 5, 0, 2, 3, 1] or [5, 4, 2, 0, 3, 1] etc.
Test your understanding!

Consider a graph with 4 vertices (0, 1, 2, 3) and edges [(0, 1), (0, 2), (1, 3), (2, 3)]. Trace Kahn's algorithm.

  1. What are the initial in-degrees of each node?
  2. Which node(s) get added to the queue initially?
  3. What is one possible final topological sort?
Show answer
  1. Initial in-degrees:
    • Node 0: 0
    • Node 1: 1 (from 0)
    • Node 2: 1 (from 0)
    • Node 3: 2 (from 1 and 2)
  2. Initial queue: Only node 0 has an in-degree of 0, so the queue starts with [0].
  3. Trace & Final Sort:
    • pop(0). result = [0]. Neighbors of 0 are 1 and 2.
    • in_degree[1] becomes 0. Add 1 to queue. queue = [1].
    • in_degree[2] becomes 0. Add 2 to queue. queue = [1, 2].
    • pop(1). result = [0, 1]. Neighbor of 1 is 3.
    • in_degree[3] becomes 1. Not added to queue yet. queue = [2].
    • pop(2). result = [0, 1, 2]. Neighbor of 2 is 3.
    • in_degree[3] becomes 0. Add 3 to queue. queue = [3].
    • pop(3). result = [0, 1, 2, 3]. No neighbors. Queue is empty.
    • A possible final sort is [0, 1, 2, 3].

3. DFS-based Approach

An alternative method uses Depth-First Search. This approach can feel a bit less direct, but it's very powerful and a natural extension of your DFS skills.

The core idea is based on post-order traversal. A node is only considered "finished" after all of its descendants in the DFS tree have been visited. In a topological sort, a node must appear before its descendants. This means if we add a node to our result list as soon as it's finished, we are building the topological sort in reverse order.

The algorithm works as follows:

  1. Initialize a visited set and a list/stack to store the result.
  2. Iterate through all vertices. If a vertex hasn't been visited, start a DFS from it.
  3. In the dfs(vertex) function:
    a. Mark the vertex as visited.
    b. For each neighbor of the vertex, if it hasn't been visited, recursively call dfs on it.
    c. After the loop over neighbors finishes (this is the post-order step), add the current vertex to the front of the result list (or append to a list and reverse it at the end).
  4. Cycle Detection: To detect a cycle, we need an extra recursion_stack set. Before visiting a node's neighbors, add it to the recursion_stack. After the visit, remove it. If you ever try to visit a node that is already in the recursion_stack, you've found a back edge, which indicates a cycle.

This DFS approach is often used in more complex problems. Let's watch an explanation of this logic in the context of a challenging competitive programming problem.

Alien Dictionary - Topological Sort - Leetcode 269 - Python

The video 'Alien Dictionary - Topological Sort' by NeetCode explains the post-order DFS logic very clearly. While the problem itself is hard, the core explanation of the algorithm is excellent.

Watch from 12:15 to 15:16. The first part (12:15-14:03) explains why a post-order DFS builds the result in reverse. The second part (14:03-15:16) explains how to use an extra set to detect cycles during the traversal.

Python Implementation of DFS-based Topological Sort

Now, let's examine the Python code for this approach.

Topological Sort (With Visualization and Code Examples)

The same FinalRoundAI article also provides an excellent implementation for the DFS-based topological sort.

Find the section 'Optimized Approach' in the article and study the Python code provided there. Pay close attention to the three states tracked for each node: not visited, in recursion stack (rec_stack), and fully visited.

Here's the code, again with comments to guide you:

def topological_sort_dfs(vertices, edges):
    # Step 1: Build the adjacency list
    graph = {i: [] for i in range(vertices)}
    for src, dest in edges:
        graph[src].append(dest)
    
    visited = [False] * vertices
    rec_stack = [False] * vertices  # For cycle detection
    result_stack = []

    def dfs(vertex):
        # Mark current vertex as visited and add to recursion stack
        visited[vertex] = True
        rec_stack[vertex] = True
        
        # Visit all neighbors
        for neighbor in graph[vertex]:
            # If neighbor is in recursion stack, we have a cycle
            if rec_stack[neighbor]:
                return False  # Cycle detected
            # If neighbor is not visited, recursively visit it
            if not visited[neighbor]:
                if not dfs(neighbor):
                    return False # Propagate cycle detection
        
        # After visiting all descendants, remove from recursion stack
        rec_stack[vertex] = False
        # Post-order step: add vertex to the result
        result_stack.append(vertex)
        return True
    
    # Perform DFS for all unvisited vertices (handles disconnected graphs)
    for vertex in range(vertices):
        if not visited[vertex]:
            if not dfs(vertex):
                return []  # Cycle detected, no topological sort possible
    
    # The stack was built in post-order, so reverse it for the final result
    return result_stack[::-1]

# Example usage
vertices = 6
edges = [(5, 2), (5, 0), (4, 0), (4, 1), (2, 3), (3, 1)]
print("Topological Sort (DFS):", topological_sort_dfs(vertices, edges))
# Possible output: [5, 4, 2, 3, 1, 0] or [4, 5, 2, 3, 1, 0] etc.

Conclusion

You have now mastered topological sorting, a key algorithm for directed graphs. You've learned not just what it is, but two distinct and equally valid ways to implement it.

Key Takeaways:

  • A topological sort is a linear ordering of vertices in a DAG where for every edge u -> v, u appears before v.
  • It's essential for problems involving dependencies, such as task scheduling or build systems.
  • Kahn's Algorithm uses a BFS-like approach, processing nodes with an in-degree of 0.
  • The DFS-based algorithm uses a post-order traversal to build the sorted list in reverse.
  • Both algorithms have a time complexity of and can be adapted to detect cycles. If a cycle is present, a topological sort is not possible.

Up Next:
This lesson concludes our module on graph traversals. You've built a strong foundation. In our next module, "Shortest Paths and Minimum Spanning Trees," we will start by exploring Dijkstra's algorithm. This will show you how to find the shortest path from a single source to all other nodes in a weighted graph, and it will be a great opportunity to apply your knowledge of heaps and priority queues