Python Arena
Step 4 of 6

Topological sort

Some jobs must wait for others. A pizza can't go in the oven before the toppings are on, a course can't start before its prerequisites, and a program can't be built before the libraries it uses. These are directed graphs, where an edge from a to b says "a comes before b". An order of all the vertices in which every edge points forward, from earlier to later, is a topological order, and finding one is a topological sort.

It exists exactly when the graph has no directed cycle: in a, b, c, a, each of them would have to come before itself. A directed graph without cycles is a DAG, a directed acyclic graph.

Kahn's algorithm

The in-degree of a vertex, the number of edges coming in, is here the number of jobs it still waits for. A job with in-degree 0 waits for nothing and can go now. Kahn's algorithm works from that:

  1. Count every vertex's in-degree, in one pass over the edges.
  2. Put the vertices with in-degree 0 in a queue.
  3. Take a vertex from the queue and add it to the order. Its edges are now done, so lower the in-degree of the vertex at the other end of each one by 1. A vertex whose in-degree reaches 0 has nothing left to wait for, and joins the queue.
  4. When the queue is empty, the order holds every vertex, unless the graph has a cycle.
from collections import deque


def topological_order(graph):
    indegree = {v: 0 for v in graph}
    for v in graph:
        for w in graph[v]:
            indegree[w] += 1
    queue = deque([v for v in graph if indegree[v] == 0])
    order = []
    while queue:
        v = queue.popleft()
        order.append(v)
        for w in graph[v]:
            indegree[w] -= 1
            if indegree[w] == 0:
                queue.append(w)
    return order


pizza = {
    "make dough": ["roll out"],
    "make sauce": ["add toppings"],
    "grate cheese": ["add toppings"],
    "roll out": ["add toppings"],
    "add toppings": ["bake"],
    "heat oven": ["bake"],
    "bake": [],
}
print(topological_order(pizza))
Output
['make dough', 'make sauce', 'grate cheese', 'heat oven', 'roll out', 'add toppings', 'bake']

Why it's right: a vertex joins the queue only when its in-degree reaches 0, that is, once every vertex with an edge into it is already in the order. So every edge points forward. Often many orders are right (the oven could be heated first), and Kahn's algorithm gives one of them. When a task wants one particular order, such as the smallest number first whenever there's a choice, use a heap from heapq in place of the deque.

Finding a cycle

On a cycle, each vertex waits for the one before it, so none of them ever reaches in-degree 0, and neither does anything that waits for them. The queue runs empty early, and the order comes out short:

from collections import deque


def topological_order(graph):
    indegree = {v: 0 for v in graph}
    for v in graph:
        for w in graph[v]:
            indegree[w] += 1
    queue = deque([v for v in graph if indegree[v] == 0])
    order = []
    while queue:
        v = queue.popleft()
        order.append(v)
        for w in graph[v]:
            indegree[w] -= 1
            if indegree[w] == 0:
                queue.append(w)
    return order


loop = {"a": ["b"], "b": ["c"], "c": ["a", "d"], "d": [], "e": ["d"]}
order = topological_order(loop)
print(order)
if len(order) < len(loop):
    done = set(order)
    print("stuck:", [v for v in loop if v not in done])
Output
['e']
stuck: ['a', 'b', 'c', 'd']

a, b and c are a cycle. d isn't on it, but it waits for c, so it's stuck too. Only e gets done.

So never take a short order for a finished one: compare its length with the number of vertices. The vertices left out are the ones on a cycle or behind one. (Another classic way to find a cycle is a DFS that marks each vertex "in progress" until everything after it is done: an edge back to a vertex in progress closes a cycle. Kahn's algorithm finds it without any recursion.)

What it costs

Counting the in-degrees is one pass over the edges. After that, each vertex joins and leaves the queue once, and each edge lowers an in-degree once: O(V + E) time, and O(V + E) memory for the adjacency lists and the counts. With a heap instead of the deque, each vertex costs O(log V) more: O(V log V + E).

The tempting way is to look for a job that has nothing left to wait for, take it, and look again. Each look scans the vertices, once for every vertex taken: O(V²), billions of steps for 100,000 packages. Kahn's queue already holds the jobs that are ready, so nothing is ever searched for.

Your task

Challenge 1 of 3

Fill in the blanks of Kahn's algorithm: counting each vertex's in-degree, and the moment a vertex is ready to join the queue. The program should print:

[2, 3, 1, 4, 0]
[1, 3]

Challenge 2

Packages numbered 0 to n - 1 must be installed so that each one comes after the packages it needs. deps lists pairs (a, b): package a must be installed before package b. Write install_order(n, deps) that returns the order as a list, taking the smallest-numbered package whenever several are ready, or None when there's a cycle and no order exists.

Keep the ready packages in a heap with heapq, so the smallest one comes out in O(log n). The hidden tests have 100,000 packages: looking for the next ready package with a scan of all of them is O(n²).

For example, here 0, 4 and 5 are ready at the start. After 5, both 1 and 2 are, and 1 goes first. So this program:

print(install_order(6, [(5, 2), (2, 3), (4, 3), (0, 1), (5, 1)]))
print(install_order(4, [(1, 3), (3, 2), (2, 1), (0, 2)]))

prints:

[0, 4, 5, 1, 2, 3]
None

Challenge 3

A school has n courses, numbered 0 to n - 1. prereqs lists pairs (a, b): course a must be passed in an earlier term than course b. Any number of courses can be taken in the same term. Write fewest_terms(n, prereqs) that returns the fewest terms needed to pass every course, or -1 when that's impossible because of a cycle.

Run Kahn's algorithm one level at a time, like the level-by-level BFS of the module on trees: the courses that are ready at the start make up the first term, and the courses they unlock make up the next. The hidden tests have 60,000 courses and up to 60,000 terms, so don't scan all the courses again for every term.

For example, courses 0 and 1 can be taken in the first term, then 2 and 4, and 3 last. So this program:

print(fewest_terms(5, [(0, 2), (1, 2), (2, 3), (1, 4)]))
print(fewest_terms(2, [(0, 1), (1, 0)]))

prints:

3
-1