Graph Traversals and Shortest Paths
Breadth-first and depth-first search in Python with their orders, trees and running times, applications (connected components, cycle detection, bipartiteness, topological sorting), and shortest paths — BFS for unweighted graphs, Dijkstra's algorithm with a heap, Bellman–Ford for negative edges and negative-cycle detection, DAG shortest paths, and Floyd–Warshall for all pairs.
Breadth-first search (BFS)
Explore the graph layer by layer from a source using a queue.
from collections import deque
def bfs(g, s):
dist = {s: 0}
parent = {s: None}
q = deque([s])
while q:
u = q.popleft()
for v in g[u]:
if v not in dist: # first discovery = shortest distance
dist[v] = dist[u] + 1
parent[v] = u
q.append(v)
return dist, parent
- Time with adjacency lists ( with a matrix); space .
- Gives shortest paths in unweighted graphs; the
parentpointers form a BFS tree. - Applications: shortest hops, connected components, bipartiteness (2-colour by BFS layers; an edge inside a layer means an odd cycle), level-order traversal, web crawling, uninformed search in AI (search).
Depth-first search (DFS)
Go as deep as possible, backtracking when stuck. Recursion (or an explicit stack) does the bookkeeping.
def dfs(g, s, seen=None, order=None):
if seen is None:
seen, order = set(), []
seen.add(s)
order.append(s) # pre-order visit
for v in g[s]:
if v not in seen:
dfs(g, v, seen, order)
return order
- Time ; recursion depth up to .
- Record discovery/finish times; in a directed graph edges are tree, back, forward or cross edges. A back edge means a cycle.
- Applications: connected components, cycle detection, topological sort, strongly connected components, path existence, maze solving, backtracking.
Topological sorting
A topological order of a DAG lists vertices so every edge goes from earlier to later (task scheduling, build systems, course prerequisites). It exists iff the graph has no directed cycle.
from collections import deque
def topo_sort(g): # Kahn's algorithm
indeg = {u: 0 for u in g}
for u in g:
for v in g[u]:
indeg[v] += 1
q = deque(u for u in g if indeg[u] == 0)
order = []
while q:
u = q.popleft()
order.append(u)
for v in g[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return order if len(order) == len(g) else None # None → cycle
Alternative: run DFS and output vertices in reverse finishing order. Both are .
Shortest paths in weighted graphs
Relaxation is the shared primitive: if , set and .
Dijkstra's algorithm (non-negative weights)
import heapq
def dijkstra(g, s): # g[u] = [(v, w), ...]
dist = {s: 0}
pq = [(0, s)]
done = set()
while pq:
d, u = heapq.heappop(pq)
if u in done:
continue # stale entry
done.add(u)
for v, w in g[u]:
nd = d + w
if nd < dist.get(v, float("inf")):
dist[v] = nd
heapq.heappush(pq, (nd, v))
return dist
- Greedy: always finalise the closest unfinished vertex. Correct because with non-negative weights no later path can be shorter.
- with a heap; with an array.
- Fails with negative edges (it may finalise a vertex too early).
Bellman–Ford (negative weights allowed)
Relax every edge times; a further improvement in round reveals a reachable negative cycle. .
Shortest paths in a DAG
Relax edges in topological order: , works with negative weights (no cycles to worry about). Negating the weights gives longest paths (critical-path scheduling).
Floyd–Warshall (all pairs)
— allow vertex as an intermediate. Three nested loops, time, space; handles negative edges; a negative means a negative cycle.
Which algorithm?
| Situation | Algorithm | Time |
|---|---|---|
| Unweighted, single source | BFS | |
| Non-negative weights, single source | Dijkstra | |
| Negative weights, single source | Bellman–Ford | |
| DAG (any weights) | topological-order relaxation | |
| All pairs, dense | Floyd–Warshall | |
| Weights 0/1 | 0-1 BFS with a deque |
Quick revision
Practice questions
GATE-style questions: 3 one-mark and 7 two-mark. Wrong MCQs cost ⅓ of their marks in the real exam; MSQ and NAT have no negative marking. Attempt each before opening the solution.
An undirected graph has adjacency lists (neighbours visited in alphabetical order)
A: B, C · B: A, D, E · C: A, F · D: B · E: B, F · F: C, E.
The order in which a recursive depth-first search from A visits the vertices is
A directed graph has edges with weights S→A 4, S→B 1, B→A 2, A→C 1, B→C 5, C→D 3. The length of the shortest path from S to D is ______.
In an unweighted graph, shortest paths (fewest edges) from a source can be found in time by
Which statements are true?
One or more options are correct.
A DAG has vertices 1, 2, 3, 4 and edges and only. The number of distinct topological orderings is ______.
What does this program print?
from collections import deque
g = {0: [1, 2], 1: [3], 2: [3, 4], 3: [5], 4: [5], 5: []}
dist = {0: 0}
q = deque([0])
while q:
u = q.popleft()
for v in g[u]:
if v not in dist:
dist[v] = dist[u] + 1
q.append(v)
print(sum(dist.values()))
Dijkstra's algorithm implemented with a binary heap on a graph with vertices and edges runs in
On a graph with 6 vertices and no negative cycles, the Bellman–Ford algorithm needs at most ______ rounds of relaxing every edge to guarantee correct shortest-path distances.
What does this program print?
g = {1: [2], 2: [1], 3: [4], 4: [3, 5], 5: [4], 6: []}
seen = set()
def dfs(u):
seen.add(u)
for v in g[u]:
if v not in seen:
dfs(v)
c = 0
for u in g:
if u not in seen:
dfs(u)
c += 1
print(c)
For the weighted directed graph (S→A 4, S→B 1, B→A 2, A→C 1, B→C 5, C→D 3), the order in which Dijkstra's algorithm extracts (finalises) the vertices is