← All Guides

Advanced Graphs

7 drills · 7 pattern exercises · 16 micro drills

Patterns Overview

Patterns used in this topic, with difficulty breakdown.

BFS + Heap 1 drill 1 Med
Topological Sort 3 drills 3 Med
DFS Recursive 2 drills 1 Easy 1 Med
DFS + Map 1 drill 1 Med

Foundation Drill

100 Flood fill Easy DFS Recursive

Starting from a pixel, change its color and all connected same-color pixels to a new color (like the paint bucket tool).

Start here in Foundations →

Coding Drills

All 7 drills grouped by pattern. Each includes prompt, solution, and complexity.

BFS + Heap (1)

95 Dijkstra simplified Medium

In plain English: Find the shortest path from one starting node to all other nodes in a graph with positive edge weights.

Always process the closest unfinished node first (via a min-heap). Once a node is popped, its shortest distance is finalized — greedy on distance.

Prompt

Given a weighted directed graph as an adjacency list and a source node, find the shortest distance to all other nodes using Dijkstra's algorithm.

Solution

import heapq

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

# graph: {0: [(1,4),(2,1)], 1: [(3,1)], 2: [(1,2),(3,5)], 3: []}
# dijkstra(graph, 0) => {0:0, 1:3, 2:1, 3:4}
O((V+E) log V) time · O(V) space

Topological Sort (3)

96 Topological sort (Kahn's) Medium

In plain English: Order tasks so that every task comes after its prerequisites, using a queue-based approach.

Start with nodes having no prerequisites (in-degree 0). Process them, reduce neighbors' in-degrees, and add newly zero-degree nodes to the queue.

Prompt

Given a directed acyclic graph (DAG) as an adjacency list, return a topological ordering of its nodes using Kahn's algorithm (BFS-based).

Solution

from collections import deque

def topological_sort(graph, n):
    in_deg = [0] * n
    for u in range(n):
        for v in graph.get(u, []):
            in_deg[v] += 1
    # seed queue with nodes that have no incoming edges (ready to process)
    q = deque(i for i in range(n) if in_deg[i] == 0)
    order = []
    while q:
        u = q.popleft()
        order.append(u)
        for v in graph.get(u, []):
            in_deg[v] -= 1
            # neighbour becomes processable once all its prerequisites are done
            if in_deg[v] == 0:
                q.append(v)
    return order if len(order) == n else []  # empty if cycle

# Test: {0:[1,2], 1:[3], 2:[3], 3:[]}, n=4 => [0,1,2,3] or [0,2,1,3]
O(V + E) time · O(V) space
97 Course schedule Medium

In plain English: Check if you can finish all courses given their prerequisite requirements (no circular dependencies).

Build a graph and run topological sort. If the resulting order has all n courses, no cycle exists. If fewer, a cycle blocks completion.

Prompt

Given n courses and prerequisite pairs [a, b] meaning 'take b before a', determine if all courses can be completed (detect if the dependency graph has a cycle).

Solution

from collections import deque

def can_finish(num_courses, prerequisites):
    graph = {i: [] for i in range(num_courses)}
    in_deg = [0] * num_courses
    for a, b in prerequisites:
        graph[b].append(a)
        in_deg[a] += 1
    q = deque(i for i in range(num_courses) if in_deg[i] == 0)
    count = 0
    while q:
        u = q.popleft()
        count += 1
        for v in graph[u]:
            in_deg[v] -= 1
            if in_deg[v] == 0:
                q.append(v)
    return count == num_courses  # all courses processed = no cycle

# Test: can_finish(2, [[1,0]]) == True
# Test: can_finish(2, [[1,0],[0,1]]) == False  (cycle)
O(V + E) time · O(V + E) space
146 Course Schedule II Medium

In plain English: Find an order to take all courses respecting prerequisites, or determine it's impossible.

Kahn's algorithm: compute in-degrees, start with zero in-degree nodes. Process each node by reducing neighbors' in-degrees. If all nodes are processed, we have a valid order; otherwise there's a cycle.

Prompt

Given numCourses and prerequisites pairs [a,b] meaning 'b before a', return a valid course ordering. Return [] if impossible (cycle exists).

Solution

from collections import deque, defaultdict

def find_order(num_courses, prerequisites):
    # Kahn's algorithm: iteratively remove zero in-degree nodes to build a topological order
    graph = defaultdict(list)
    in_degree = [0] * num_courses
    for a, b in prerequisites:
        graph[b].append(a)
        in_degree[a] += 1
    queue = deque(i for i in range(num_courses) if in_degree[i] == 0)
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbor in graph[node]:
            in_degree[neighbor] -= 1
            # course becomes takeable once all its prerequisites are satisfied
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
    return order if len(order) == num_courses else []

# Test: find_order(4, [[1,0],[2,0],[3,1],[3,2]])
#       == [0,1,2,3] or [0,2,1,3] (valid topological orders)
# Test: find_order(2, [[0,1],[1,0]]) == [] (cycle)
O(V + E) time · O(V + E) space

DFS Recursive (2)

98 Number of islands Medium

In plain English: Count separate groups of connected land cells in a grid.

Each unvisited '1' starts a new island. DFS/BFS from it marks all connected land as visited — the number of fresh DFS calls equals the number of islands.

Prompt

Given a 2D grid of '1' (land) and '0' (water), count the number of islands. An island is surrounded by water and formed by connecting adjacent land cells horizontally or vertically.

Solution

def num_islands(grid):
    if not grid:
        return 0
    m, n = len(grid), len(grid[0])
    count = 0

    def dfs(i, j):
        if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
            return
        grid[i][j] = '#'  # mark visited
        dfs(i+1, j); dfs(i-1, j); dfs(i, j+1); dfs(i, j-1)

    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                dfs(i, j)
                count += 1
    return count

# Test: grid = [["1","1","0"],["1","0","0"],["0","0","1"]] => 2
O(m*n) time · O(m*n) space (recursion stack)
100 Flood fill Easy

In plain English: Starting from a pixel, change its color and all connected same-color pixels to a new color (like the paint bucket tool).

DFS from the starting pixel, changing color as you go. Only recurse into neighbors that match the original color — naturally stops at boundaries.

Prompt

Given a 2D image grid, a starting pixel (sr, sc), and a new color, flood fill the connected region of the starting pixel's color with the new color.

Solution

def flood_fill(image, sr, sc, new_color):
    orig = image[sr][sc]
    if orig == new_color:
        return image
    m, n = len(image), len(image[0])

    def dfs(i, j):
        if i < 0 or i >= m or j < 0 or j >= n or image[i][j] != orig:
            return
        image[i][j] = new_color  # fill
        dfs(i+1, j); dfs(i-1, j); dfs(i, j+1); dfs(i, j-1)

    dfs(sr, sc)
    return image

# Test: flood_fill([[1,1,1],[1,1,0],[1,0,1]], 1, 1, 2)
# => [[2,2,2],[2,2,0],[2,0,1]]
O(m*n) time · O(m*n) space

DFS + Map (1)

99 Clone graph Medium

In plain English: Make a complete copy of a graph where each node and its connections are duplicated.

Use a hashmap from original node to cloned node. DFS through the graph — if a neighbor is already cloned, reuse it; otherwise clone and recurse.

Prompt

Given a reference to a node in an undirected connected graph, return a deep copy of the graph. Each node has a value and a list of neighbors.

Solution

class GraphNode:
    def __init__(self, val=0, neighbors=None):
        self.val = val
        self.neighbors = neighbors if neighbors else []

def clone_graph(node):
    if not node:
        return None
    cloned = {}
    def dfs(n):
        if n in cloned:
            return cloned[n]
        copy = GraphNode(n.val)
        cloned[n] = copy  # map original -> clone
        for nb in n.neighbors:
            copy.neighbors.append(dfs(nb))
        return copy
    return dfs(node)

# Test: graph 1--2--3--1 => cloned graph with same structure
O(V + E) time · O(V) space

Pattern Recognition

7 exercises: spot the signals, pick the method, avoid the trap.

95 Dijkstra simplified Medium

Shortest path from source in a weighted graph.

Signals

Heap / Priority Queue

Min-heap of (distance, node). Pop closest, relax edges. First pop of each node is shortest. O((V+E) log V).

Queue / BFS

BFS only works for unweighted graphs. For weighted, you MUST use Dijkstra or Bellman-Ford.

Trap

Using BFS on weighted graphs gives WRONG answers. The heap ensures you process closest first.

96 Topological sort (Kahn's) Medium

Return a topological ordering of a DAG.

Signals

Topological Sort

Kahn's: queue nodes with in-degree 0. Process, reduce neighbors' in-degrees. Add newly zero-degree nodes. O(V+E).

DFS / Recursion

DFS post-order reversed gives topological order. Also O(V+E).

Trap

If result has fewer than V nodes, the graph has a cycle (not a DAG).

97 Course schedule Medium

Can all courses be completed given prerequisites? (Cycle detection in DAG.)

Signals

Topological Sort

Run Kahn's algorithm. If all nodes are processed, no cycle → all courses completable. O(V+E).

DFS / Recursion

DFS with 3-color marking (white/gray/black). Gray→Gray edge = cycle. O(V+E).

Trap

This IS a cycle detection problem in disguise. Model courses as a directed graph.

98 Number of islands Medium

Count islands in a 2D grid of '1' (land) and '0' (water).

Signals

DFS / Recursion

For each unvisited '1', run DFS to mark all connected land. Count DFS starts. O(m*n).

Queue / BFS

BFS from each unvisited '1' also works. Same counting logic, same O(m*n).

Trap

This is connected components on a grid. Grid = implicit graph where neighbors are adjacent cells.

99 Clone graph Medium

Deep copy a graph. Each node has a value and neighbor list.

Signals

DFS / Recursion

DFS with hashmap {original: clone}. For each neighbor: if cloned, reuse; else clone and recurse. O(V+E).

Queue / BFS

BFS with same hashmap approach. O(V+E).

Trap

Must handle cycles — the hashmap prevents infinite loops by checking if a node is already cloned.

100 Flood fill Easy

Change all connected cells of the same color starting from (sr, sc).

Signals

DFS / Recursion

DFS from (sr, sc). Change color, recurse on 4 neighbors matching original color. O(m*n).

Queue / BFS

BFS from start pixel also works. Same O(m*n).

Trap

If new_color == original_color, return immediately — otherwise infinite recursion.

146 Course Schedule II Medium

Given n courses and prerequisites, return a valid order to take all courses, or empty if impossible.

Signals

Topological Sort

Kahn's BFS: process nodes with in-degree 0. If result has fewer than n nodes, there's a cycle. O(V+E).

DFS / Recursion

DFS with three states (unvisited/in-progress/done). Post-order gives reverse topological order. O(V+E).

Trap

Don't forget to detect cycles — if the topological order has fewer than n nodes, return empty.

Related Micro Drills

16 quick recall drills to reinforce this topic.

225 Dijkstra's algorithm template Graph Exploration 15s

Min-heap BFS for weighted graphs. Skip stale entries (d > dist[u]). O((V+E) log V). Non-negative weights only.

import heapq
from collections import defaultdict

def dijkstra(graph, src, n):
    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 dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                heapq.heappush(heap, (dist[v], v))
    return dist
69 Heappop Search 5s

heappop extracts the minimum. Pair with heappush for priority queue operations.

import heapq
heapq.heappop(h)
236 Network delay time (Dijkstra) Graph Exploration 15s

Dijkstra from source k. Answer is max distance (time for signal to reach farthest node). -1 if not all reachable.

import heapq
from collections import defaultdict

def networkDelayTime(times, n, k):
    graph = defaultdict(list)
    for u, v, w in times:
        graph[u].append((v, w))
    dist = {k: 0}
    heap = [(0, k)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist.get(u, float('inf')): continue
        for v, w in graph[u]:
            nd = d + w
            if nd < dist.get(v, float('inf')):
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return max(dist.values()) if len(dist) == n else -1
146 Topological sort (Kahn's) Graph Exploration 15s

BFS-based topo sort processes zero-indegree nodes first. Detects cycles if order is incomplete.

from collections import deque
q = deque(n for n in range(V) if indeg[n] == 0)
order = []
while q:
    u = q.popleft()
    order.append(u)
    for v in adj[u]:
        indeg[v] -= 1
        if indeg[v] == 0: q.append(v)
265 Topological sort (Kahn's BFS) Graph Exploration 20s

Kahn's algorithm: start from zero in-degree nodes, peel layers. If len(order) < n, there's a cycle.

from collections import deque, defaultdict
in_deg = [0] * n
graph = defaultdict(list)
for a, b in edges:
    graph[b].append(a)
    in_deg[a] += 1
q = deque(i for i in range(n) if in_deg[i] == 0)
order = []
while q:
    node = q.popleft()
    order.append(node)
    for nb in graph[node]:
        in_deg[nb] -= 1
        if in_deg[nb] == 0:
            q.append(nb)
5 Check if sorted Pointer Manipulation 10s

Sortedness check is a precondition guard for binary search and merge operations.

all(a[i] &lt;= a[i+1] for i in range(len(a)-1))
41 Deque append/popleft Queue Patterns 10s

Deque gives O(1) operations at both ends. Essential for BFS and sliding window.

from collections import deque
q = deque()
q.append(x)
q.popleft()
67 Add edge (undirected) Graph Exploration 5s

Undirected edges add both directions. Foundation for graph construction.

graph[u].append(v)
graph[v].append(u)
144 Multi-source BFS (rotting oranges) Graph Exploration 15s

Initialize queue with all sources. BFS radiates outward in parallel from all starting points.

q = deque()
for i in range(R):
    for j in range(C):
        if grid[i][j] == 2: q.append((i, j, 0))
while q:
    i, j, t = q.popleft()
    for di, dj in ((1,0),(-1,0),(0,1),(0,-1)):
        ni, nj = i+di, j+dj
        if 0&lt;=ni&lt;R and 0&lt;=nj&lt;C and grid[ni][nj]==1:
            grid[ni][nj] = 2
            q.append((ni, nj, t+1))
231 Number of islands (DFS on grid) Graph Exploration 10s

Flood-fill DFS: mark visited by changing '1' to '0'. Each DFS call covers one island. Count DFS starts.

def numIslands(grid):
    m, n = len(grid), len(grid[0])
    def dfs(i, j):
        if i &lt; 0 or i &gt;= m or j &lt; 0 or j &gt;= n or grid[i][j] != '1':
            return
        grid[i][j] = '0'  # mark visited
        dfs(i+1, j); dfs(i-1, j); dfs(i, j+1); dfs(i, j-1)

    count = 0
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                dfs(i, j)
                count += 1
    return count
87 Move zeros to end Pointer Manipulation 10s

Move-zeroes is remove-element followed by a fill. Classic two-pointer warm-up.

w = 0
for r in range(len(a)):
    if a[r] != 0:
        a[w] = a[r]
        w += 1
while w &lt; len(a):
    a[w] = 0
    w += 1
126 Remove duplicates in-place Pointer Manipulation 10s

Write-pointer compaction for sorted arrays. Returns new length.

w = 1
for i in range(1, len(a)):
    if a[i] != a[i-1]:
        a[w] = a[i]
        w += 1
return w
233 Clone graph (DFS + hashmap) Graph Exploration 10s

HashMap maps original → clone. DFS creates clone before recursing neighbors to handle cycles.

def cloneGraph(node):
    if not node: return None
    cloned = {}
    def dfs(n):
        if n in cloned: return cloned[n]
        copy = Node(n.val)
        cloned[n] = copy
        for nb in n.neighbors:
            copy.neighbors.append(dfs(nb))
        return copy
    return dfs(node)
66 DFS recursive Graph Exploration 10s

Recursive DFS is the most natural graph traversal. Watch for stack overflow on deep graphs.

def dfs(node):
    visited.add(node)
    for nb in graph[node]:
        if nb not in visited:
            dfs(nb)
263 Flood fill (DFS on grid) Graph Exploration 15s

Recursive DFS on a grid with bounds + color check. Same pattern for island counting, connected regions, etc.

def fill(grid, i, j, old, new):
    if i &lt; 0 or i &gt;= len(grid): return
    if j &lt; 0 or j &gt;= len(grid[0]): return
    if grid[i][j] != old: return
    grid[i][j] = new
    for di, dj in [(1,0),(-1,0),(0,1),(0,-1)]:
        fill(grid, i+di, j+dj, old, new)
26 Init defaultdict Hash Strategies 10s

defaultdict eliminates key-existence checks. Simplifies grouping and adjacency list construction.

from collections import defaultdict
d = defaultdict(list)