01 - DSA: Graphs ​
Why This Matters for Temple ​
Temple (ex-Zomato founding team) leans heavy on core CS fundamentals. Graphs show up everywhere: dependency resolution, network modeling, shortest-path routing, scheduling. Expect whiteboard-style problems and follow-ups on complexity.
Quick Reference ​
Scan this table in 5 minutes before your interview.
| Algorithm | Time | Space | Key Idea | When to Use |
|---|---|---|---|---|
| BFS | O(V + E) | O(V) | Level-by-level exploration using a queue | Shortest path in unweighted graph, level-order |
| DFS | O(V + E) | O(V) | Go deep via recursion/stack, backtrack | Path finding, cycle detection, connected components |
| Topological Sort (Kahn's) | O(V + E) | O(V) | Process zero in-degree nodes first | Dependency ordering, course scheduling |
| Topological Sort (DFS) | O(V + E) | O(V) | Post-order DFS, reverse the result | Same as above, sometimes cleaner recursion |
| Dijkstra | O((V + E) log V) | O(V) | Greedy + min-heap, relax edges | Shortest path in weighted graph (non-negative) |
| Cycle Detection (Directed) | O(V + E) | O(V) | 3-color DFS: WHITE/GRAY/BLACK | Detecting back edges in directed graphs |
| Cycle Detection (Undirected) | O(V + E) | O(V) | DFS with parent tracking | Detecting cycles in undirected graphs |
| Union-Find | O(alpha(n)) amortized | O(V) | Path compression + union by rank | Dynamic connectivity, Kruskal's MST |
| Bipartite Check | O(V + E) | O(V) | BFS 2-coloring | Odd-cycle detection, 2-partition problems |
| Bellman-Ford | O(V * E) | O(V) | Relax all edges V-1 times | Negative weights, negative cycle detection |
| Floyd-Warshall | O(V^3) | O(V^2) | All-pairs via DP on intermediate nodes | All-pairs shortest path, small dense graphs |
| Prim's MST | O((V + E) log V) | O(V) | Greedy — always pick cheapest edge from tree | Minimum spanning tree (dense graphs) |
| Kruskal's MST | O(E log E) | O(V) | Sort edges + Union-Find | Minimum spanning tree (sparse graphs) |
| Kosaraju's SCC | O(V + E) | O(V) | Two-pass DFS on original + reversed graph | Strongly connected components |
1. Graph Representations ​
Adjacency List ​
Best for sparse graphs (most real-world graphs). Space: O(V + E).
// Unweighted adjacency list
function buildAdjList(n: number, edges: number[][]): Map<number, number[]> {
const graph = new Map<number, number[]>();
for (let i = 0; i < n; i++) {
graph.set(i, []);
}
for (const [u, v] of edges) {
graph.get(u)!.push(v);
graph.get(v)!.push(u); // remove for directed graph
}
return graph;
}
// Weighted adjacency list
function buildWeightedAdjList(
n: number,
edges: [number, number, number][]
): Map<number, [number, number][]> {
const graph = new Map<number, [number, number][]>();
for (let i = 0; i < n; i++) {
graph.set(i, []);
}
for (const [u, v, w] of edges) {
graph.get(u)!.push([v, w]);
graph.get(v)!.push([u, w]); // remove for directed graph
}
return graph;
}Adjacency Matrix ​
Best for dense graphs or when you need O(1) edge lookup. Space: O(V^2).
function buildAdjMatrix(n: number, edges: number[][]): number[][] {
const matrix: number[][] = Array.from({ length: n }, () => Array(n).fill(0));
for (const [u, v] of edges) {
matrix[u][v] = 1;
matrix[v][u] = 1; // remove for directed graph
}
return matrix;
}
// Weighted version — store weights instead of 1/0
function buildWeightedAdjMatrix(
n: number,
edges: [number, number, number][]
): number[][] {
const matrix: number[][] = Array.from({ length: n }, () =>
Array(n).fill(Infinity)
);
for (let i = 0; i < n; i++) matrix[i][i] = 0;
for (const [u, v, w] of edges) {
matrix[u][v] = w;
matrix[v][u] = w; // remove for directed graph
}
return matrix;
}Trade-offs ​
| Operation | Adjacency List | Adjacency Matrix |
|---|---|---|
| Space | O(V + E) | O(V^2) |
| Check if edge exists | O(degree) | O(1) |
| Iterate neighbors | O(degree) | O(V) |
| Add edge | O(1) | O(1) |
| Best for | Sparse graphs, most problems | Dense graphs, Floyd-Warshall |
Rule of thumb: Use adjacency list unless you need constant-time edge queries or E is close to V^2.
2. BFS (Breadth-First Search) ​
BFS explores level by level. It naturally finds the shortest path in an unweighted graph.
Template ​
function bfs(graph: Map<number, number[]>, start: number): number[] {
const visited = new Set<number>([start]);
const queue: number[] = [start];
const order: number[] = [];
while (queue.length > 0) {
const node = queue.shift()!;
order.push(node);
for (const neighbor of graph.get(node) || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return order;
}BFS with Distance Tracking ​
function bfsWithDistance(
graph: Map<number, number[]>,
start: number
): Map<number, number> {
const dist = new Map<number, number>([[start, 0]]);
const queue: number[] = [start];
while (queue.length > 0) {
const node = queue.shift()!;
const currentDist = dist.get(node)!;
for (const neighbor of graph.get(node) || []) {
if (!dist.has(neighbor)) {
dist.set(neighbor, currentDist + 1);
queue.push(neighbor);
}
}
}
return dist;
}Multi-Source BFS ​
Used when you have multiple starting points (e.g., "rotting oranges", "walls and gates").
function multiSourceBfs(
grid: number[][],
sources: [number, number][]
): number[][] {
const rows = grid.length;
const cols = grid[0].length;
const dist: number[][] = Array.from({ length: rows }, () =>
Array(cols).fill(-1)
);
const queue: [number, number][] = [];
// Enqueue all sources at distance 0
for (const [r, c] of sources) {
dist[r][c] = 0;
queue.push([r, c]);
}
const dirs = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
let idx = 0;
while (idx < queue.length) {
const [r, c] = queue[idx++];
for (const [dr, dc] of dirs) {
const nr = r + dr;
const nc = c + dc;
if (
nr >= 0 &&
nr < rows &&
nc >= 0 &&
nc < cols &&
dist[nr][nc] === -1
) {
dist[nr][nc] = dist[r][c] + 1;
queue.push([nr, nc]);
}
}
}
return dist;
}When to Use BFS ​
- Shortest path in an unweighted graph
- Level-order traversal (trees or graphs)
- Multi-source shortest distance
- Anything that asks "minimum number of steps/moves"
3. DFS (Depth-First Search) ​
DFS goes as deep as possible before backtracking. Two flavors: recursive and iterative.
Recursive Template ​
function dfsRecursive(graph: Map<number, number[]>, start: number): number[] {
const visited = new Set<number>();
const order: number[] = [];
function dfs(node: number): void {
visited.add(node);
order.push(node);
for (const neighbor of graph.get(node) || []) {
if (!visited.has(neighbor)) {
dfs(neighbor);
}
}
}
dfs(start);
return order;
}Iterative Template ​
function dfsIterative(graph: Map<number, number[]>, start: number): number[] {
const visited = new Set<number>();
const stack: number[] = [start];
const order: number[] = [];
while (stack.length > 0) {
const node = stack.pop()!;
if (visited.has(node)) continue;
visited.add(node);
order.push(node);
// Push neighbors in reverse to maintain left-to-right order
const neighbors = graph.get(node) || [];
for (let i = neighbors.length - 1; i >= 0; i--) {
if (!visited.has(neighbors[i])) {
stack.push(neighbors[i]);
}
}
}
return order;
}DFS on a Grid (Flood Fill Pattern) ​
Used in "Number of Islands", "Surrounded Regions", etc.
function dfsGrid(grid: string[][], r: number, c: number): void {
const rows = grid.length;
const cols = grid[0].length;
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] !== "1") {
return;
}
grid[r][c] = "0"; // mark visited
dfsGrid(grid, r + 1, c);
dfsGrid(grid, r - 1, c);
dfsGrid(grid, r, c + 1);
dfsGrid(grid, r, c - 1);
}When to Use DFS ​
- Path finding / path existence
- Cycle detection
- Connected components
- Topological sort
- Backtracking (permutations, combinations)
- Anything that asks "does a path exist" or "find all paths"
4. Topological Sort ​
Linear ordering of vertices such that for every directed edge u -> v, u comes before v. Only works on DAGs (Directed Acyclic Graphs). If a cycle exists, topological sort is impossible.
Kahn's Algorithm (BFS-Based, In-Degree) ​
This is the most intuitive approach. Process nodes with in-degree 0, then reduce in-degrees.
function topologicalSortKahn(
numNodes: number,
edges: number[][] // [prerequisite, dependent]
): number[] {
const graph = new Map<number, number[]>();
const inDegree = new Array(numNodes).fill(0);
for (let i = 0; i < numNodes; i++) graph.set(i, []);
for (const [u, v] of edges) {
graph.get(u)!.push(v);
inDegree[v]++;
}
// Start with all nodes that have no prerequisites
const queue: number[] = [];
for (let i = 0; i < numNodes; i++) {
if (inDegree[i] === 0) queue.push(i);
}
const order: number[] = [];
while (queue.length > 0) {
const node = queue.shift()!;
order.push(node);
for (const neighbor of graph.get(node)!) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) {
queue.push(neighbor);
}
}
}
// If order doesn't include all nodes, there's a cycle
if (order.length !== numNodes) {
return []; // cycle detected — no valid topological order
}
return order;
}DFS-Based Topological Sort ​
Post-order DFS: add node to result after processing all descendants, then reverse.
function topologicalSortDFS(
numNodes: number,
edges: number[][]
): number[] {
const graph = new Map<number, number[]>();
for (let i = 0; i < numNodes; i++) graph.set(i, []);
for (const [u, v] of edges) {
graph.get(u)!.push(v);
}
const visited = new Set<number>();
const stack: number[] = [];
let hasCycle = false;
const visiting = new Set<number>(); // for cycle detection
function dfs(node: number): void {
if (hasCycle) return;
visiting.add(node);
visited.add(node);
for (const neighbor of graph.get(node)!) {
if (visiting.has(neighbor)) {
hasCycle = true;
return;
}
if (!visited.has(neighbor)) {
dfs(neighbor);
}
}
visiting.delete(node);
stack.push(node); // post-order
}
for (let i = 0; i < numNodes; i++) {
if (!visited.has(i)) {
dfs(i);
}
}
if (hasCycle) return [];
return stack.reverse();
}When to Use ​
- Course scheduling: Course Schedule I (can you finish?) and II (in what order?)
- Build systems: compile dependencies in the right order
- Task scheduling: any problem with prerequisite relationships
- Cycle detection in directed graphs (if topo sort fails, cycle exists)
5. Cycle Detection ​
Directed Graph: 3-Color Method (WHITE / GRAY / BLACK) ​
- WHITE (0): unvisited
- GRAY (1): in current DFS path (on the recursion stack)
- BLACK (2): fully processed
A cycle exists if we encounter a GRAY node during DFS (back edge).
enum Color {
WHITE = 0,
GRAY = 1,
BLACK = 2,
}
function hasCycleDirected(
numNodes: number,
edges: number[][]
): boolean {
const graph = new Map<number, number[]>();
for (let i = 0; i < numNodes; i++) graph.set(i, []);
for (const [u, v] of edges) {
graph.get(u)!.push(v);
}
const color: Color[] = new Array(numNodes).fill(Color.WHITE);
function dfs(node: number): boolean {
color[node] = Color.GRAY;
for (const neighbor of graph.get(node)!) {
if (color[neighbor] === Color.GRAY) {
return true; // back edge — cycle found
}
if (color[neighbor] === Color.WHITE && dfs(neighbor)) {
return true;
}
}
color[node] = Color.BLACK;
return false;
}
for (let i = 0; i < numNodes; i++) {
if (color[i] === Color.WHITE && dfs(i)) {
return true;
}
}
return false;
}Undirected Graph: Parent Tracking ​
In undirected graphs, we track the parent to avoid counting the edge we came from as a cycle.
function hasCycleUndirected(
numNodes: number,
edges: number[][]
): boolean {
const graph = new Map<number, number[]>();
for (let i = 0; i < numNodes; i++) graph.set(i, []);
for (const [u, v] of edges) {
graph.get(u)!.push(v);
graph.get(v)!.push(u);
}
const visited = new Set<number>();
function dfs(node: number, parent: number): boolean {
visited.add(node);
for (const neighbor of graph.get(node)!) {
if (!visited.has(neighbor)) {
if (dfs(neighbor, node)) return true;
} else if (neighbor !== parent) {
return true; // visited neighbor that isn't our parent — cycle
}
}
return false;
}
for (let i = 0; i < numNodes; i++) {
if (!visited.has(i) && dfs(i, -1)) {
return true;
}
}
return false;
}Quick Comparison ​
| Directed | Undirected | |
|---|---|---|
| Method | 3-color (WHITE/GRAY/BLACK) | Parent tracking |
| Cycle indicator | Back edge to GRAY node | Edge to visited non-parent |
| Also detects via | Topological sort failure | Union-Find |
6. Dijkstra's Algorithm ​
Finds the shortest path from a source to all other nodes in a weighted graph with non-negative weights.
Uses a min-heap (priority queue) for greedy selection of the next closest node.
Min-Heap Helper ​
JavaScript doesn't have a built-in heap, so here is a minimal implementation for interviews.
class MinHeap<T> {
private heap: T[] = [];
private compareFn: (a: T, b: T) => number;
constructor(compareFn: (a: T, b: T) => number) {
this.compareFn = compareFn;
}
get size(): number {
return this.heap.length;
}
push(val: T): void {
this.heap.push(val);
this.bubbleUp(this.heap.length - 1);
}
pop(): T | undefined {
if (this.heap.length === 0) return undefined;
const top = this.heap[0];
const last = this.heap.pop()!;
if (this.heap.length > 0) {
this.heap[0] = last;
this.sinkDown(0);
}
return top;
}
private bubbleUp(i: number): void {
while (i > 0) {
const parent = (i - 1) >> 1;
if (this.compareFn(this.heap[i], this.heap[parent]) < 0) {
[this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]];
i = parent;
} else {
break;
}
}
}
private sinkDown(i: number): void {
const n = this.heap.length;
while (true) {
let smallest = i;
const left = 2 * i + 1;
const right = 2 * i + 2;
if (left < n && this.compareFn(this.heap[left], this.heap[smallest]) < 0) {
smallest = left;
}
if (right < n && this.compareFn(this.heap[right], this.heap[smallest]) < 0) {
smallest = right;
}
if (smallest !== i) {
[this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
i = smallest;
} else {
break;
}
}
}
}Dijkstra Implementation ​
function dijkstra(
graph: Map<number, [number, number][]>, // node -> [neighbor, weight]
source: number,
n: number
): number[] {
const dist = new Array(n).fill(Infinity);
dist[source] = 0;
// Min-heap of [distance, node]
const heap = new MinHeap<[number, number]>((a, b) => a[0] - b[0]);
heap.push([0, source]);
while (heap.size > 0) {
const [d, u] = heap.pop()!;
// Skip if we've already found a shorter path
if (d > dist[u]) continue;
for (const [v, w] of graph.get(u) || []) {
const newDist = dist[u] + w;
if (newDist < dist[v]) {
dist[v] = newDist;
heap.push([newDist, v]);
}
}
}
return dist;
}Example Usage: Network Delay Time (LC 743) ​
function networkDelayTime(times: number[][], n: number, k: number): number {
const graph = new Map<number, [number, number][]>();
for (let i = 1; i <= n; i++) graph.set(i, []);
for (const [u, v, w] of times) {
graph.get(u)!.push([v, w]);
}
const dist = new Array(n + 1).fill(Infinity);
dist[k] = 0;
const heap = new MinHeap<[number, number]>((a, b) => a[0] - b[0]);
heap.push([0, k]);
while (heap.size > 0) {
const [d, u] = heap.pop()!;
if (d > dist[u]) continue;
for (const [v, w] of graph.get(u)!) {
const newDist = dist[u] + w;
if (newDist < dist[v]) {
dist[v] = newDist;
heap.push([newDist, v]);
}
}
}
let maxDist = 0;
for (let i = 1; i <= n; i++) {
if (dist[i] === Infinity) return -1;
maxDist = Math.max(maxDist, dist[i]);
}
return maxDist;
}Key Points ​
- Time: O((V + E) log V) with a binary heap
- Does NOT work with negative weights (use Bellman-Ford for that)
- The "skip if d > dist[u]" line is critical -- it avoids reprocessing stale entries
- In an interview, mention that JS lacks a native heap and you'd normally import one
7. Connected Components / Union-Find ​
Union-Find (Disjoint Set Union) with Path Compression + Union by Rank ​
This is the gold standard for dynamic connectivity problems.
class UnionFind {
parent: number[];
rank: number[];
count: number; // number of connected components
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i);
this.rank = new Array(n).fill(0);
this.count = n;
}
find(x: number): number {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]); // path compression
}
return this.parent[x];
}
union(x: number, y: number): boolean {
const rootX = this.find(x);
const rootY = this.find(y);
if (rootX === rootY) return false; // already connected
// Union by rank — attach smaller tree under larger tree
if (this.rank[rootX] < this.rank[rootY]) {
this.parent[rootX] = rootY;
} else if (this.rank[rootX] > this.rank[rootY]) {
this.parent[rootY] = rootX;
} else {
this.parent[rootY] = rootX;
this.rank[rootX]++;
}
this.count--;
return true;
}
connected(x: number, y: number): boolean {
return this.find(x) === this.find(y);
}
}Example Usage: Number of Connected Components ​
function countComponents(n: number, edges: number[][]): number {
const uf = new UnionFind(n);
for (const [u, v] of edges) {
uf.union(u, v);
}
return uf.count;
}When to Use Union-Find ​
- Dynamic connectivity: "are these two nodes connected?" with incremental edge additions
- Kruskal's MST: sort edges by weight, union them greedily
- Number of islands (alternative to DFS/BFS)
- Accounts merge, redundant connection, graph valid tree
- Any time you need to merge groups and query membership efficiently
Complexity ​
find: O(alpha(n)) amortized -- practically O(1)union: O(alpha(n)) amortized- alpha(n) is the inverse Ackermann function; it grows insanely slowly (<=4 for all practical n)
8. Bipartite Check ​
A graph is bipartite if its vertices can be colored with two colors such that no two adjacent vertices share the same color. Equivalently, the graph has no odd-length cycles.
BFS Coloring Approach ​
function isBipartite(graph: number[][]): boolean {
const n = graph.length;
const color: number[] = new Array(n).fill(-1); // -1 = uncolored
for (let i = 0; i < n; i++) {
if (color[i] !== -1) continue; // already colored
// BFS from node i
const queue: number[] = [i];
color[i] = 0;
while (queue.length > 0) {
const node = queue.shift()!;
for (const neighbor of graph[node]) {
if (color[neighbor] === -1) {
color[neighbor] = 1 - color[node]; // alternate color
queue.push(neighbor);
} else if (color[neighbor] === color[node]) {
return false; // same color on adjacent nodes — not bipartite
}
}
}
}
return true;
}When to Use ​
- Is Graph Bipartite? (LC 785) -- direct application
- Possible Bipartition (LC 886) -- model dislikes as edges, check bipartiteness
- 2-coloring problems in general
- Quick test: if you find an odd cycle, the graph is NOT bipartite
9. BFS Shortest Path (Source → Destination) ​
When you need the shortest path between two specific nodes in an unweighted graph.
function shortestPath(
graph: Map<number, number[]>,
src: number,
dest: number
): number {
const visited = new Set<number>([src]);
const queue: [number, number][] = [[src, 0]]; // [node, distance]
while (queue.length > 0) {
const [node, distance] = queue.shift()!;
if (node === dest) return distance;
for (const neighbor of graph.get(node) || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push([neighbor, distance + 1]);
}
}
}
return -1; // unreachable
}10. Shortest Path in Weighted DAG (Topological Sort Method) ​
For DAGs, topological sort + relaxation is faster than Dijkstra — O(V + E) vs O((V+E) log V).
function shortestPathDAG(
n: number,
edges: [number, number, number][], // [from, to, weight]
source: number
): (number | null)[] {
// Build weighted adjacency list
const graph = new Map<number, [number, number][]>();
for (let i = 0; i < n; i++) graph.set(i, []);
for (const [u, v, w] of edges) {
graph.get(u)!.push([v, w]);
}
// Step 1: Topological sort via DFS
const visited = new Set<number>();
const stack: number[] = [];
function topoSort(node: number): void {
visited.add(node);
for (const [neighbor] of graph.get(node)!) {
if (!visited.has(neighbor)) topoSort(neighbor);
}
stack.push(node); // post-order
}
for (let i = 0; i < n; i++) {
if (!visited.has(i)) topoSort(i);
}
// Step 2: Relax edges in topological order
const dist: (number | null)[] = new Array(n).fill(null);
dist[source] = 0;
while (stack.length > 0) {
const node = stack.pop()!;
if (dist[node] === null) continue;
for (const [neighbor, weight] of graph.get(node)!) {
const newDist = dist[node]! + weight;
if (dist[neighbor] === null || newDist < dist[neighbor]!) {
dist[neighbor] = newDist;
}
}
}
return dist;
}When to Use ​
- Shortest path in a DAG (build system dependencies with costs, critical path)
- Faster than Dijkstra when you know the graph is acyclic
11. Bellman-Ford Algorithm ​
Finds shortest path from a source, handles negative weights, and detects negative cycles.
Relaxes all edges V-1 times. If any edge can still be relaxed after that, a negative cycle exists.
function bellmanFord(
n: number,
edges: [number, number, number][], // [from, to, weight]
source: number
): { dist: number[]; hasNegativeCycle: boolean } {
const dist = new Array(n).fill(Infinity);
dist[source] = 0;
// Relax all edges V-1 times
for (let i = 0; i < n - 1; i++) {
for (const [u, v, w] of edges) {
if (dist[u] !== Infinity && dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
}
}
}
// Check for negative cycles (one more relaxation pass)
for (const [u, v, w] of edges) {
if (dist[u] !== Infinity && dist[u] + w < dist[v]) {
return { dist, hasNegativeCycle: true };
}
}
return { dist, hasNegativeCycle: false };
}Key Points ​
- Time: O(V * E) — slower than Dijkstra but handles negative weights
- If
hasNegativeCycleis true, shortest paths are undefined (can decrease infinitely) - Use when the graph has negative edge weights (currency exchange, arbitrage detection)
- Dijkstra is preferred when all weights are non-negative
When to Use ​
- Negative edge weights
- Detecting negative cycles (arbitrage in currency exchange)
- When graph is small and Dijkstra's heap overhead isn't worth it
12. Floyd-Warshall Algorithm ​
Finds shortest paths between all pairs of vertices. DP approach: "can going through node k improve the path from i to j?"
function floydWarshall(n: number, edges: [number, number, number][]): number[][] {
const INF = Infinity;
// Initialize distance matrix
const dist: number[][] = Array.from({ length: n }, (_, i) =>
Array.from({ length: n }, (_, j) => (i === j ? 0 : INF))
);
// Fill known edges
for (const [u, v, w] of edges) {
dist[u][v] = w;
// dist[v][u] = w; // uncomment for undirected
}
// DP: try every intermediate node k
for (let k = 0; k < n; k++) {
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
// Optional: detect negative cycles (dist[i][i] < 0)
for (let i = 0; i < n; i++) {
if (dist[i][i] < 0) {
// Negative cycle exists involving node i
}
}
return dist;
}Key Points ​
- Time: O(V^3), Space: O(V^2) — only practical for small graphs (V < 500)
- Works with negative weights (but not negative cycles)
- The
kloop must be the outermost — this is the most common interview mistake - Best for: dense graphs where you need all-pairs distances
When to Use ​
- "Find the city with smallest number of neighbors at a threshold distance" (LC 1334)
- All-pairs shortest path on small graphs
- Transitive closure of a graph
13. Minimum Spanning Tree ​
Prim's Algorithm ​
Greedy approach: start from any node, always add the cheapest edge connecting the tree to a non-tree node. Works like Dijkstra but tracks edge weight instead of total distance.
function primMST(
graph: Map<number, [number, number][]>, // node -> [neighbor, weight]
n: number
): number {
const visited = new Array(n).fill(false);
// Min-heap of [weight, node]
const heap = new MinHeap<[number, number]>((a, b) => a[0] - b[0]);
heap.push([0, 0]); // start from node 0
let totalCost = 0;
while (heap.size > 0) {
const [weight, node] = heap.pop()!;
if (visited[node]) continue;
visited[node] = true;
totalCost += weight;
for (const [neighbor, w] of graph.get(node) || []) {
if (!visited[neighbor]) {
heap.push([w, neighbor]);
}
}
}
return totalCost;
}Kruskal's Algorithm ​
Sort all edges by weight, greedily add edges that don't create a cycle (using Union-Find).
function kruskalMST(
n: number,
edges: [number, number, number][] // [from, to, weight]
): number {
// Sort edges by weight ascending
edges.sort((a, b) => a[2] - b[2]);
const uf = new UnionFind(n);
let totalCost = 0;
let edgesUsed = 0;
for (const [u, v, w] of edges) {
if (uf.union(u, v)) {
totalCost += w;
edgesUsed++;
if (edgesUsed === n - 1) break; // MST complete
}
}
return totalCost;
}Prim's vs Kruskal's ​
| Prim's | Kruskal's | |
|---|---|---|
| Approach | Grow tree from a node | Sort edges globally |
| Data structure | Min-heap | Union-Find |
| Time | O((V + E) log V) | O(E log E) |
| Better for | Dense graphs (E ≈ V^2) | Sparse graphs (E ≈ V) |
| Need all edges upfront? | No (can stream) | Yes (must sort) |
When to Use ​
- "Minimum cost to connect all points" (LC 1584)
- "Connecting cities with minimum cost" (LC 1135)
- Network design, cable laying, circuit wiring
14. Kosaraju's Algorithm (Strongly Connected Components) ​
A strongly connected component (SCC) is a maximal set of vertices where every vertex is reachable from every other vertex. Only applies to directed graphs.
Two-pass algorithm:
- Pass 1: DFS on original graph, push nodes to stack by finish time
- Pass 2: Build reversed graph, DFS in stack order — each DFS tree is one SCC
function kosaraju(
n: number,
adj: number[][] // adj[i] = neighbors of node i
): number {
const visited = new Array(n).fill(false);
const stack: number[] = [];
// Pass 1: DFS on original graph, record finish order
function dfs1(node: number): void {
visited[node] = true;
for (const neighbor of adj[node]) {
if (!visited[neighbor]) dfs1(neighbor);
}
stack.push(node); // push after all descendants processed
}
for (let i = 0; i < n; i++) {
if (!visited[i]) dfs1(i);
}
// Build reversed graph
const adjReversed: number[][] = Array.from({ length: n }, () => []);
for (let u = 0; u < n; u++) {
for (const v of adj[u]) {
adjReversed[v].push(u);
}
}
// Pass 2: DFS on reversed graph in stack order
visited.fill(false);
let sccCount = 0;
function dfs2(node: number): void {
visited[node] = true;
for (const neighbor of adjReversed[node]) {
if (!visited[neighbor]) dfs2(neighbor);
}
}
while (stack.length > 0) {
const node = stack.pop()!;
if (!visited[node]) {
dfs2(node);
sccCount++;
}
}
return sccCount;
}Key Points ​
- Time: O(V + E) — two DFS passes
- Why it works: finishing order from Pass 1 ensures that in Pass 2 (on reversed graph), DFS can't "escape" the current SCC
- Use when you need to find groups of mutually reachable nodes (social network clusters, dependency groups)
15. Grid Pattern: Rotting Oranges (Multi-Source BFS) ​
Classic multi-source BFS problem — all rotten oranges spread simultaneously.
function orangesRotting(grid: number[][]): number {
const rows = grid.length;
const cols = grid[0].length;
const queue: [number, number][] = [];
let fresh = 0;
// Find all rotten oranges and count fresh ones
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === 2) queue.push([r, c]);
if (grid[r][c] === 1) fresh++;
}
}
if (fresh === 0) return 0;
const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]];
let time = 0;
let idx = 0;
while (idx < queue.length && fresh > 0) {
const size = queue.length - idx; // current level size
for (let i = 0; i < size; i++) {
const [r, c] = queue[idx++];
for (const [dr, dc] of dirs) {
const nr = r + dr;
const nc = c + dc;
if (
nr >= 0 && nr < rows &&
nc >= 0 && nc < cols &&
grid[nr][nc] === 1
) {
grid[nr][nc] = 2;
queue.push([nr, nc]);
fresh--;
}
}
}
time++;
}
return fresh === 0 ? time : -1;
}Key Insight ​
- Multi-source BFS = add ALL starting points to the queue at level 0, then expand level by level
- Each level = 1 unit of time
- This pattern also applies to: Walls and Gates (LC 286), Shortest Bridge (LC 934)
16. Shortest Path Algorithm Decision Guide ​
| Scenario | Algorithm | Why |
|---|---|---|
| Unweighted graph | BFS | O(V + E), simplest |
| Weighted, non-negative | Dijkstra | O((V+E) log V), greedy + heap |
| Weighted DAG | Topo Sort + Relax | O(V + E), fastest for DAGs |
| Negative weights | Bellman-Ford | O(V*E), handles negatives |
| All-pairs, small graph | Floyd-Warshall | O(V^3), simple DP |
| All-pairs, sparse graph | Run Dijkstra from each node | O(V*(V+E) log V) |
17. Practice Problems ​
| Problem | LeetCode # | Difficulty | Key Technique | Quick Approach |
|---|---|---|---|---|
| Number of Islands | 200 | Medium | BFS/DFS flood fill | Iterate grid, DFS/BFS to mark connected '1's as visited |
| Rotting Oranges | 994 | Medium | Multi-source BFS | Enqueue all rotten oranges, BFS level-by-level, count time |
| Course Schedule | 207 | Medium | Cycle detection (directed) | Build directed graph from prerequisites, detect cycle via topo sort or 3-color DFS |
| Course Schedule II | 210 | Medium | Topological sort | Kahn's algorithm; return empty if cycle detected (order.length < numCourses) |
| Clone Graph | 133 | Medium | BFS/DFS + hashmap | BFS/DFS traversal; use Map<Node, Node> to track old-to-new mapping |
| Network Delay Time | 743 | Medium | Dijkstra | Build weighted graph, run Dijkstra from source, return max distance (or -1 if unreachable) |
| Is Graph Bipartite? | 785 | Medium | BFS coloring | BFS from each uncolored node, alternate 0/1 colors, fail if adjacent nodes share color |
| Word Ladder | 127 | Hard | BFS shortest path | Each word is a node, edges connect words differing by 1 char; BFS from begin to end word |
| Pacific Atlantic Water Flow | 417 | Medium | Multi-source DFS | DFS inward from Pacific border and Atlantic border separately; intersect reachable sets |
| Surrounded Regions | 130 | Medium | Boundary DFS | DFS from border 'O's to mark safe cells; flip remaining 'O's to 'X' |
| Min Cost to Connect All Points | 1584 | Medium | Prim's / Kruskal's MST | Build complete graph with Manhattan distances, run MST algorithm |
| City With Smallest Number of Neighbors | 1334 | Medium | Floyd-Warshall / Dijkstra | All-pairs shortest path, count neighbors within threshold per city |
| Cheapest Flights Within K Stops | 787 | Medium | Bellman-Ford (modified) | Run K+1 relaxation passes instead of V-1; tracks hop count |
| Number of Connected Components | 323 | Medium | Union-Find / DFS | Union all edges, return uf.count |
Problem-by-Problem Notes ​
200 - Number of Islands: Classic flood fill. Iterate every cell; when you hit a '1', DFS/BFS to mark the entire island, increment count. O(m * n) time.
994 - Rotting Oranges: Multi-source BFS. Enqueue all rotten oranges at time 0, BFS level-by-level. Each level = 1 minute. Track fresh count; if fresh > 0 after BFS exhausts, return -1.
207 - Course Schedule: Model as directed graph (prerequisite -> course). If there's a cycle, you can't finish all courses. Use Kahn's: if topo sort doesn't include all nodes, return false.
210 - Course Schedule II: Same as 207 but return the actual ordering. Kahn's gives you the order directly; return empty array if cycle detected.
133 - Clone Graph: BFS or DFS. Maintain a Map<Node, ClonedNode>. For each node, clone it, then connect its neighbors (cloning them if not yet cloned).
743 - Network Delay Time: Textbook Dijkstra. Build weighted adjacency list, run Dijkstra from node k, return the maximum distance. If any node is unreachable (Infinity), return -1.
785 - Is Graph Bipartite?: BFS with 2-coloring. Try to color each component. If any edge connects same-colored nodes, return false.
127 - Word Ladder: BFS where each word is a node. For efficiency, generate all possible 1-char transformations and check against a word set rather than checking all pairs.
417 - Pacific Atlantic Water Flow: Reverse the problem. Instead of flowing from cell to ocean, DFS from ocean borders inward (to cells with height >= current). A cell that reaches both oceans is in the answer.
130 - Surrounded Regions: Boundary-first approach. DFS/BFS from every 'O' on the border to mark it safe. After that, any remaining 'O' is surrounded and should be flipped to 'X'.
1584 - Min Cost to Connect All Points: Build a complete graph with Manhattan distances. Run Prim's (start from any point, greedily add cheapest edge) or Kruskal's (sort all edges, Union-Find).
1334 - City With Smallest Number of Neighbors at a Threshold: Floyd-Warshall for all-pairs shortest paths. For each city count neighbors with dist[i][j] <= threshold. Return city with smallest count (highest index on tie).
787 - Cheapest Flights Within K Stops: Modified Bellman-Ford — run only K+1 relaxation passes. Use a copy of the dist array each round to avoid cascading relaxations within a single pass.
323 - Number of Connected Components: Union-Find or DFS. With Union-Find: union all edges, return uf.count. With DFS: count number of DFS trees when iterating all nodes.