Skills101

Data Structures & Algorithms · Lesson 12 of 15

Graphs — BFS, DFS, Topological Sort, Union-Find and Dijkstra

A graph is a set of nodes (vertices) connected by edges. Cities and roads, people and friendships, courses and prerequisites — all graphs. Trees are a special graph with no cycles. Graph questions look scary, but BFS and DFS solve most of them.

Representing a graph

Interviews usually give you an edge list like [[0,1],[1,2],[2,0]]. Convert it to an adjacency list — each node maps to its neighbours:

from collections import defaultdict

def build_graph(n, edges, directed=False):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        if not directed:
            graph[v].append(u)
    return graph
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
for (int[] e : edges) { graph.get(e[0]).add(e[1]); graph.get(e[1]).add(e[0]); }

A grid is also a graph: each cell is a node connected to its 4 neighbours (up, down, left, right).

Always keep a visited set. Unlike trees, graphs can have cycles — without it you loop forever.

DFS — go deep first

def dfs(node, graph, visited):
    visited.add(node)
    for nxt in graph[node]:
        if nxt not in visited:
            dfs(nxt, graph, visited)

Number of Islands (count groups of connected '1' cells): scan the grid; each time you find unvisited land, that's a new island — run DFS to "sink" all its cells.

def num_islands(grid):
    rows, cols = len(grid), len(grid[0])
    def sink(r, c):
        if r < 0 or c < 0 or r >= rows or c >= cols or grid[r][c] != '1':
            return
        grid[r][c] = '0'                          # mark visited
        sink(r + 1, c); sink(r - 1, c); sink(r, c + 1); sink(r, c - 1)

    count = 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                count += 1
                sink(r, c)
    return count

O(rows × cols) — every cell is visited once. Number of Provinces, Flood Fill and Surrounded Regions are the same idea.

BFS — go level by level (shortest steps)

BFS explores nodes in order of distance from the start, so in an unweighted graph it finds the shortest path (fewest edges).

from collections import deque

def shortest_steps(graph, start, target):
    q, dist = deque([start]), {start: 0}
    while q:
        node = q.popleft()
        if node == target:
            return dist[node]
        for nxt in graph[node]:
            if nxt not in dist:
                dist[nxt] = dist[node] + 1
                q.append(nxt)
    return -1

Rotting Oranges uses multi-source BFS: put all rotten oranges in the queue at the start, then spread one minute (one level) at a time. Word Ladder is BFS where each word connects to words one letter away.

Topological sort — ordering with prerequisites

Course Schedule: can you finish all courses if some must come before others? That's asking whether the directed graph has a cycle. Kahn's algorithm: repeatedly take courses with no remaining prerequisites (in-degree 0).

def can_finish(n, prerequisites):
    graph = defaultdict(list)
    indegree = [0] * n
    for course, pre in prerequisites:
        graph[pre].append(course)
        indegree[course] += 1

    q = deque(i for i in range(n) if indegree[i] == 0)
    taken = 0
    while q:
        c = q.popleft()
        taken += 1
        for nxt in graph[c]:
            indegree[nxt] -= 1
            if indegree[nxt] == 0:
                q.append(nxt)
    return taken == n          # if a cycle exists, some courses never reach in-degree 0

The order in which courses leave the queue is a valid topological order (Course Schedule II). O(V + E).

Union-Find (Disjoint Set Union)

Union-Find tracks which nodes belong to the same group, with near-O(1) union and find. Perfect for "are these connected?" and "which edge creates a cycle?" (Redundant Connection).

parent = list(range(n))

def find(x):
    while parent[x] != x:
        parent[x] = parent[parent[x]]   # path compression
        x = parent[x]
    return x

def union(a, b):
    ra, rb = find(a), find(b)
    if ra == rb:
        return False                    # already connected → this edge makes a cycle
    parent[ra] = rb
    return True

Dijkstra — shortest path with weights

When edges have weights (travel time, cost), BFS isn't enough. Dijkstra's algorithm uses a min-heap to always expand the closest unfinished node.

import heapq

def dijkstra(graph, src, n):            # graph[u] = [(v, w), ...]
    dist = [float('inf')] * n
    dist[src] = 0
    heap = [(0, src)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue                    # stale entry
        for v, w in graph[u]:
            if d + w < dist[v]:
                dist[v] = d + w
                heapq.heappush(heap, (dist[v], v))
    return dist

O((V + E) log V). Works only with non-negative weights. Network Delay Time is Dijkstra directly.

Which algorithm when?

QuestionUse
count connected groups / islandsDFS or BFS (or Union-Find)
shortest path, unweighted / fewest stepsBFS
spreading from many sources at oncemulti-source BFS
order with prerequisites / detect cycle in directed graphtopological sort
dynamic connectivity, redundant edgeUnion-Find
shortest path with non-negative weightsDijkstra

Common mistakes

  1. No visited tracking → infinite loops.
  2. Marking visited when popping in BFS instead of when pushing → the same node enters the queue many times.
  3. Grid bounds checks in the wrong order (check bounds before reading grid[r][c]).
  4. Using BFS for weighted shortest paths.

Key takeaways

  • Convert input to an adjacency list; grids are implicit graphs.
  • DFS for exploring/connectivity, BFS for shortest steps.
  • Topological sort for prerequisites, Union-Find for grouping, Dijkstra for weighted shortest paths.

Practice what you learned

📝 Graphs Quiz8 questions · instant answers & explanations · +10 XPTake the quiz →