CMPUT-204 · Lesson 8 of 12 · 27 min

Depth-first search applications

Depth-first search does more than visit every vertex. The order in which it discovers and finishes vertices reveals the structure of a directed graph: whether it has cycles, how its vertices can be ordered so that every edge points forward, and how it breaks into groups of mutually reachable vertices. This lesson develops three linear-time algorithms built on DFS: cycle detection, topological sorting and strongly connected components. They power build systems, spreadsheet recalculation, package managers, deadlock detection and the analysis of web and social networks.

Classifying edges

When DFS runs on a directed graph, every edge \((u, v)\) falls into one of four classes, determined by the timestamps from lesson 7. A tree edge leads to an undiscovered vertex and becomes part of the DFS forest. A back edge leads from \(u\) to one of its ancestors, a vertex that has been discovered but not yet finished. A forward edge leads to a descendant that is already finished. A cross edge leads to a vertex that is finished and is neither an ancestor nor a descendant, in another branch or another tree of the forest.

The four edge types in a depth-first search of a directed graph
Edge typeState of v when (u, v) is examinedTimestamp relation
Tree edgeUndiscoveredd[u] < d[v] < f[v] < f[u]; v discovered from u
Back edgeDiscovered, not finished (an ancestor of u)d[v] ≤ d[u] < f[u] ≤ f[v]
Forward edgeFinished, a descendant of ud[u] < d[v] < f[v] < f[u]; not a tree edge
Cross edgeFinished, not a descendantd[v] < f[v] < d[u] < f[u]

A three-colour bookkeeping scheme makes the classification easy to implement. Each vertex is white before discovery, grey while it is on the recursion stack and black once finished. An edge to a white vertex is a tree edge, an edge to a grey vertex is a back edge, and an edge to a black vertex is a forward or cross edge, distinguished by comparing discovery times. In an undirected graph only tree and back edges occur, because every edge is explored first from whichever endpoint is discovered first.

Cycle detection

A directed graph has a cycle if and only if DFS finds a back edge. One direction is immediate: a back edge \((u, v)\) together with the tree path from \(v\) down to \(u\) forms a cycle. For the other direction, suppose the graph has a cycle and let \(v\) be the first vertex of the cycle that DFS discovers. At that moment every other vertex of the cycle is white and reachable from \(v\), so by the white-path theorem each becomes a descendant of \(v\). In particular, the vertex \(u\) just before \(v\) on the cycle is a descendant, and the edge \((u, v)\) is examined while \(v\) is still grey, so it is a back edge. Cycle detection therefore runs in \(\Theta(n + m)\).

Classifying edges with white, grey and black states
import sys
sys.setrecursionlimit(10000)
NEW, ACTIVE, DONE = 0, 1, 2

def classify(adj):
    state = {u: NEW for u in adj}
    d, time, kinds = {}, [0], []
    def visit(u):
        state[u] = ACTIVE; time[0] += 1; d[u] = time[0]
        for v in adj[u]:
            if state[v] == NEW:
                kinds.append((u, v, 'tree')); visit(v)
            elif state[v] == ACTIVE:
                kinds.append((u, v, 'back'))
            elif d[u] < d[v]:
                kinds.append((u, v, 'forward'))
            else:
                kinds.append((u, v, 'cross'))
        state[u] = DONE
    for u in adj:
        if state[u] == NEW:
            visit(u)
    return kinds

g = {'a': ['b', 'c'], 'b': ['d'], 'c': ['d'], 'd': ['a', 'e'], 'e': [], 'f': ['e', 'c']}
for u, v, k in classify(g):
    print(f'{u} -> {v}: {k}')
print('has cycle:', any(k == 'back' for _, _, k in classify(g)))
Output
a -> b: tree
b -> d: tree
d -> a: back
d -> e: tree
a -> c: tree
c -> d: cross
f -> e: cross
f -> c: cross
has cycle: True

The back edge from d to a reveals the cycle a, b, d, a. The edge from c to d is a cross edge, because d was finished in the branch through b before the search reached c. The edges from f lead into the tree that was already explored, so they are cross edges too. The code names the three states NEW, ACTIVE and DONE for white, grey and black; any three distinct values would work.

Directed acyclic graphs and topological order

A directed acyclic graph, or DAG, has no directed cycles. DAGs model dependencies: course prerequisites, tasks that must precede other tasks, modules a program imports, cells a spreadsheet formula depends on. A topological order is a listing of the vertices in which every edge \((u, v)\) has \(u\) before \(v\). CMPUT-272 lesson 9 showed that a partial order can always be extended to a total order; here we compute one in linear time. A directed graph has a topological order if and only if it is acyclic: an order cannot exist if there is a cycle, because some edge of the cycle would have to point backward.

The DFS method is short and elegant: run DFS and list the vertices in decreasing order of finish time. To see why this works, consider any edge \((u, v)\) in a DAG. When it is examined, \(v\) cannot be grey, since that would make it a back edge and the graph would have a cycle. If \(v\) is white, it becomes a descendant of \(u\) and finishes before \(u\). If \(v\) is black, it has already finished. Either way \(f[v] < f[u]\), so \(u\) appears before \(v\) when we sort by decreasing finish time. In practice we push each vertex onto a list as it finishes and reverse the list at the end, so no sorting is needed.

A DAG of nine CMPUT courses with arrows from prerequisites to later courses, and beneath it the same courses laid out left to right in a topological order with every arrow pointing to the right.
A dependency DAG and one of its topological orders

A second method, Kahn's algorithm, works from the other end. Compute each vertex's in-degree, the number of edges entering it. Put every vertex with in-degree 0 in a queue; these have no unmet prerequisites. Repeatedly remove a vertex from the queue, output it, and decrease the in-degree of each of its out-neighbours, enqueuing any whose in-degree drops to 0. If the algorithm outputs every vertex, the order is topological; if vertices remain with positive in-degree, they lie on or behind a cycle. Kahn's algorithm also runs in \(\Theta(n + m)\) and is popular in build tools because it reports exactly which items are blocked.

Two topological sorts of a prerequisite graph, and cycle reporting
from collections import deque

prereq = {                         # edge u -> v means u must come before v
    'CMPUT-174': ['CMPUT-201', 'CMPUT-272'],
    'CMPUT-201': ['CMPUT-204', 'CMPUT-229', 'CMPUT-291'],
    'CMPUT-272': ['CMPUT-204'],
    'CMPUT-204': ['CMPUT-304', 'CMPUT-366'],
    'CMPUT-229': ['CMPUT-379'],
    'CMPUT-291': [], 'CMPUT-304': [], 'CMPUT-366': [], 'CMPUT-379': [],
}

def topo_dfs(adj):
    seen, out = set(), []
    def visit(u):
        seen.add(u)
        for v in adj[u]:
            if v not in seen:
                visit(v)
        out.append(u)                  # u finishes after all its descendants
    for u in adj:
        if u not in seen:
            visit(u)
    return out[::-1]

def topo_kahn(adj):
    indeg = {u: 0 for u in adj}
    for u in adj:
        for v in adj[u]:
            indeg[v] += 1
    q = deque(u for u in adj if indeg[u] == 0)
    out = []
    while q:
        u = q.popleft(); out.append(u)
        for v in adj[u]:
            indeg[v] -= 1
            if indeg[v] == 0:
                q.append(v)
    blocked = [u for u in adj if indeg[u] > 0]
    return out, blocked

def respects(order, adj):
    pos = {u: i for i, u in enumerate(order)}
    return all(pos[u] < pos[v] for u in adj for v in adj[u])

a = topo_dfs(prereq); b, blocked = topo_kahn(prereq)
print('DFS :', ' '.join(c[-3:] for c in a), respects(a, prereq))
print('Kahn:', ' '.join(c[-3:] for c in b), respects(b, prereq), 'blocked:', blocked)
prereq['CMPUT-304'] = ['CMPUT-272']    # introduce a cycle 272 -> 204 -> 304 -> 272
print('with a cycle, blocked:', topo_kahn(prereq)[1])
Output
DFS : 174 272 201 291 229 379 204 366 304 True
Kahn: 174 201 272 229 291 204 379 304 366 True blocked: []
with a cycle, blocked: ['CMPUT-272', 'CMPUT-204', 'CMPUT-304', 'CMPUT-366']

The two methods produce different but equally valid orders, since a DAG usually has many topological orders. When an edge creates a cycle, Kahn's algorithm cannot release CMPUT-272, CMPUT-204 or CMPUT-304, nor CMPUT-366, which waits on one of them, and reports all four as blocked.

Worked problem

Ordering spreadsheet recalculation

Cell C1 = A1 + B1, D1 = C1 × 2, E1 = C1 + D1, and the user changes A1. In what order should cells be recalculated?

Show solution
  1. Build a graph with an edge from each cell to every cell whose formula uses it: A1→C1, B1→C1, C1→D1, C1→E1, D1→E1.
  2. Take the part reachable from the changed cell A1: A1, C1, D1, E1.
  3. Topologically sort it: A1, C1, D1, E1.
  4. Recalculate in that order, so each formula sees updated inputs; a cycle would mean a circular reference error.

Answer: A1, C1, D1, E1. Spreadsheets detect circular references exactly by finding a cycle in this graph.

Strongly connected components

In a directed graph, vertices \(u\) and \(v\) are strongly connected if each is reachable from the other. This relation is an equivalence relation, so it partitions the vertices into strongly connected components, or SCCs. Inside an SCC, every vertex can reach every other; between SCCs, edges go only one way. Shrinking each SCC to a single vertex gives the component graph, or condensation, which is always a DAG. SCCs identify groups of web pages that link to each other, mutually recursive functions in a program, and sets of processes that may deadlock.

Kosaraju's algorithm finds all SCCs with two depth-first searches. First, run DFS on \(G\) and record the vertices in order of finish time. Second, form the transpose graph \(G^T\), which has every edge reversed, and run DFS on it, taking start vertices in decreasing order of first-pass finish time. Each tree of the second search is exactly one SCC. The intuition is that the vertex with the latest finish time lies in a source component of the condensation; in the reversed graph, that component is a sink, so DFS from it cannot escape and collects exactly that component. Removing it and repeating peels off components one at a time. The total cost is \(\Theta(n + m)\). Tarjan's algorithm finds the same components in a single DFS using low-link values and is often faster in practice.

Kosaraju's two-pass algorithm for strongly connected components
import sys
sys.setrecursionlimit(10000)

def kosaraju(adj):
    order, seen = [], set()
    def first(u):
        seen.add(u)
        for v in adj[u]:
            if v not in seen:
                first(v)
        order.append(u)
    for u in adj:
        if u not in seen:
            first(u)
    radj = {u: [] for u in adj}               # transpose graph
    for u in adj:
        for v in adj[u]:
            radj[v].append(u)
    comp, comps = {}, []
    def second(u, label):
        comp[u] = label; comps[-1].append(u)
        for v in radj[u]:
            if v not in comp:
                second(v, label)
    for u in reversed(order):                 # decreasing finish time
        if u not in comp:
            comps.append([])
            second(u, len(comps) - 1)
    return comps

g = {1: [2], 2: [3, 4], 3: [1], 4: [5], 5: [6], 6: [4, 7], 7: [], 8: [7, 8]}
for c in kosaraju(g):
    print(sorted(c))
Output
[8]
[1, 2, 3]
[4, 5, 6]
[7]

The output lists four components in an order that is itself a topological order of the condensation. Vertex 8, with its self-loop, forms a component of its own that reaches {7}. The component {1, 2, 3} can reach {4, 5, 6}, which can reach {7}, and {7} comes last because it reaches nothing. Any directed graph is thus a DAG of strongly connected pieces, which is often the most useful way to understand it.

Linear-time DFS applications
ProblemMethodTime
Does a directed graph have a cycle?DFS; look for a back edgeΘ(n + m)
Order tasks respecting dependenciesDFS reverse finish order, or KahnΘ(n + m)
Which tasks are blocked by a cycle?Kahn's algorithmΘ(n + m)
Groups of mutually reachable verticesKosaraju or TarjanΘ(n + m)
Structure of the whole graphCondensation into a DAG of SCCsΘ(n + m)

Common mistakes

  • MistakeTreating any edge to an already visited vertex as evidence of a cycle in a directed graph.

    InsteadOnly edges to grey vertices, still on the recursion stack, are back edges; edges to finished vertices are forward or cross edges.

  • MistakeListing vertices in DFS discovery order and calling it a topological order.

    InsteadUse decreasing finish order; discovery order can put a vertex before one of its prerequisites.

  • MistakeExpecting a unique topological order.

    InsteadA DAG usually has many valid orders; any one that respects every edge is correct.

  • MistakeRunning the second pass of Kosaraju on the original graph.

    InsteadThe second DFS runs on the transpose graph, in decreasing order of first-pass finish times.

  • MistakeForgetting to restart DFS from every unvisited vertex.

    InsteadDisconnected parts would be missed; loop over all vertices.

Summary and key terms

  • DFS classifies each directed edge as a tree, back, forward or cross edge using vertex states or timestamps.
  • A directed graph has a cycle if and only if DFS finds a back edge, so cycle detection is linear.
  • A topological order exists exactly when the graph is a DAG.
  • Listing vertices in decreasing DFS finish time gives a topological order, because every edge goes from a later to an earlier finisher.
  • Kahn's algorithm repeatedly removes in-degree-0 vertices and reports vertices blocked by cycles.
  • Strongly connected components partition a directed graph; collapsing them gives a DAG.
  • Kosaraju's algorithm finds all SCCs with one DFS on G and one on its transpose, in Θ(n + m).
Tree edge
An edge DFS follows to discover a new vertex.
Back edge
An edge from a vertex to one of its DFS ancestors; its presence means a cycle.
Forward edge
A non-tree edge from a vertex to one of its DFS descendants.
Cross edge
An edge to a finished vertex that is neither ancestor nor descendant.
DAG
A directed acyclic graph, a directed graph with no directed cycles.
Topological order
An ordering of vertices in which every edge points from earlier to later.
In-degree
The number of edges entering a vertex.
Strongly connected component
A maximal set of vertices in which every vertex can reach every other.
Transpose graph
The graph obtained by reversing every edge.
Condensation
The DAG formed by collapsing each strongly connected component into one vertex.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. DFS examines edge (u, v) while v is grey. What type of edge is it, and what does it imply?

    Show answer

    A back edge; the graph has a cycle.

  2. Can an undirected graph have cross edges in DFS?

    Show answer

    No; undirected DFS produces only tree and back edges.

  3. Give a topological order of: a→b, a→c, b→d, c→d.

    Show answer

    a, b, c, d (a, c, b, d also works).

  4. Why is discovery order not always topological?

    Show answer

    In a→c, b→a, DFS starting at a discovers a before b, but b must precede a.

  5. Kahn's algorithm stops with 3 vertices still having positive in-degree. What can you conclude?

    Show answer

    The graph has a cycle; those vertices are on a cycle or depend on one.

  6. How many SCCs does a DAG with n vertices have?

    Show answer

    n; every vertex is its own component.

  7. A directed graph is a single cycle through all n vertices. How many SCCs?

    Show answer

    One.

  8. What is the running time of Kosaraju's algorithm?

    Show answer

    Θ(n + m): two DFS passes and building the transpose.

  9. Name a real system that uses topological sorting.

    Show answer

    Build tools such as make, package managers, spreadsheets, course planners or task schedulers.

  10. Why is the condensation of any directed graph acyclic?

    Show answer

    A cycle among components would make their vertices mutually reachable, merging them into one component.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz