Graph Exercises: Answer Sheet & Worked Solutions
This is the answer sheet for the graph exercises. Each solution gives the key idea, the optimal complexity, the code in all four languages (use the switcher), and a common pitfall. Try the problems first — then check your work here.
Q1. Number of Islands
Idea: Treat the grid as a graph where each land cell connects to its 4 neighbors. Scan every cell; when you hit unvisited land, increment the count and flood-fill (DFS/BFS) the whole island, sinking it to water so it isn’t counted again. This is connected components on a grid.
Complexity: O(m·n) time, O(m·n) worst-case space (recursion / queue).
int numIslands(std::vector<std::vector<char>>& grid) {
int m = grid.size(), n = grid[0].size(), count = 0;
std::function<void(int,int)> dfs = [&](int r, int c) {
if (r < 0 || c < 0 || r >= m || c >= n || grid[r][c] != '1') return;
grid[r][c] = '0'; // sink
dfs(r+1,c); dfs(r-1,c); dfs(r,c+1); dfs(r,c-1);
};
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (grid[r][c] == '1') { count++; dfs(r, c); }
return count;
}
int numIslands(char[][] grid) {
int m = grid.length, n = grid[0].length, count = 0;
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (grid[r][c] == '1') { count++; dfs(grid, r, c); }
return count;
}
void dfs(char[][] grid, int r, int c) {
if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length
|| grid[r][c] != '1') return;
grid[r][c] = '0'; // sink
dfs(grid, r+1, c); dfs(grid, r-1, c);
dfs(grid, r, c+1); dfs(grid, r, c-1);
}
function numIslands(grid) {
const m = grid.length, n = grid[0].length;
let count = 0;
function dfs(r, c) {
if (r < 0 || c < 0 || r >= m || c >= n || grid[r][c] !== '1') return;
grid[r][c] = '0'; // sink
dfs(r+1, c); dfs(r-1, c); dfs(r, c+1); dfs(r, c-1);
}
for (let r = 0; r < m; r++)
for (let c = 0; c < n; c++)
if (grid[r][c] === '1') { count++; dfs(r, c); }
return count;
}
def num_islands(grid):
if not grid:
return 0
m, n = len(grid), len(grid[0])
count = 0
def dfs(r, c):
if r < 0 or c < 0 or r >= m or c >= n or grid[r][c] != '1':
return
grid[r][c] = '0' # sink
dfs(r+1, c); dfs(r-1, c); dfs(r, c+1); dfs(r, c-1)
for r in range(m):
for c in range(n):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count
Pitfall: Forgetting to mark cells visited (sinking to
'0') causes infinite recursion or massive overcounting. If you can’t mutate the grid, keep a separatevisitedmatrix.
Q2. Flood Fill
Idea: A straightforward DFS/BFS from the start pixel, spreading to 4-directional neighbors that share the original color. Capture the start color first, and bail out immediately if it already equals the new color (otherwise you loop forever).
Complexity: O(m·n) time, O(m·n) space worst case.
std::vector<std::vector<int>> floodFill(std::vector<std::vector<int>>& img,
int sr, int sc, int newColor) {
int old = img[sr][sc];
if (old == newColor) return img;
int m = img.size(), n = img[0].size();
std::function<void(int,int)> dfs = [&](int r, int c) {
if (r < 0 || c < 0 || r >= m || c >= n || img[r][c] != old) return;
img[r][c] = newColor;
dfs(r+1,c); dfs(r-1,c); dfs(r,c+1); dfs(r,c-1);
};
dfs(sr, sc);
return img;
}
int[][] floodFill(int[][] img, int sr, int sc, int newColor) {
int old = img[sr][sc];
if (old == newColor) return img;
fill(img, sr, sc, old, newColor);
return img;
}
void fill(int[][] img, int r, int c, int old, int nc) {
if (r < 0 || c < 0 || r >= img.length || c >= img[0].length
|| img[r][c] != old) return;
img[r][c] = nc;
fill(img, r+1, c, old, nc); fill(img, r-1, c, old, nc);
fill(img, r, c+1, old, nc); fill(img, r, c-1, old, nc);
}
function floodFill(img, sr, sc, newColor) {
const old = img[sr][sc];
if (old === newColor) return img;
const m = img.length, n = img[0].length;
function dfs(r, c) {
if (r < 0 || c < 0 || r >= m || c >= n || img[r][c] !== old) return;
img[r][c] = newColor;
dfs(r+1, c); dfs(r-1, c); dfs(r, c+1); dfs(r, c-1);
}
dfs(sr, sc);
return img;
}
def flood_fill(img, sr, sc, new_color):
old = img[sr][sc]
if old == new_color:
return img
m, n = len(img), len(img[0])
def dfs(r, c):
if r < 0 or c < 0 or r >= m or c >= n or img[r][c] != old:
return
img[r][c] = new_color
dfs(r+1, c); dfs(r-1, c); dfs(r, c+1); dfs(r, c-1)
dfs(sr, sc)
return img
Pitfall: Skipping the
old == newColorearly return makes the recursion never terminate, because repainted cells still match the fill condition.
Q3. Clone Graph
Idea: DFS (or BFS) while keeping a hash map from original node → its clone. Create a clone the first time you see a node, then recurse to clone and wire up its neighbors. The map both stores results and breaks cycles — if a node is already in the map, return its existing clone.
Complexity: O(V + E) time, O(V) space.
// struct Node { int val; vector<Node*> neighbors; };
Node* cloneGraph(Node* node, std::unordered_map<Node*, Node*>& seen) {
if (!node) return nullptr;
if (seen.count(node)) return seen[node];
Node* copy = new Node(node->val);
seen[node] = copy; // record before recursing
for (Node* nb : node->neighbors)
copy->neighbors.push_back(cloneGraph(nb, seen));
return copy;
}
// class Node { int val; List<Node> neighbors; }
Node cloneGraph(Node node, Map<Node, Node> seen) {
if (node == null) return null;
if (seen.containsKey(node)) return seen.get(node);
Node copy = new Node(node.val);
seen.put(node, copy); // record before recursing
for (Node nb : node.neighbors)
copy.neighbors.add(cloneGraph(nb, seen));
return copy;
}
// class Node { constructor(val) { this.val = val; this.neighbors = []; } }
function cloneGraph(node, seen = new Map()) {
if (!node) return null;
if (seen.has(node)) return seen.get(node);
const copy = new Node(node.val);
seen.set(node, copy); // record before recursing
for (const nb of node.neighbors)
copy.neighbors.push(cloneGraph(nb, seen));
return copy;
}
# class Node: def __init__(self, val): self.val = val; self.neighbors = []
def clone_graph(node, seen=None):
if node is None:
return None
if seen is None:
seen = {}
if node in seen:
return seen[node]
copy = Node(node.val)
seen[node] = copy # record before recursing
for nb in node.neighbors:
copy.neighbors.append(clone_graph(nb, seen))
return copy
Pitfall: Insert the clone into the map before recursing into neighbors. If you recurse first, a cycle leads you back to an unrecorded node and you spin forever creating duplicate clones.
Q4. Course Schedule
Idea: Build a directed graph b → a for each prerequisite [a, b]. You can finish all courses iff the graph is a DAG (no cycle). Kahn’s topological sort is the cleanest test: if you can process all numCourses vertices by repeatedly removing in-degree-0 nodes, there’s no cycle.
Complexity: O(V + E) time, O(V + E) space.
bool canFinish(int numCourses, std::vector<std::vector<int>>& prereq) {
std::vector<std::vector<int>> adj(numCourses);
std::vector<int> indeg(numCourses, 0);
for (auto& p : prereq) { adj[p[1]].push_back(p[0]); indeg[p[0]]++; }
std::queue<int> q;
for (int i = 0; i < numCourses; i++) if (indeg[i] == 0) q.push(i);
int done = 0;
while (!q.empty()) {
int u = q.front(); q.pop(); done++;
for (int v : adj[u]) if (--indeg[v] == 0) q.push(v);
}
return done == numCourses;
}
boolean canFinish(int numCourses, int[][] prereq) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numCourses; i++) adj.add(new ArrayList<>());
int[] indeg = new int[numCourses];
for (int[] p : prereq) { adj.get(p[1]).add(p[0]); indeg[p[0]]++; }
Queue<Integer> q = new ArrayDeque<>();
for (int i = 0; i < numCourses; i++) if (indeg[i] == 0) q.add(i);
int done = 0;
while (!q.isEmpty()) {
int u = q.poll(); done++;
for (int v : adj.get(u)) if (--indeg[v] == 0) q.add(v);
}
return done == numCourses;
}
function canFinish(numCourses, prereq) {
const adj = Array.from({ length: numCourses }, () => []);
const indeg = new Array(numCourses).fill(0);
for (const [a, b] of prereq) { adj[b].push(a); indeg[a]++; }
const queue = [];
for (let i = 0; i < numCourses; i++) if (indeg[i] === 0) queue.push(i);
let done = 0, head = 0;
while (head < queue.length) {
const u = queue[head++]; done++;
for (const v of adj[u]) if (--indeg[v] === 0) queue.push(v);
}
return done === numCourses;
}
from collections import deque
def can_finish(num_courses, prereq):
adj = [[] for _ in range(num_courses)]
indeg = [0] * num_courses
for a, b in prereq:
adj[b].append(a)
indeg[a] += 1
q = deque(i for i in range(num_courses) if indeg[i] == 0)
done = 0
while q:
u = q.popleft()
done += 1
for v in adj[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return done == num_courses
Pitfall: Get the edge direction right. Prerequisite
[a, b]means “b before a,” so the edge isb → aand you increment the in-degree ofa. Reversing it silently passes some tests and fails others.
Q5. Word Ladder
Idea: Model each word as a vertex with edges to words that differ by one letter; the answer is the shortest path from beginWord to endWord, so use BFS. Generate neighbors by trying every position × every letter a–z and keeping only words still in the unused set. Removing words from the set as you enqueue them keeps each visited once.
Complexity: O(N · L · 26) time where N = number of words, L = word length; O(N · L) space.
int ladderLength(std::string begin, std::string end,
std::vector<std::string>& wordList) {
std::unordered_set<std::string> dict(wordList.begin(), wordList.end());
if (!dict.count(end)) return 0;
std::queue<std::string> q; q.push(begin);
int steps = 1;
while (!q.empty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
std::string w = q.front(); q.pop();
if (w == end) return steps;
for (int p = 0; p < (int)w.size(); p++) {
char orig = w[p];
for (char c = 'a'; c <= 'z'; c++) {
w[p] = c;
if (dict.count(w)) { dict.erase(w); q.push(w); }
}
w[p] = orig;
}
}
steps++;
}
return 0;
}
int ladderLength(String begin, String end, List<String> wordList) {
Set<String> dict = new HashSet<>(wordList);
if (!dict.contains(end)) return 0;
Queue<String> q = new ArrayDeque<>();
q.add(begin);
int steps = 1;
while (!q.isEmpty()) {
int sz = q.size();
for (int i = 0; i < sz; i++) {
String w = q.poll();
if (w.equals(end)) return steps;
char[] arr = w.toCharArray();
for (int p = 0; p < arr.length; p++) {
char orig = arr[p];
for (char c = 'a'; c <= 'z'; c++) {
arr[p] = c;
String nxt = new String(arr);
if (dict.remove(nxt)) q.add(nxt);
}
arr[p] = orig;
}
}
steps++;
}
return 0;
}
function ladderLength(begin, end, wordList) {
const dict = new Set(wordList);
if (!dict.has(end)) return 0;
let queue = [begin], steps = 1;
while (queue.length) {
const next = [];
for (const word of queue) {
if (word === end) return steps;
const arr = word.split('');
for (let p = 0; p < arr.length; p++) {
const orig = arr[p];
for (let c = 97; c <= 122; c++) {
arr[p] = String.fromCharCode(c);
const nxt = arr.join('');
if (dict.has(nxt)) { dict.delete(nxt); next.push(nxt); }
}
arr[p] = orig;
}
}
queue = next;
steps++;
}
return 0;
}
from collections import deque
def ladder_length(begin, end, word_list):
dict_ = set(word_list)
if end not in dict_:
return 0
q = deque([begin])
steps = 1
while q:
for _ in range(len(q)):
word = q.popleft()
if word == end:
return steps
for p in range(len(word)):
for c in "abcdefghijklmnopqrstuvwxyz":
nxt = word[:p] + c + word[p+1:]
if nxt in dict_:
dict_.remove(nxt)
q.append(nxt)
steps += 1
return 0
Pitfall: Counting edges instead of words. The transformation length counts the words in the sequence (including both endpoints), so start
stepsat 1, not 0.
Q6. Network Delay Time
Idea: Single-source shortest path from k, then the answer is the maximum shortest distance (the last node to hear the signal). Use Dijkstra with a min-heap. If any node is still unreachable (distance infinite), return -1.
Complexity: O((V + E) log V) time, O(V + E) space.
int networkDelayTime(std::vector<std::vector<int>>& times, int n, int k) {
std::vector<std::vector<std::pair<int,int>>> g(n + 1);
for (auto& t : times) g[t[0]].push_back({t[1], t[2]});
std::vector<int> dist(n + 1, INT_MAX);
dist[k] = 0;
std::priority_queue<std::pair<int,int>, std::vector<std::pair<int,int>>,
std::greater<>> pq;
pq.push({0, k});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue;
for (auto [v, w] : g[u])
if (d + w < dist[v]) { dist[v] = d + w; pq.push({dist[v], v}); }
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == INT_MAX) return -1;
ans = std::max(ans, dist[i]);
}
return ans;
}
int networkDelayTime(int[][] times, int n, int k) {
List<List<int[]>> g = new ArrayList<>();
for (int i = 0; i <= n; i++) g.add(new ArrayList<>());
for (int[] t : times) g.get(t[0]).add(new int[]{t[1], t[2]});
int[] dist = new int[n + 1];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[k] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.add(new int[]{0, k});
while (!pq.isEmpty()) {
int[] top = pq.poll();
int d = top[0], u = top[1];
if (d > dist[u]) continue;
for (int[] e : g.get(u))
if (d + e[1] < dist[e[0]]) {
dist[e[0]] = d + e[1];
pq.add(new int[]{dist[e[0]], e[0]});
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == Integer.MAX_VALUE) return -1;
ans = Math.max(ans, dist[i]);
}
return ans;
}
function networkDelayTime(times, n, k) {
const g = Array.from({ length: n + 1 }, () => []);
for (const [u, v, w] of times) g[u].push([v, w]);
const dist = new Array(n + 1).fill(Infinity);
dist[k] = 0;
const heap = [[0, k]]; // [distance, node]
const push = (x) => {
heap.push(x); let i = heap.length - 1;
while (i > 0) { const p = (i - 1) >> 1;
if (heap[p][0] <= heap[i][0]) break;
[heap[i], heap[p]] = [heap[p], heap[i]]; i = p; }
};
const pop = () => {
const top = heap[0], last = heap.pop();
if (heap.length) { heap[0] = last; let i = 0;
while (true) { let s = i, l = 2*i+1, r = 2*i+2;
if (l < heap.length && heap[l][0] < heap[s][0]) s = l;
if (r < heap.length && heap[r][0] < heap[s][0]) s = r;
if (s === i) break; [heap[i], heap[s]] = [heap[s], heap[i]]; i = s; } }
return top;
};
while (heap.length) {
const [d, u] = pop();
if (d > dist[u]) continue;
for (const [v, w] of g[u])
if (d + w < dist[v]) { dist[v] = d + w; push([dist[v], v]); }
}
let ans = 0;
for (let i = 1; i <= n; i++) {
if (dist[i] === Infinity) return -1;
ans = Math.max(ans, dist[i]);
}
return ans;
}
import heapq
def network_delay_time(times, n, k):
g = [[] for _ in range(n + 1)]
for u, v, w in times:
g[u].append((v, w))
dist = [float('inf')] * (n + 1)
dist[k] = 0
pq = [(0, k)]
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]:
continue
for v, w in g[u]:
if d + w < dist[v]:
dist[v] = d + w
heapq.heappush(pq, (dist[v], v))
ans = max(dist[1:])
return ans if ans != float('inf') else -1
Pitfall: Nodes are labeled
1..n, so size your arraysn + 1and ignore index 0. Also remember to return-1when any node stays unreachable — taking the max alone would wrongly return infinity.
Q7. Cheapest Flights Within K Stops
Idea: A bounded version of Bellman-Ford: run exactly k + 1 relaxation rounds (at most k stops means at most k + 1 edges). Crucially, relax from a snapshot of distances each round so a single round can’t chain multiple flights together.
Complexity: O(k · E) time, O(V) space.
int findCheapestPrice(int n, std::vector<std::vector<int>>& flights,
int src, int dst, int k) {
const int INF = 1e9;
std::vector<int> dist(n, INF);
dist[src] = 0;
for (int i = 0; i <= k; i++) { // k+1 rounds
std::vector<int> tmp = dist; // snapshot
for (auto& f : flights) {
int u = f[0], v = f[1], w = f[2];
if (dist[u] != INF && dist[u] + w < tmp[v])
tmp[v] = dist[u] + w;
}
dist = tmp;
}
return dist[dst] == INF ? -1 : dist[dst];
}
int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
final int INF = 1_000_000_000;
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[src] = 0;
for (int i = 0; i <= k; i++) { // k+1 rounds
int[] tmp = dist.clone(); // snapshot
for (int[] f : flights) {
int u = f[0], v = f[1], w = f[2];
if (dist[u] != INF && dist[u] + w < tmp[v])
tmp[v] = dist[u] + w;
}
dist = tmp;
}
return dist[dst] == INF ? -1 : dist[dst];
}
function findCheapestPrice(n, flights, src, dst, k) {
const INF = Infinity;
let dist = new Array(n).fill(INF);
dist[src] = 0;
for (let i = 0; i <= k; i++) { // k+1 rounds
const tmp = dist.slice(); // snapshot
for (const [u, v, w] of flights)
if (dist[u] !== INF && dist[u] + w < tmp[v])
tmp[v] = dist[u] + w;
dist = tmp;
}
return dist[dst] === INF ? -1 : dist[dst];
}
def find_cheapest_price(n, flights, src, dst, k):
INF = float('inf')
dist = [INF] * n
dist[src] = 0
for _ in range(k + 1): # k+1 rounds
tmp = dist[:] # snapshot
for u, v, w in flights:
if dist[u] != INF and dist[u] + w < tmp[v]:
tmp[v] = dist[u] + w
dist = tmp
return -1 if dist[dst] == INF else dist[dst]
Pitfall: Relaxing in place (without the per-round snapshot) lets one round take several flights at once, so you’d allow more than
kstops. Always relax from the previous round’s distances.
That’s the full answer sheet. Back to the graph exercises or continue to greedy algorithms.