Graphs — The Complete Visual Reference

Graph fundamentals, representations, BFS/DFS, shortest paths, MSTs, Union-Find, topological sort — visual diagrams, complexity tags & 35 algorithm patterns
What Is a Graph?
Simple: Directed: Weighted: A---B A → B A -5- B | /| ↑ ↓ | / | | / | | ↓ 3 2 4 C---D D ← C C -7- D G = (V, E) V = vertices, E = edges Undirected: {u, v} both ways Directed: (u, v) one way only
TypeDescriptionExample
UndirectedEdges have no directionFriendships, roads
Directed (digraph)Edges have direction (u → v)Web links, dependencies
WeightedEdges have numerical costsRoad distances, latency
DAGDirected Acyclic GraphTask scheduling, builds
BipartiteVertices split into two setsMatching problems
CompleteEvery vertex connects to every otherKn
Sparse|E| ≈ |V|Road networks
Dense|E| ≈ |V|²Small social groups
TreeConnected acyclic undirected graphFile systems
📖
Graph Terminology
TermDefinition
Vertex / NodeAn entity in the graph
EdgeA connection between two vertices
AdjacentTwo vertices connected by an edge
DegreeNumber of edges connected to a vertex
In-degree(Directed) edges pointing TO a vertex
Out-degree(Directed) edges pointing FROM a vertex
PathSequence of vertices connected by edges
CyclePath that starts and ends at the same vertex
ConnectedPath exists between every pair of vertices
Strongly connected(Directed) path exists in BOTH directions
ComponentMaximal connected subgraph
Spanning treeTree including all vertices with min edges
BridgeEdge whose removal disconnects the graph
Articulation pointVertex whose removal disconnects the graph
Graph Representations
Adjacency List (most common for sparse graphs): 0: [1, 2] Each vertex stores its neighbors 1: [0, 2, 3] 2: [0, 1, 3] 3: [1, 2] Adjacency Matrix: 0 1 2 3 0 [ 0, 1, 1, 0 ] matrix[i][j] = 1 if edge exists 1 [ 1, 0, 1, 1 ] 2 [ 1, 1, 0, 1 ] 3 [ 0, 1, 1, 0 ] Edge List: [(0,1), (0,2), (1,2), (1,3), (2,3)]
// Adjacency List — unweighted val graph: Map<Int, List<Int>> = mapOf( 0 to listOf(1, 2), 1 to listOf(0, 2, 3), 2 to listOf(0, 1, 3), 3 to listOf(1, 2) ) // Adjacency List — weighted val weighted: Map<Int, List<Pair<Int, Int>>> = mapOf( 0 to listOf(1 to 5, 2 to 3), 1 to listOf(0 to 5, 2 to 2, 3 to 4) )
FeatureAdj ListAdj MatrixEdge List
SpaceO(V + E)O(V²)O(E)
Check edge (u,v)O(deg(u))O(1)O(E)
All neighbors of uO(deg(u))O(V)O(E)
Best forSparse graphsDense graphsEdge-centric algos
Typical useBFS, DFS, DijkstraFloyd-WarshallKruskal, Bellman-Ford
Building Graphs in Kotlin
// From an edge list fun buildGraph(n: Int, edges: List<IntArray>, undirected: Boolean = true): Array<MutableList<Int>> { val graph = Array(n) { mutableListOf<Int>() } for ((u, v) in edges) { graph[u].add(v) if (undirected) graph[v].add(u) } return graph } // Weighted graph fun buildWeightedGraph(n: Int, edges: Array<IntArray>): Array<MutableList<Pair<Int, Int>>> { val graph = Array(n) { mutableListOf<Pair<Int, Int>>() } for (e in edges) { graph[e[0]].add(e[1] to e[2]) } return graph } // Grid as implicit graph (4-directional) val dirs = listOf(-1 to 0, 1 to 0, 0 to -1, 0 to 1) fun neighbors(r: Int, c: Int, rows: Int, cols: Int): List<Pair<Int, Int>> = dirs.mapNotNull { (dr, dc) -> val nr = r + dr; val nc = c + dc if (nr in 0 until rows && nc in 0 until cols) nr to nc else null }
BFS — Breadth-First Search
O(V+E) O(V)
Layer-by-layer exploration using a queue: 0 Level 0 / \ 1 2 Level 1 / \ \ 3 4 5 Level 2 Visit order: 0, 1, 2, 3, 4, 5 Key: visits ALL nodes at distance d before any at d+1
// Basic BFS fun bfs(graph: Map<Int, List<Int>>, start: Int): List<Int> { val visited = mutableSetOf(start) val queue = ArrayDeque<Int>() val order = mutableListOf<Int>() queue.addLast(start) while (queue.isNotEmpty()) { val node = queue.removeFirst() order.add(node) for (neighbor in graph[node].orEmpty()) { if (visited.add(neighbor)) queue.addLast(neighbor) } } return order } // BFS with shortest distances fun bfsDistance(graph: Map<Int, List<Int>>, start: Int): Map<Int, Int> { val dist = mutableMapOf(start to 0) val queue = ArrayDeque<Int>() queue.addLast(start) while (queue.isNotEmpty()) { val node = queue.removeFirst() for (neighbor in graph[node].orEmpty()) { if (neighbor !in dist) { dist[neighbor] = dist[node]!! + 1 queue.addLast(neighbor) } } } return dist }
When to Use
bfs shortest pathWhy: BFS naturally finds shortest paths because it explores layer by layer. Each level = one more edge. level-by-level processingWhy: Queue processes one complete layer before the next. Perfect for tree level-order, minimum moves. nearest target searchWhy: BFS finds the closest target first. Stop as soon as target is dequeued. multi-source bfsWhy: Start from multiple sources simultaneously. Classic: rotten oranges, 01-matrix. bfs traversalWhy: Visit all reachable nodes in breadth-first order. Foundation for shortest path and level-order processing.
▶ Visualize
DFS — Depth-First Search
O(V+E) O(V)
Go as deep as possible, then backtrack: Visit order: 0 → 1 → 3 → 4 → 2 → 5 Edge classification (directed): Tree edge: to unvisited vertex Back edge: to ancestor → CYCLE! Forward edge: to descendant (already visited) Cross edge: to different subtree Three-color detection: WHITE(0) = unvisited | GRAY(1) = in progress | BLACK(2) = done Edge to GRAY node = back edge = cycle
// Recursive DFS fun dfs(graph: Map<Int, List<Int>>, start: Int): List<Int> { val visited = mutableSetOf<Int>() val order = mutableListOf<Int>() fun explore(node: Int) { if (!visited.add(node)) return order.add(node) for (neighbor in graph[node].orEmpty()) explore(neighbor) } explore(start) return order } // Iterative DFS (explicit stack) fun dfsIterative(graph: Map<Int, List<Int>>, start: Int): List<Int> { val visited = mutableSetOf<Int>() val stack = ArrayDeque<Int>() val order = mutableListOf<Int>() stack.addLast(start) while (stack.isNotEmpty()) { val node = stack.removeLast() if (!visited.add(node)) continue order.add(node) for (neighbor in graph[node].orEmpty()) { if (neighbor !in visited) stack.addLast(neighbor) } } return order }
When to Use
cycle detection (directed)Why: Back edges in DFS tree indicate cycles. Use three-color for directed graphs. topological sortWhy: DFS reverse postorder gives topological ordering of a DAG. connected componentsWhy: Each DFS from an unvisited vertex discovers one component. path existenceWhy: DFS explores full paths. Good for "can we reach X from Y?" backtrackingWhy: DFS naturally explores, undoes, and tries alternatives. dfs traversalWhy: Visit all reachable nodes in depth-first order. Foundation for cycle detection, topological sort, and connected components.
▶ Visualize
Connected Components
O(V+E) O(V)
// BFS/DFS: iterate all vertices, start new traversal from each unvisited fun connectedComponents(n: Int, graph: Map<Int, List<Int>>): Int { val visited = BooleanArray(n) var count = 0 for (v in 0 until n) { if (visited[v]) continue count++ val queue = ArrayDeque<Int>() queue.addLast(v); visited[v] = true while (queue.isNotEmpty()) { val node = queue.removeFirst() for (nb in graph[node].orEmpty()) if (!visited[nb]) { visited[nb] = true; queue.addLast(nb) } } } return count }
When to Use
connected componentsWhy: Each BFS/DFS from an unvisited vertex = one component. O(V+E). dynamic connectivityWhy: Use Union-Find when edges arrive one at a time. O(E × α(V)) ≈ O(E). cycle detectionWhy: Adding an edge between nodes in the same component creates a cycle. Union-Find detects this in near O(1) per edge.
▶ Visualize
Cycle Detection
O(V+E) O(V)
// Undirected: DFS — visited neighbor that's NOT parent = cycle fun hasCycleUndirected(n: Int, graph: Map<Int, List<Int>>): Boolean { val visited = BooleanArray(n) fun dfs(node: Int, parent: Int): Boolean { visited[node] = true for (nb in graph[node].orEmpty()) { if (!visited[nb]) { if (dfs(nb, node)) return true } else if (nb != parent) return true } return false } return (0 until n).any { !visited[it] && dfs(it, -1) } } // Directed: three-color DFS — edge to GRAY = back edge = cycle fun hasCycleDirected(n: Int, graph: Map<Int, List<Int>>): Boolean { val color = IntArray(n) // 0=WHITE, 1=GRAY, 2=BLACK fun dfs(node: Int): Boolean { color[node] = 1 for (nb in graph[node].orEmpty()) { if (color[nb] == 1) return true // back edge if (color[nb] == 0 && dfs(nb)) return true } color[node] = 2; return false } return (0 until n).any { color[it] == 0 && dfs(it) } }
When to Use
undirected cycle detectionWhy: DFS parent check or Union-Find. Visited non-parent neighbor = cycle. cycle detection (directed)Why: Three-color DFS (back edge to GRAY node) or Kahn's topological sort (processed < n means cycle).
▶ Visualize
Topological Sort
O(V+E) O(V)
Linear ordering where for every edge (u,v), u comes before v. Only valid for DAGs (Directed Acyclic Graphs). DAG: 5 → 0 ← 4 Topological orders: ↓ ↓ [4, 5, 0, 2, 3, 1] 2 → 3 → 1 [5, 4, 2, 0, 3, 1] ...
// DFS-based: reverse postorder fun topoSortDFS(n: Int, graph: Map<Int, List<Int>>): List<Int> { val visited = BooleanArray(n) val order = ArrayDeque<Int>() fun dfs(node: Int) { visited[node] = true for (nb in graph[node].orEmpty()) if (!visited[nb]) dfs(nb) order.addFirst(node) } for (i in 0 until n) if (!visited[i]) dfs(i) return order.toList() } // Kahn's BFS-based: remove vertices with in-degree 0 fun topoSortKahns(n: Int, graph: Map<Int, List<Int>>): List<Int> { val inDegree = IntArray(n) for (nbs in graph.values) for (v in nbs) inDegree[v]++ val queue = ArrayDeque<Int>() for (i in 0 until n) if (inDegree[i] == 0) queue.addLast(i) val order = mutableListOf<Int>() while (queue.isNotEmpty()) { val node = queue.removeFirst(); order.add(node) for (nb in graph[node].orEmpty()) { inDegree[nb]-- if (inDegree[nb] == 0) queue.addLast(nb) } } if (order.size != n) throw IllegalArgumentException("Cycle!") return order }
When to Use
task scheduling (topo sort application)Why: Course schedule, build order, prerequisites. Kahn's also detects cycles. compilation orderWhy: Modules must be compiled after their dependencies. Topological sort gives valid order.
▶ Visualize
Shortest Path — Dijkstra’s Algorithm
O((V+E) log V) O(V+E)
Greedy: always process the vertex with smallest known distance. Uses a min-heap (priority queue). Requirement: all edge weights must be NON-NEGATIVE. Negative weights? → Use Bellman-Ford Unweighted? → Use BFS (simpler, same result) All pairs? → Use Floyd-Warshall Weights 0/1 only? → Use 01-BFS (O(V+E), faster)
import java.util.PriorityQueue fun dijkstra(graph: Array<MutableList<Pair<Int, Int>>>, source: Int, n: Int): IntArray { val dist = IntArray(n) { Int.MAX_VALUE } dist[source] = 0 val pq = PriorityQueue<Pair<Int, Int>>(compareBy { it.second }) pq.add(source to 0) while (pq.isNotEmpty()) { val (u, d) = pq.poll() if (d > dist[u]) continue // stale entry for ((v, w) in graph[u]) { val nd = dist[u] + w if (nd < dist[v]) { dist[v] = nd; pq.add(v to nd) } } } return dist }
When to Use
dijkstra's shortest pathWhy: Greedy with min-heap. Process each vertex once. Standard for weighted graphs. network delay timeWhy: Find max of all shortest distances = time for signal to reach all nodes. shortest path with path reconstructionWhy: Track predecessor array during Dijkstra to reconstruct the actual shortest path, not just the distance.
▶ Visualize
Shortest Path — Bellman-Ford
O(V × E) O(V)
// Handles NEGATIVE weights. Detects negative cycles. fun bellmanFord(n: Int, edges: List<Triple<Int,Int,Int>>, src: Int): IntArray? { val dist = IntArray(n) { Int.MAX_VALUE }; dist[src] = 0 repeat(n - 1) { for ((u, v, w) in edges) if (dist[u] != Int.MAX_VALUE && dist[u] + w < dist[v]) dist[v] = dist[u] + w } // Check for negative cycles for ((u, v, w) in edges) if (dist[u] != Int.MAX_VALUE && dist[u] + w < dist[v]) return null return dist }
When to Use
negative edge weights (bellman-ford)Why: Dijkstra fails with negatives. Bellman-Ford relaxes all edges V-1 times. negative cycle detectionWhy: If V-th iteration still relaxes, a negative cycle exists. cheapest flights within k stopsWhy: Modified Bellman-Ford: relax only k+1 times, using previous iteration's distances.
▶ Visualize
Shortest Path — Floyd-Warshall (All Pairs)
O(V³) O(V²)
// dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]) fun floydWarshall(n: Int, edges: List<Triple<Int,Int,Int>>): Array<IntArray> { val INF = Int.MAX_VALUE / 2 val dist = Array(n) { i -> IntArray(n) { j -> if (i == j) 0 else INF } } for ((u, v, w) in edges) dist[u][v] = minOf(dist[u][v], w) for (k in 0 until n) for (i in 0 until n) for (j in 0 until n) if (dist[i][k] + dist[k][j] < dist[i][j]) dist[i][j] = dist[i][k] + dist[k][j] return dist }
Union-Find (Disjoint Set Union)
O(α(n)) per op O(V)
class UnionFind(n: Int) { val parent = IntArray(n) { it } val rank = IntArray(n) var components = n; private set fun find(x: Int): Int { if (parent[x] != x) parent[x] = find(parent[x]) // path compression return parent[x] } fun union(x: Int, y: Int): Boolean { val rx = find(x); val ry = find(y) if (rx == ry) return false when { rank[rx] < rank[ry] -> parent[rx] = ry rank[rx] > rank[ry] -> parent[ry] = rx else -> { parent[ry] = rx; rank[rx]++ } } components--; return true } fun connected(x: Int, y: Int) = find(x) == find(y) }
ApplicationHow Union-Find Helps
Connected componentsUnion edges, count remaining components
Cycle detection (undirected)Union fails → cycle
Kruskal’s MSTCheck if edge creates cycle
Redundant connectionEdge whose union fails is redundant
Graph valid treen-1 edges + no cycle (union never fails)
Number of islands (dynamic)Union adjacent land cells
▶ Visualize
Minimum Spanning Tree — Kruskal & Prim
O(E log E) O(V)
// Kruskal's: sort edges, greedily add cheapest non-cycle edge fun kruskal(n: Int, edges: List<Triple<Int,Int,Int>>): Int { val uf = UnionFind(n) var total = 0 for ((u, v, w) in edges.sortedBy { it.third }) { if (uf.union(u, v)) total += w } return total } // Prim's: grow tree, add cheapest frontier edge (min-heap) fun prim(n: Int, graph: Array<MutableList<Pair<Int,Int>>>): Int { val inMST = BooleanArray(n) val pq = PriorityQueue<Pair<Int,Int>>(compareBy { it.second }) pq.add(0 to 0) var total = 0 while (pq.isNotEmpty()) { val (u, w) = pq.poll() if (inMST[u]) continue inMST[u] = true; total += w for ((v, wt) in graph[u]) if (!inMST[v]) pq.add(v to wt) } return total }
FeatureKruskal’sPrim’s
StrategySort edges, add cheapestGrow tree, add cheapest frontier
Data structureUnion-FindMin-Heap
TimeO(E log E)O((V+E) log V)
Better forSparse graphs, edge listsDense graphs, adj lists
When to Use
kruskal's mstWhy: Sort edges by weight, greedily add if no cycle (Union-Find). Best for sparse graphs. prim's mstWhy: Grow MST from a starting node using a priority queue. Best for dense graphs.
▶ Visualize
Number of Islands (Grid BFS/DFS)
O(R × C) O(R × C)
fun numIslands(grid: Array<CharArray>): Int { val rows = grid.size; val cols = grid[0].size var count = 0 fun dfs(r: Int, c: Int) { if (r !in 0 until rows || c !in 0 until cols || grid[r][c] != '1') return grid[r][c] = '0' dfs(r-1,c); dfs(r+1,c); dfs(r,c-1); dfs(r,c+1) } for (r in 0 until rows) for (c in 0 until cols) if (grid[r][c] == '1') { count++; dfs(r, c) } return count }
When to Use
number of islandsWhy: Each DFS/BFS from unvisited '1' discovers one island. Mark visited to avoid re-counting. max area of islandWhy: Same DFS, but return the count of cells visited. Track maximum.
▶ Visualize
Multi-Source BFS (Rotten Oranges)
O(R × C) O(R × C)
// Start BFS from ALL sources simultaneously fun orangesRotting(grid: Array<IntArray>): Int { val rows = grid.size; val cols = grid[0].size val queue = ArrayDeque<Pair<Int,Int>>() var fresh = 0 for (r in 0 until rows) for (c in 0 until cols) when (grid[r][c]) { 2 -> queue.addLast(r to c); 1 -> fresh++ } if (fresh == 0) return 0 val dirs = listOf(-1 to 0, 1 to 0, 0 to -1, 0 to 1) var minutes = 0 while (queue.isNotEmpty()) { var rotted = false repeat(queue.size) { val (r, c) = queue.removeFirst() for ((dr, dc) in dirs) { val nr = r+dr; val nc = c+dc if (nr in 0 until rows && nc in 0 until cols && grid[nr][nc]==1) { grid[nr][nc] = 2; fresh--; queue.addLast(nr to nc); rotted = true } } } if (rotted) minutes++ } return if (fresh == 0) minutes else -1 }
Bipartite Graph Check
O(V+E) O(V)
// 2-color BFS: if neighbor has SAME color, not bipartite fun isBipartite(graph: Array<IntArray>): Boolean { val n = graph.size; val color = IntArray(n) { -1 } for (start in 0 until n) { if (color[start] != -1) continue val queue = ArrayDeque<Int>() queue.addLast(start); color[start] = 0 while (queue.isNotEmpty()) { val node = queue.removeFirst() for (nb in graph[node]) { if (color[nb] == -1) { color[nb] = 1 - color[node]; queue.addLast(nb) } else if (color[nb] == color[node]) return false } } } return true }
Strongly Connected Components (Kosaraju / Tarjan)
O(V+E) O(V+E)
SCC: maximal set of vertices where every vertex is reachable from every other. Kosaraju’s: 1. DFS on original → record finish order 2. Transpose graph (reverse all edges) 3. DFS on transposed in reverse finish order → each DFS = one SCC Tarjan’s: Single DFS pass with low-link values + stack. low[u] = min discovery index reachable from u. If low[u] == disc[u], u is root of an SCC.
// Tarjan's SCC fun tarjan(n: Int, graph: Map<Int, List<Int>>): List<List<Int>> { val disc = IntArray(n){-1}; val low = IntArray(n); val onStk = BooleanArray(n) val stk = ArrayDeque<Int>(); val sccs = mutableListOf<List<Int>>(); var t = 0 fun dfs(u: Int) { disc[u] = t; low[u] = t; t++; stk.addLast(u); onStk[u] = true for (v in graph[u].orEmpty()) { if (disc[v]==-1) { dfs(v); low[u] = minOf(low[u], low[v]) } else if (onStk[v]) low[u] = minOf(low[u], disc[v]) } if (low[u] == disc[u]) { val comp = mutableListOf<Int>() while (true) { val v = stk.removeLast(); onStk[v]=false; comp.add(v); if(v==u) break } sccs.add(comp) } } for (i in 0 until n) if (disc[i]==-1) dfs(i) return sccs }
Bridges & Articulation Points
O(V+E) O(V)
// Bridge: edge (u,v) where low[v] > disc[u] // Articulation point: root with ≥2 children, or non-root with low[v] ≥ disc[u] fun findBridges(n: Int, graph: Map<Int, List<Int>>): List<Pair<Int,Int>> { val disc = IntArray(n){-1}; val low = IntArray(n); var t = 0 val bridges = mutableListOf<Pair<Int,Int>>() fun dfs(u: Int, parent: Int) { disc[u] = t; low[u] = t; t++ for (v in graph[u].orEmpty()) { if (disc[v]==-1) { dfs(v, u); low[u] = minOf(low[u], low[v]) if (low[v] > disc[u]) bridges.add(u to v) } else if (v != parent) low[u] = minOf(low[u], disc[v]) } } for (i in 0 until n) if (disc[i]==-1) dfs(i, -1) return bridges }
A* Search
O(E log V)* O(V)
A* = Dijkstra + heuristic. f(n) = g(n) + h(n) g(n) = actual cost from start to n h(n) = estimated cost from n to target Optimal when h(n) is admissible (never overestimates). Grid heuristic: Manhattan distance = |r1−r2| + |c1−c2|
fun aStar(graph: Map<Int,List<Pair<Int,Int>>>, start: Int, target: Int, h: (Int)->Int): Int { val pq = PriorityQueue<Triple<Int,Int,Int>>(compareBy { it.first }) val g = mutableMapOf(start to 0) pq.add(Triple(h(start), 0, start)) while (pq.isNotEmpty()) { val (_, gc, node) = pq.poll() if (node == target) return gc if (gc > (g[node] ?: Int.MAX_VALUE)) continue for ((nb, w) in graph[node].orEmpty()) { val ng = gc + w if (ng < (g[nb] ?: Int.MAX_VALUE)) { g[nb] = ng; pq.add(Triple(ng+h(nb), ng, nb)) } } } return -1 }
Maximum Flow — Edmonds-Karp
O(V × E²) O(V²)
// BFS-based Ford-Fulkerson: find augmenting paths with BFS fun maxFlow(n: Int, cap: Array<IntArray>, s: Int, t: Int): Int { val res = Array(n) { i -> cap[i].copyOf() } var total = 0 while (true) { val parent = IntArray(n){-1}; parent[s] = s val queue = ArrayDeque<Pair<Int,Int>>() queue.addLast(s to Int.MAX_VALUE) while (queue.isNotEmpty()) { val (u, flow) = queue.removeFirst() for (v in 0 until n) { if (parent[v]==-1 && res[u][v]>0) { parent[v] = u; val nf = minOf(flow, res[u][v]) if (v==t) { total+=nf; var c=v; while(c!=s){ val p=parent[c]; res[p][c]-=nf; res[c][p]+=nf; c=p }; break } queue.addLast(v to nf) } } } if (parent[t]==-1) break } return total }
Shortest Path — Algorithm Comparison
AlgorithmTimeWeightsSourceBest For
BFSO(V+E)UnweightedSingleUnweighted graphs
01-BFSO(V+E)0 or 1SingleBinary-weight graphs
DijkstraO((V+E) log V)Non-negativeSingleGeneral weighted
Bellman-FordO(V × E)Any (neg ok)SingleNegative weights
Floyd-WarshallO(V³)AnyAll pairsSmall graphs
A*O(E log V)*Non-negativeSingle + targetWith good heuristic
Algorithm Complexities
AlgorithmTimeSpace
BFS / DFSO(V + E)O(V)
Topological Sort (DFS / Kahn’s)O(V + E)O(V)
Kruskal’s MSTO(E log E)O(V)
Prim’s MSTO((V+E) log V)O(V)
Union-Find (per operation)O(α(n))O(V)
Bridges / Articulation PointsO(V + E)O(V)
SCC (Kosaraju / Tarjan)O(V + E)O(V + E)
Eulerian Path (Hierholzer)O(E)O(E)
Max Flow (Edmonds-Karp)O(V × E²)O(V²)
Bipartite CheckO(V + E)O(V)
The Eight Essential Graph Patterns
PatternDescriptionKey Algorithm
BFS shortest pathShortest path in unweighted graphBFS with distance
DFS explorationTraverse, find paths, detect propertiesRecursive DFS
Topological sortOrder with dependenciesKahn’s or DFS postorder
Union-FindDynamic connectivity, components, cyclesPath compression + rank
DijkstraShortest path with non-negative weightsPriority queue
Grid as graph2D grid → implicit 4/8-connected graphBFS/DFS with directions
Multi-source BFSStart from multiple sources simultaneouslyRotten oranges pattern
Graph coloringBipartite check, 2-colorabilityBFS/DFS with alternating colors
Graph Pitfalls
// 1. Mark visited BEFORE enqueuing, not after dequeuing // WRONG: check visited on dequeue → duplicates in queue // RIGHT: visited.add(neighbor) returns false if already visited // 2. Undirected graphs: add BOTH directions for ((u, v) in edges) { graph[u]!!.add(v) graph[v]!!.add(u) // DON'T FORGET THIS } // 3. Undirected vs directed cycle detection are DIFFERENT // Undirected: back-to-parent is NOT a cycle (exclude parent) // Directed: use three-color (WHITE/GRAY/BLACK) // 4. Adjacency matrix for V=100,000 → 40 GB! Use adj list // 5. Dijkstra does NOT work with negative weights → use Bellman-Ford // 6. Handle disconnected graphs: loop over ALL vertices for (i in 0 until n) { if (!visited[i]) explore(i) // don't just start from vertex 0! } // 7. Integer overflow in shortest path // WRONG: dist[u] + w (overflows if dist[u] == MAX_VALUE) // RIGHT: check dist[u] != MAX_VALUE first // 8. Stale entries in Dijkstra's PQ val (u, d) = pq.poll() if (d > dist[u]) continue // skip stale! // 9. Grid bounds checking if (nr in 0 until rows && nc in 0 until cols) { /* safe */ } // 10. Topological sort on cyclic graph gives WRONG results silently // Use Kahn's — it detects cycles (processed < n means cycle)