Chapter 8 of 10

Graph Traversals

Concept: What is Graph Traversal

Graph traversal means visiting every vertex of a graph in a systematic way, making sure no vertex is missed and no vertex is visited twice. Unlike arrays or linked lists where there is only one natural order to visit elements, a graph can be traversed in many different ways depending on the strategy used. The two most fundamental and important traversal strategies are Breadth-First Search (BFS) and Depth-First Search (DFS).

Example 269: Why Traversal Matters

Traversal is the foundation for solving many graph problems: finding shortest paths, detecting cycles, topological sorting, finding connected components, and many more. Mastering BFS and DFS step-by-step tracing is the single highest-yield graph topic in competitive exams.

Breadth-First Search (BFS)

Introduction to BFS

Concept: What is BFS

Breadth-First Search (BFS) explores a graph level by level, outward from a chosen source vertex. It visits all neighbours of the current vertex before moving any deeper into the graph. BFS uses a FIFO queue to control the order of traversal, which is exactly what guarantees the level-by-level behaviour.

Example 270: Where BFS is Used
  • Finding the shortest path (fewest edges) in an unweighted graph.

  • Finding all connected components of a graph.

  • Level-order traversal of a tree (BFS applied to a tree is exactly level-order traversal).

BFS Algorithm

Concept: Colors and Labels Used in BFS

Each vertex is given a color to track its traversal state:

  • WHITE: not yet discovered.

  • GRAY: discovered, but still has unexplored neighbours (currently in the queue).

  • BLACK: fully explored, all its neighbours have been looked at.

Along with color, we track $d[v]$ (distance from source $s$) and $\pi[v]$ (the parent of $v$ in the BFS tree).

Algorithm: BFS(G, s)

diagram
Concept: Time Complexity

With an adjacency list, each vertex is enqueued and dequeued exactly once ($O(V)$ total), and each edge is examined at most twice across the whole run ($O(E)$ total). So BFS runs in $O(V + E)$ time. With an adjacency matrix, finding the neighbours of a vertex takes $O(V)$ each time, giving $O(V^2)$ total.

BFS Trace: Example Graph

Example 271: Worked Trace: BFS on an Undirected Graph

Consider the undirected graph with vertices $\{A, B, C, D, E\}$ and edges $\{(A,B), (A,C), (B,C), (C,D), (C,E)\}$. Adjacency lists (alphabetical order): $A:[B,C]$, $B:[A,C]$, $C:[A,B,D,E]$, $D:[C]$, $E:[C]$. BFS starts from source $A$.

diagram
diagram
Example 272: Filled BFS Table (Final State)
diagram

BFS order (dequeue order): $A, B, C, D, E$

BFS on Directed Graphs

Concept: BFS on Directed Graphs

The core logic and the $O(V+E)$ complexity are identical for directed graphs. BFS simply follows the outbound edges listed in $Adj[u]$; it never looks at incoming edges.

Example 273: Worked Trace: BFS on the Directed Version of the Same Graph

Take the same 5 vertices, but now the edges are directed: $A\to B$, $A\to C$, $B\to C$, $C\to D$, $C\to E$. BFS from source $A$.

diagram
Example 274: Filled BFS Table for the Directed Graph

Since $B$'s only out-neighbour $C$ is already discovered (via $A$) by the time $B$ is processed, the resulting distances and parents come out exactly the same as the undirected case here.

diagram

BFS on Disconnected Graph — Connected Components

Concept: Detecting Connected Components with BFS

If the graph is disconnected, a single BFS from one source will not reach all vertices. To find all connected components, we run BFS from every unvisited vertex. Each BFS run that starts fresh (from an unvisited vertex) discovers one complete connected component.

The number of times we start BFS from an unvisited source = the number of connected components.

diagram

Properties of BFS

Property 1: Multiple BFS Traversals

Multiple valid BFS orderings can exist for the same graph, depending on:

  • which vertex is chosen as the source,

  • the order in which neighbours appear in the adjacency list.

Changing either of these can give a different but equally valid BFS sequence.

Property 2: Distance as Level Number

$d[u]$ equals the exact level of $u$ in the BFS tree. In our example graph: $d[A] = 0$ (Level 0), $d[B] = d[C] = 1$ (Level 1), $d[D] = d[E] = 2$ (Level 2).

Property 3: Dequeued Order Defines the BFS Order

The sequence in which vertices are removed from the queue (DEQUEUE calls) is the BFS traversal order. For our example: \[ A, B, C, D, E \]

Property 4: Shortest Path Length in Unweighted Graphs

In an unweighted graph, $d[u]$ gives the minimum number of edges on any path from $s$ to $u$. In our example, $d[D] = 2$ means the shortest path from $A$ to $D$ uses exactly 2 edges: $A \to C \to D$.

Property 5: Level-Order Traversal on Trees

BFS applied to a tree is exactly the same thing as level-order traversal. No cycle detection or visited array is even needed, because a tree has no cycles, so a vertex can never be reached a second time.

diagram
Property 6: Finding Connected Components

Run BFS from any unvisited (WHITE) vertex. Each full BFS call discovers exactly one entire connected component. Repeating this until every vertex has been visited, and counting how many times BFS was started, gives the total number of connected components.

diagram
Example 275: Applying Property 6

2 calls to BFS are needed to cover every vertex here, so the graph has 2 connected components: $\{A, B\}$ and $\{C, D, E\}$.

Property 7: Tree Edges

A tree edge is an edge $(u,v)$ that first discovers a new WHITE vertex $v$. The collection of all tree edges forms the BFS spanning tree.

diagram
Example 276: Applying Property 7

In this graph, the solid green edges $(A,B)$, $(A,C)$, $(C,D)$ are tree edges, since each one first discovers a new vertex. The dashed edge $(B,C)$ connects two vertices that are both already discovered, so it is not a tree edge.

Property 8: Cross Edges

A cross edge is a non-tree edge connecting two vertices that are both already discovered. In an undirected BFS tree, a cross edge always satisfies: \[ |d[u] - d[v]| \le 1 \]

Example 277: Applying Property 8

Edge $(B, C)$ from the previous diagram is a cross edge. Here $d[B] = 1$ and $d[C] = 1$, so $|d[B] - d[C]| = 0 \le 1$, which confirms it is a valid cross edge.

Property 9: Back Edges

A back edge is a directed edge from a descendant back to an ancestor in the BFS tree. Back edges can only occur in directed graphs that contain a cycle; they never occur in an undirected BFS tree.

diagram
Example 278: Applying Property 9

Edge $(C, A)$ (red, directed back to the root) is a back edge. This situation cannot happen in an undirected BFS tree, because if $A \to C$ exists, then $(A,C)$ already becomes a tree edge the moment $C$ is first discovered, which would make $C \to A$ redundant rather than a separate back edge.

Property 10: Forward Edges Never Exist in BFS

A forward edge would be a hypothetical edge from an ancestor to a non-child descendant further down the tree. Such edges never appear in any BFS tree, directed or undirected.

diagram
Example 279: Why Forward Edges Never Occur in BFS

BFS always discovers the shallowest reachable vertices first. If an edge $A \to D$ really existed, then $D$ would have been discovered directly from $A$ at Level 1, not through $B$ at Level 2. So this hypothetical dashed edge (a "forward edge") is a contradiction, and it simply cannot occur in a BFS tree.

Property 11: Maximum Queue Size

The maximum size the queue ever reaches during BFS equals the widest level of the BFS tree: \[ \text{Max Queue Size} = \max_{k} \left| \{v \in V : d[v] = k\} \right| \]

diagram
Example 280: Applying Property 11

Level 1 contains 3 vertices ($B$, $C$, $D$), which is the widest level in this graph (Level 0 has only $A$, Level 2 has only $E$). So the maximum queue size reached during this BFS run is $\boxed{3}$.

Property 12: Queue Level Monotonicity Invariant

At any point during BFS, if the queue contains $Q = \langle v_1, v_2, \ldots, v_r \rangle$ (front to back), then: \[ d[v_1] \le d[v_2] \le \cdots \le d[v_r], \qquad d[v_r] - d[v_1] \le 1 \] In other words, the queue is always sorted by distance, and it never spans more than two consecutive levels at once. You can never have two vertices whose distances differ by 2 or more sitting in the queue at the same time.

Property 13: Odd Cycle Detection via BFS

If, while running BFS, we find an edge $(u,v)$ such that \[ d[u] \equiv d[v] \pmod{2} \] (both distances have the same parity), then the graph contains an odd-length cycle.

diagram
Example 281: Applying Property 13

Here $d[C] = 1$ and $d[D] = 1$ (discovered via $B$). The edge $(C,D)$, shown in red, connects two vertices at the same distance (both odd). By the rule, this forces an odd cycle to exist: following $A \to B \to D \to C \to A$ traces out a cycle of length 4 through the tree plus this cross edge; more precisely, the presence of an edge joining two same-parity levels is what certifies an odd cycle exists somewhere in the graph.

Property 14: Tree Diameter via 2-Pass BFS

To find the diameter of a tree (the length of the longest path between any two vertices), we do not need to try all pairs. A clever shortcut works:

  1. Run BFS from any arbitrary vertex $x$. Find the vertex $u$ that is farthest from $x$.

  2. Run BFS again, this time from $u$. Find the vertex $v$ that is farthest from $u$.

The distance $d[v]$ found in the second BFS pass is exactly the diameter of the tree.

diagram
Property 15: Cycle Detection via Parent Check

While BFS explores neighbour $v$ of the current vertex $u$: if $v$ is already visited AND $v \ne \pi[u]$ (i.e., $v$ is not the parent of $u$), then a cycle has been detected.

diagram
Example 282: Applying Property 15

BFS from $A$: visit $B$, set $\pi[B] = A$. Visit $C$, set $\pi[C] = A$. When $B$ later checks its neighbour $C$ (the red edge), $C$ is already visited, and $C \ne \pi[B] = A$. This confirms a cycle exists: $A$–$B$–$C$–$A$.

bfs-full-implementation.py
from collections import deque

WHITE, GRAY, BLACK = 0, 1, 2

def bfs(graph, s, vertices):
    color = {v: WHITE for v in vertices}
    d     = {v: float('inf') for v in vertices}
    pi    = {v: None for v in vertices}

    color[s] = GRAY
    d[s]     = 0
    order    = []

    Q = deque([s])
    while Q:
        u = Q.popleft()
        order.append(u)
        for v in graph[u]:
            if color[v] == WHITE:
                color[v] = GRAY
                d[v]     = d[u] + 1
                pi[v]    = u
                Q.append(v)
        color[u] = BLACK

    return order, d, pi


def count_components(graph, vertices):
    visited = {v: False for v in vertices}
    count   = 0
    for v in vertices:
        if not visited[v]:
            count += 1
            Q = deque([v])
            visited[v] = True
            while Q:
                u = Q.popleft()
                for w in graph[u]:
                    if not visited[w]:
                        visited[w] = True
                        Q.append(w)
    return count


def tree_diameter(graph, vertices):
    """Two-pass BFS to find the diameter of a tree."""
    _, d1, _ = bfs(graph, vertices[0], vertices)
    u = max(d1, key=d1.get)          # farthest vertex from arbitrary start
    _, d2, _ = bfs(graph, u, vertices)
    v = max(d2, key=d2.get)          # farthest vertex from u
    return d2[v]                      # diameter


# --- Driver Code ---
vertices = ['A', 'B', 'C', 'D', 'E']
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'C'],
    'C': ['A', 'B', 'D', 'E'],
    'D': ['C'],
    'E': ['C'],
}

order, d, pi = bfs(graph, 'A', vertices)
print("BFS order:", order)   # ['A', 'B', 'C', 'D', 'E']
print("Distances:", d)       # {'A':0,'B':1,'C':1,'D':2,'E':2}
print("Parents:", pi)        # {'A':None,'B':'A','C':'A','D':'C','E':'C'}

Depth-First Search (DFS)

Concept: DFS — The Idea

DFS explores a graph by going as deep as possible along each path before backtracking. Starting from a source vertex, it visits one neighbour, then one of that neighbour's neighbours, and keeps going deeper until it reaches a vertex with no unvisited neighbours. Then it backtracks and tries another direction.

DFS uses a stack (or recursion, which uses the call stack implicitly). Think of exploring a maze by always taking the first available turn, going as far as you can, then backing up when you hit a dead end.

Example 283: Analogy: Exploring a Cave System

Imagine exploring a cave with many tunnels branching off. You always take the first unexplored tunnel and go as deep as possible. When you hit a dead end, you back up to the last junction and take the next unexplored tunnel. DFS works exactly this way.

DFS Discovery and Finish Timestamps

Concept: Discovery and Finish Times

In DFS, we maintain a global counter (clock). Two timestamps are recorded for each vertex $v$:

  • Discovery time $d[v]$: the clock value when $v$ is first visited (entered).

  • Finish time $f[v]$: the clock value when DFS has finished exploring all of $v$'s descendants (exited).

For any vertex $v$, $d[v] < f[v]$. If $u$ is an ancestor of $v$ in the DFS tree, then $d[u] < d[v] < f[v] < f[u]$, meaning the interval $[d[v], f[v]]$ is completely inside $[d[u], f[u]]$.

This parenthesis theorem is very important for classifying edges.

DFS Step-by-Step Trace with Timestamps

Example 284: DFS Trace on Directed Graph

Graph with 6 vertices, edges: $0\to1, 0\to2, 1\to3, 1\to4, 2\to5, 3\to2$. Start from vertex 0. Neighbours visited in increasing order.

diagram
diagram

DFS Edge Classification

Concept: Four Types of Edges in DFS

During DFS on a directed graph, every edge $(u, v)$ falls into exactly one of four categories:

  • Tree Edge: $(u, v)$ is a tree edge if $v$ was discovered for the first time through $u$. It is the edge used to discover a new vertex. It appears in the DFS tree.

  • Back Edge: $(u, v)$ is a back edge if $v$ is an ancestor of $u$ in the DFS tree (i.e., $v$ is still on the recursion stack when we see this edge). Back edges indicate a cycle in a directed graph.

  • Forward Edge: $(u, v)$ is a forward edge if $v$ is a descendant of $u$ in the DFS tree, but $v$ was not discovered directly through $u$ (it was discovered through some other path). Identified by $d[u] < d[v]$ and $v$ already finished.

  • Cross Edge: $(u, v)$ is a cross edge if $v$ is in a different DFS subtree (not an ancestor or descendant of $u$). Identified by $d[v] < d[u]$ and $v$ already finished.

Example 285: Quick Identification Rule

When DFS is exploring vertex $u$ and encounters edge $(u, v)$:

  • If $v$ is unvisited: Tree Edge.

  • If $v$ is currently on the DFS stack (visited but not finished): Back Edge.

  • If $v$ is already finished and $d[u] < d[v]$: Forward Edge.

  • If $v$ is already finished and $d[u] > d[v]$: Cross Edge.

diagram

Algorithm: DFS(G)

diagram

Algorithm: DFS-Visit(u)

diagram
dfs.py
def dfs_full(graph, n):
    visited  = [False] * n
    on_stack = [False] * n   # currently in recursion stack
    d        = [0] * n       # discovery time
    f        = [0] * n       # finish time
    parent   = [-1] * n
    clock    = [0]           # use list so nested function can modify it
    order    = []
    has_cycle = [False]

    def dfs_visit(u):
        visited[u]  = True
        on_stack[u] = True
        clock[0]   += 1
        d[u]        = clock[0]
        order.append(u)

        for v in graph[u]:
            if not visited[v]:
                parent[v] = u
                dfs_visit(v)
            elif on_stack[v]:
                has_cycle[0] = True   # back edge = cycle

        clock[0]   += 1
        f[u]        = clock[0]
        on_stack[u] = False

    for i in range(n):
        if not visited[i]:
            dfs_visit(i)

    return order, d, f, parent, has_cycle[0]


# --- Driver Code ---
# Directed graph: 0->1, 0->2, 1->3, 1->4, 2->5, 3->2
n     = 6
graph = {0:[1,2], 1:[3,4], 2:[5], 3:[2], 4:[], 5:[]}

order, d, f, parent, cycle = dfs_full(graph, n)
print("DFS order:", order)
print("Discovery:", d)
print("Finish:   ", f)
print("Has cycle:", cycle)

# Output:
# DFS order: [0, 1, 3, 2, 5, 4]
# Discovery: [1, 2, 4, 3, 9, 5]
# Finish:    [12, 11, 8, 8, 10, 6]
# Has cycle: False

Topological Sort

Concept: What is Topological Sort

A topological sort (or topological ordering) of a DAG (Directed Acyclic Graph) is a linear ordering of all its vertices such that for every directed edge $(u, v)$, vertex $u$ appears before vertex $v$ in the ordering.

Think of scheduling tasks where some tasks must be completed before others. For example, you must wear socks before shoes, wear shirt before jacket. A topological sort gives a valid sequence of completing all tasks.

Topological sort is only defined for DAGs. If the graph has a cycle, no topological ordering exists.

Example 286: Course Prerequisites Example

Suppose courses have prerequisites: Math before DS, Math before Algo, DS before OS, Algo before Compiler. A topological sort gives a valid order to take these courses without violating any prerequisite.

diagram

Method 1: DFS-Based Topological Sort

Concept: DFS Topological Sort

When DFS finishes a vertex $u$ (assigns finish time), we push $u$ onto a stack. After DFS completes for all vertices, we pop the stack to get the topological order. The vertex with the largest finish time comes first in the topological order.

Why it works: If there is an edge $(u, v)$, then DFS on $u$ must complete all of $v$'s work before $u$ finishes. So $f[v] < f[u]$, meaning $u$ finishes after $v$, so $u$ appears before $v$ when we output in decreasing finish time order.

Example 287: DFS Topological Sort Trace

Graph: $0 \to 1, 0 \to 2, 1 \to 3, 2 \to 3$. This is a DAG.

DFS from 0: discover 0, then 1, then 3. Finish 3 (push 3). Back to 1, finish 1 (push 1). Back to 0, go to 2, discover 2. Finish 2 (wait, 3 already finished). Finish 2 (push 2). Finish 0 (push 0).

Stack (top to bottom): 0, 2, 1, 3. Pop gives: 0, 2, 1, 3.

Verify: edge $0\to1$: 0 before 1 $\checkmark$, edge $0\to2$: 0 before 2 $\checkmark$, edge $1\to3$: 1 before 3 $\checkmark$, edge $2\to3$: 2 before 3 $\checkmark$.

Algorithm: Topological-Sort-DFS(G)

diagram

Algorithm: Topo-DFS-Visit(u, S)

diagram

Method 2: Kahn's Algorithm (BFS with In-Degree)

Concept: Kahn's Algorithm

Kahn's algorithm is an iterative BFS-based method. The key insight is: in a topological ordering, the first vertex must have in-degree 0 (no prerequisites). After removing it, the next vertex with in-degree 0 can come next, and so on.

Steps:

  1. Compute the in-degree of every vertex.

  2. Add all vertices with in-degree 0 to a queue.

  3. While the queue is not empty:

    1. Dequeue vertex $u$, add it to the topological order.

    2. For each neighbour $v$ of $u$: decrease $v$'s in-degree by 1. If $v$'s in-degree becomes 0, enqueue $v$.

  4. If the topological order contains fewer than $|V|$ vertices, the graph has a cycle.

This also detects cycles: if we cannot empty the queue before visiting all vertices, a cycle exists.

diagram

Algorithm: Kahn-Topological-Sort(G)

diagram
Example 288: Multiple Valid Topological Orderings

A DAG may have many valid topological orderings. The order in which we add zero-in-degree vertices to the queue (and which queue we use) determines the specific ordering produced. For the graph $0\to2, 0\to1, 1\to3, 2\to3$:

  • If we process 1 before 2 at step 2: order is 0, 1, 2, 3.

  • If we process 2 before 1 at step 2: order is 0, 2, 1, 3.

Both are valid. The DAG does not have a unique topological ordering unless it is a Hamiltonian path (each vertex has at most one zero-in-degree vertex at any time during Kahn's).

topological-sort.py
from collections import deque

# ============================================================
# Method 1: DFS-Based Topological Sort
# ============================================================
def topo_sort_dfs(graph, n):
    visited = [False] * n
    stack   = []

    def dfs_visit(u):
        visited[u] = True
        for v in graph[u]:
            if not visited[v]:
                dfs_visit(v)
        stack.append(u)   # push AFTER all descendants

    for i in range(n):
        if not visited[i]:
            dfs_visit(i)

    return stack[::-1]   # reverse = topological order


# ============================================================
# Method 2: Kahn's Algorithm (BFS with in-degree)
# ============================================================
def kahn_topo_sort(graph, n):
    indegree = [0] * n
    for u in range(n):
        for v in graph[u]:
            indegree[v] += 1

    queue = deque()
    for v in range(n):
        if indegree[v] == 0:
            queue.append(v)

    result = []
    while queue:
        u = queue.popleft()
        result.append(u)
        for v in graph[u]:
            indegree[v] -= 1
            if indegree[v] == 0:
                queue.append(v)

    if len(result) != n:
        print("Cycle detected! No topological sort possible.")
        return []
    return result


# --- Driver Code ---
# DAG: 0->2, 0->1, 1->3, 2->3, 3->4
n     = 5
graph = {0:[1,2], 1:[3], 2:[3], 3:[4], 4:[]}

print("DFS Topo Sort:", topo_sort_dfs(graph, n))
# Output: [0, 2, 1, 3, 4] or [0, 1, 2, 3, 4]

print("Kahn's Topo Sort:", kahn_topo_sort(graph, n))
# Output: [0, 1, 2, 3, 4]

# Test with cycle
graph_cycle = {0:[1], 1:[2], 2:[0]}
print("Cycle graph:", kahn_topo_sort(graph_cycle, 3))
# Output: Cycle detected! []
diagram
Example 289: GATE Exam Tips
  • BFS/DFS tracing: Always maintain a queue/stack carefully. Follow the adjacency list order strictly. Commonly asked: what is the BFS/DFS order, what are the timestamps, what type is each edge.

  • Edge classification: On a directed graph DFS, a back edge means a cycle. On an undirected graph DFS, there are no forward or cross edges, only tree and back edges.

  • Topological sort: Only on DAGs. Multiple valid orderings may exist. Kahn's algorithm also detects cycles. DFS-based gives reverse of finish time.

  • Complexity: $O(V+E)$ on adjacency list, $O(V^2)$ on adjacency matrix. Always specify which representation.

  • BFS shortest path: Only gives shortest in terms of edge count (unweighted graphs). Not for weighted graphs.