The night-before sheet: spot the pattern, write the simple version, say the right thing out loud.
π Your own rules: DSA thoughts β Graph, Must Remember. Each section below links the ones that apply.
"Things and connections. If you can say βX connects to Yβ, it's a graph, even when the word graph never appears."
There are only two ways to walk a graph, and every algorithm below is one of them with a twist:
visited so no node is processed twice.Is it secretly a graph? If you can name the nodes and the edges, yes.
| Nodes | Edges | Example |
|---|---|---|
| π© grid cells | up / down / left / right | 200 Islands, 994 Oranges |
| π courses / tasks | "do A before B" (one-way) | 207, 210 |
| π€ words / lock codes | one letter / one wheel turn apart | 127, 752 |
| π₯ accounts / people | share an email / are friends | 721, 547 |
| β variables | a / b = 2 (weighted, both ways) | 399 |
| ποΈ (cell, extra info) | a move that also changes the extra info | 1293, 864, 847 |
visited) 4. Can it be disconnected? (β loop over every node)The emoji is the hook: remember the picture, and the tool comes with it. The same emojis come back in each section's π§© idea box.
| The problem says⦠| Use | LC |
|---|---|---|
| π fewest steps / minimum moves, every move costs the same | BFS | 1091, 127, 752 |
| π spreads from many places at once (rot, fire, gates) | Multi-source BFS | 994, 542, 286π |
| ποΈ count islands / regions / groups | DFS sweep or Union-Find | 200, 695, 547 |
| π deep-copy a graph | DFS + oldβnew map | 133 |
| π¨ split into 2 groups with no conflict | 2-colouring | 785, 886 |
β ratios: a/b, b/c β a/c? | Weighted DFS, multiply | 399 |
| π prerequisites, order of tasks, "can you finish?" | Topological sort (Kahn's) | 207, 210 |
| π½ order of letters from a sorted word list | Topological sort on letters | 269π |
| π merge groups / "are x and y connected?" / extra edge | Union-Find | 721, 684, 323π |
| π£οΈ shortest path, weights β₯ 0 | Dijkstra | 743, 1514 |
| β°οΈ minimise the largest step on the path | Dijkstra with max | 1631, 778 |
| βοΈ at most K stops / K edges | Bellman-Ford, K+1 rounds | 787 |
| π cheapest way to connect everything | MST (Kruskal) | 1584 |
| ποΈ state is more than a position (keys held, walls left) | BFS over (cell, extra) | 1293, 864 |
| π longest increasing path in a grid | DFS + memo (values only go up β no cycles) | 329 |
| π§³ use every edge once / itinerary | Hierholzer | 332 |
| π edge whose removal splits the graph | Bridges (low-link) | 1192 |
| π visit every node, n β€ 12 | BFS over (node, bitmask) | 847 |
π = LeetCode Premium. For 261, 269, 305, 323, 444 and 505 the tracker links a free LintCode or NeetCode copy; for 1168 it links CSES 1675 Road Reparation (same Kruskal core).
graph[u] answers it instantly.0β1 appears in both graph[0] and graph[1]).Use an adjacency list: graph[u] = the neighbours of u. It takes O(V + E) space. Use a matrix only when you need an O(1) "is there an edge?" check on a small, dense graph.
// Paste inside each grid function
const DIRS = [[-1, 0], [0, 1], [1, 0], [0, -1]]; // up, right, down, left
// Edge list β adjacency list. Create every node first so isolated nodes exist.
const graph: number[][] = Array.from({length: n}, () => []);
for(const [u, v] of edges) {
graph[u].push(v);
graph[v].push(u); // undirected = add both ways; directed = drop this line
}
// Weighted: graph[u].push([v, w])
[1, 0], and the edge goes 0 β 1. Draw two nodes before you code; getting the direction wrong gives silent wrong answers.JavaScript has no built-in heap. On LeetCode use the built-in MinPriorityQueue (that's what your solutions call). An interview doc has none, so be able to type the class below in 3 minutes: it has the same API, so your Dijkstra code runs unchanged.
// On LeetCode (built in)
const pq = new MinPriorityQueue<number[]>(x => x[0]); // ordered by x[0]
pq.enqueue([0, src]);
const [d, node] = pq.dequeue();
while(!pq.isEmpty()) { /* ... */ }
// Interview doc (no built-in): same API as above
class MinPriorityQueue<T> {
private items: T[] = [];
constructor(private key: (x: T) => number) {}
isEmpty(): boolean { return this.items.length === 0; }
enqueue(item: T): void {
const a = this.items;
a.push(item);
let i = a.length - 1;
while(i > 0) { // bubble up while smaller than the parent
const parent = Math.floor((i - 1) / 2);
if(this.key(a[parent]) <= this.key(a[i])) break;
[a[parent], a[i]] = [a[i], a[parent]];
i = parent;
}
}
dequeue(): T {
const a = this.items;
const top = a[0];
const last = a.pop()!;
if(a.length > 0) {
a[0] = last; // move the last item to the root, then sink it
let i = 0;
while(true) {
const left = 2 * i + 1;
const right = 2 * i + 2;
let smallest = i;
if(left < a.length && this.key(a[left]) < this.key(a[smallest])) smallest = left;
if(right < a.length && this.key(a[right]) < this.key(a[smallest])) smallest = right;
if(smallest === i) break;
[a[smallest], a[i]] = [a[i], a[smallest]];
i = smallest;
}
}
return top;
}
}
"Explore in rings. The first time you reach a node is the shortest way there."
Only true when every edge costs the same. Process the graph one level at a time: keep one queue array and a front index. At the start of each level take size = queue.length - front and process exactly that many. The level number is the distance. This avoids queue.shift(), which is O(n).
// π ROTTING ORANGES (multi-source BFS)
function orangesRotting(grid: number[][]): number {
const m = grid.length;
const n = grid[0].length;
let fresh = 0;
let queue: number[][] = [];
for(let i = 0; i < m; i++) {
for(let j = 0; j < n; j++) {
if(grid[i][j] === 1) fresh++;
else if(grid[i][j] === 2) queue.push([i, j]); // ALL sources start together
}
}
if(fresh === 0) return 0;
if(queue.length === 0) return -1;
let front = 0;
let time = -1; // the first pass is the starting level, so it brings time to 0
const DIRS = [[-1, 0], [0, 1], [1, 0], [0, -1]];
let visitedFresh = 0;
while(front < queue.length) {
let size = queue.length - front; // this level only
time++;
while(size--) {
const [row, col] = queue[front++];
for(const [dr, dc] of DIRS) {
const nr = dr + row;
const nc = dc + col;
if(nr >= 0 && nr < m && nc >= 0 && nc < n) {
if(grid[nr][nc] === 1) {
grid[nr][nc] = 2; // mark when ADDED, not when popped
visitedFresh++;
queue.push([nr, nc]);
}
}
}
}
}
return visitedFresh === fresh ? time : -1;
};
[[2,1,1],[1,1,0],[0,1,1]]. Each ring is one pass of the outer while, so the last level is the answer: 4. A cell gets its number when it is pushed.grid[nr][nc] = 2 before push). Otherwise the same node gets pushed by many neighbours and the queue blows up.time = -1 is safe only because the two early returns run first: with no fresh oranges, or no rotten ones, the loop would give the wrong answer.
// πͺ WORD LADDER (front/back bidirectional BFS)
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
if(beginWord === endWord) return 0;
const wordSet = new Set(wordList);
if(!wordSet.has(endWord)) return 0;
let front = new Set<string>([beginWord]);
let back = new Set<string>([endWord]);
let level = 1; // counts words, not edges
while(front.size > 0) {
if(front.size > back.size) {
[front, back] = [back, front]; // always expand the smaller side
}
const nextLevel = new Set<string>();
level++;
for(const word of front) {
for(let i = 0; i < word.length; i++) {
const originCh = word[i];
for(let c = 0; c < 26; c++) {
const newCh = String.fromCharCode(c + 97);
if(newCh === originCh) continue;
const newWord = word.slice(0, i) + newCh + word.slice(i + 1);
if(back.has(newWord)) return level; // the two sides met
if(wordSet.has(newWord)) {
wordSet.delete(newWord); // delete = mark visited
nextLevel.add(newWord);
}
}
}
}
front = nextLevel;
}
return 0;
};
| Variation | What changes |
|---|---|
| Extra state (1293 walls you can break, 864 keys) | The node becomes (r, c, extra), so seen becomes seen[r][c][extra]. Same BFS. |
| Need the path, not just the length | Store parent[next] = node when you push. Walk back from the target, then reverse. |
| 752 Open the Lock | Node = 4-digit string, 8 neighbours (each wheel Β±1). Dead ends start in seen. |
| 815 Bus Routes | BFS over routes, not stops. Map stop β routes; each bus taken = one level. |
| Edge weights are only 0 or 1 (1368) | "0-1 BFS": a 0-edge goes to the front of a deque, a 1-edge to the back. In JS, plain Dijkstra is simpler and fine. |
| Huge graph, known start and end | Bidirectional BFS: that's the 127 version above. In the interview say plain BFS first (one queue + front, like 994), then offer bidirectional as the optimisation. |
"Go as deep as you can, then back up."
Use it for anything about structure: components, copying, cycles, colouring, and searching all paths. Recursion is the cleanest; switch to an explicit stack only if depth can pass about 10,000.
// ποΈ NUMBER OF ISLANDS (DFS flood fill)
function numIslands(grid: string[][]): number {
const m = grid.length;
const n = grid[0].length;
let isLands = 0;
const DIRS = [[0, 1], [0, -1], [1, 0], [-1, 0]];
function dfs(row: number, col: number): void {
grid[row][col] = '0'; // mark visited in place
for(const [dr, dc] of DIRS) {
const nr = dr + row;
const nc = dc + col;
if(nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] === '1') {
dfs(nr, nc);
}
}
}
for(let i = 0; i < m; i++) {
for(let j = 0; j < n; j++) {
if(grid[i][j] === '1') {
isLands++; // new island found
dfs(i, j);
}
}
}
return isLands;
};
You check bounds and '1' before recursing, so dfs marks on entry and needs no early-return line.
| Problem | The twist |
|---|---|
| 695 Max Area of Island | dfs returns 1 + sum of the 4 calls; keep the max. |
| 130 Surrounded Regions Β· 417 Pacific Atlantic | Start from the border and mark what's reachable; everything else is the answer. |
| 934 Shortest Bridge | DFS to mark the first island, then multi-source BFS from all of it to the second. |
| 827 Making a Large Island | Label each island with an id and store its size. For each 0, add up the sizes of the distinct ids around it, plus 1. |
| 547 Number of Provinces | The same sweep on an adjacency matrix instead of a grid. |
// 𧬠CLONE GRAPH (DFS + map)
function cloneGraph(node: _Node | null): _Node | null {
if(!node) return node;
const cloned = new Map<_Node, _Node>(); // old node β copy; also the visited set
function dfs(original: _Node): _Node {
if(cloned.has(original)) return cloned.get(original)!;
const copy = new _Node(original.val);
cloned.set(original, copy); // save BEFORE recursing (cycles!)
for(const nei of original.neighbors) {
copy.neighbors.push(dfs(nei));
}
return copy;
}
return dfs(node);
};
// π DIRECTED CYCLE (3-state DFS)
function hasCycle(n: number, graph: number[][]): boolean {
// 0 = not visited, 1 = on the current path, 2 = fully explored
const state: number[] = new Array(n).fill(0);
function dfs(node: number): boolean { // true = no cycle through this node
state[node] = 1;
for(const next of graph[node]) {
if(state[next] === 1) return false; // back to our own path = cycle
if(state[next] === 0 && !dfs(next)) return false;
}
state[node] = 2;
return true;
}
for(let i = 0; i < n; i++) {
if(state[i] === 0 && !dfs(i)) return true;
}
return false;
}
visited set would flag the diamond too.visited check would wrongly report one. Only a node that is still in progress (on the current path) proves a cycle.Undirected cycle: use Union-Find (Β§6): an edge whose two ends are already connected closes a cycle. 261 Valid Treeπ: exactly n β 1 edges and everything connected.
// π¨ IS GRAPH BIPARTITE (DFS and BFS colouring)
function isBipartite(graph: number[][]): boolean {
const n = graph.length;
const color = Array(n).fill(-1); // -1 = not painted
function dfs(node: number, col: number): boolean {
color[node] = col;
for(const nei of graph[node]) {
if(color[nei] === -1) {
if(!dfs(nei, 1 - col)) return false;
} else if(color[nei] === col) return false; // same colour on both ends
}
return true;
}
function bfs(node: number, col: number): boolean {
color[node] = col;
const queue = [node];
let front = 0;
while(front < queue.length) {
const curr = queue[front++];
for(const nei of graph[curr]) {
if(color[nei] === -1) {
color[nei] = 1 - color[curr];
queue.push(nei);
} else if(color[nei] === color[curr]) return false;
}
}
return true;
}
for(let i = 0; i < n; i++) { // graph may be disconnected
if(color[i] === -1) {
// if(!dfs(i, 0)) return false;
if(!bfs(i, 0)) return false;
}
}
return true;
};
Your accepted submission uses bfs; dfs is the same check with recursion. 886 Possible Bipartition is the same code after building the graph from the "dislikes" pairs. A graph is bipartite exactly when it has no odd-length cycle.
// β EVALUATE DIVISION (graph + BFS, carry the product)
// a/b = 2 means edge aβb (2) and bβa (1/2).
function calcEquation(eq: string[][], vals: number[], que: string[][]): number[] {
const graph = new Map<string, [string, number][]>();
const eLen = eq.length;
for(let i = 0; i < eLen; i++) {
const [u, v] = eq[i];
const val = vals[i];
if(!graph.has(u)) graph.set(u, []);
if(!graph.has(v)) graph.set(v, []);
graph.get(u)!.push([v, val]);
graph.get(v)!.push([u, 1 / val]);
}
function bfs(source: string, target: string): number {
const visited = new Set<string>();
visited.add(source);
const queue: [string, number][] = [[source, 1]];
let front = 0;
while(front < queue.length) {
const [node, currProd] = queue[front++];
if(node === target) return currProd;
for(const [nei, neiVal] of graph.get(node) ?? []) {
if(!visited.has(nei)) {
const newProd = neiVal * currProd;
visited.add(nei);
queue.push([nei, newProd]);
}
}
}
return -1;
}
const qLen = que.length;
const ans: number[] = [];
for(let i = 0; i < qLen; i++) {
const [u, v] = que[i];
if(!graph.has(u) || !graph.has(v)) ans.push(-1);
else if(u === v) ans.push(1);
else ans.push(bfs(u, v));
}
return ans;
};
seen set). Escalation: count islands β largest island β islands added one at a time (305π, Union-Find) β change one 0 to 1 (827)."Repeatedly take a task that nothing is waiting on."
Count each node's incoming edges (indegree). Start with every node at 0. Taking a node lowers its neighbours' counts, and any that reach 0 join the queue. If you finish with fewer than n nodes, the rest are stuck in a cycle. That one check also answers 207 "can you finish?"
// π COURSE SCHEDULE II (Kahn's BFS topological sort)
function findOrder(numCourses: number, prerequisites: number[][]): number[] {
const adj: number[][] = Array.from({ length: numCourses }, () => []);
const indegree: number[] = new Array(numCourses).fill(0);
for (const [course, prereq] of prerequisites) {
adj[prereq].push(course); // taking prereq unlocks course
indegree[course]++; // course has one more prerequisite to satisfy
}
const queue: number[] = [];
for (let i = 0; i < numCourses; i++) {
if (indegree[i] === 0) queue.push(i);
}
let front = 0;
let order = [];
while (front < queue.length) {
const course = queue[front++];
order.push(course);
for (const next of adj[course]) {
indegree[next]--;
if (indegree[next] === 0) queue.push(next);
}
}
return order.length === numCourses ? order : []; // stuck before n β cycle
};
Same answer with DFS (your accepted submission): post-order, then reverse.
// DFS version: post-order, then reverse
function findOrder(numCourses: number, prerequisites: number[][]): number[] {
const adj: number[][] = Array.from({ length: numCourses }, () => []);
for (const [course, prereq] of prerequisites) {
adj[prereq].push(course);
}
let order = [];
// 0 = not visited, 1 = on the active path, 2 = fully explored
const state: number[] = new Array(numCourses).fill(0);
function dfs(node: number): boolean {
state[node] = 1;
for (const next of adj[node]) {
if (state[next] === 1) return false;
if (state[next] === 0 && !dfs(next)) return false;
}
state[node] = 2;
order.push(node);
return true;
}
for (let i = 0; i < numCourses; i++) {
if (state[i] === 0 && !dfs(i)) return [];
}
return order.reverse();
};
Restyled to match 210, not copied from your code (your NeetCode solution wasn't readable).
// π½ ALIEN DICTIONARY (Kahn's on letters)
function alienOrder(words: string[]): string {
const adj = new Map<string, Set<string>>();
const indegree = new Map<string, number>();
for(const word of words) { // every letter is a node
for(const ch of word) {
if(!adj.has(ch)) adj.set(ch, new Set());
if(!indegree.has(ch)) indegree.set(ch, 0);
}
}
for(let i = 0; i < words.length - 1; i++) {
const w1 = words[i];
const w2 = words[i + 1];
if(w1.length > w2.length && w1.startsWith(w2)) return ''; // "abc" before "ab" = invalid
for(let j = 0; j < Math.min(w1.length, w2.length); j++) {
if(w1[j] !== w2[j]) {
if(!adj.get(w1[j])!.has(w2[j])) { // skip duplicate edges
adj.get(w1[j])!.add(w2[j]);
indegree.set(w2[j], indegree.get(w2[j])! + 1);
}
break; // only the FIRST difference counts
}
}
}
const queue: string[] = [];
for(const [ch, deg] of indegree) {
if(deg === 0) queue.push(ch);
}
let front = 0;
let order = '';
while(front < queue.length) {
const ch = queue[front++];
order += ch;
for(const next of adj.get(ch)!) {
indegree.set(next, indegree.get(next)! - 1);
if(indegree.get(next) === 0) queue.push(next);
}
}
return order.length === indegree.size ? order : ''; // stuck β cycle
};
| Variation | What changes |
|---|---|
| 207 Course Schedule | Return order.length === n. |
| Fewest semesters (1136π) / parallel tasks with durations (2050) | Process level by level; the number of levels (or the longest path in time) is the answer. |
| 802 Find Eventual Safe States | Reverse every edge, then run Kahn's from the terminal nodes. Or use the 3-state DFS: safe = finishes with no cycle. |
| 310 Minimum Height Trees | The same idea on an undirected tree: peel leaves (degree 1) layer by layer. The last 1 or 2 nodes are the answer. |
| Is the order unique? (444π) | Unique only if the queue never holds 2 nodes at once. |
n taken = cycle)."Every group has a root. Two nodes are connected if they have the same root."
find(x) asks "who is your leader?", walking up until someone is their own parent. union(a, b) makes one leader report to the other. Same leader = same group. Path compression lets everyone remember the top leader directly, so later finds are instant.Best when edges arrive one at a time and you keep asking "connected yet?" With path compression and union by rank, each operation is nearly O(1). It works on undirected graphs only.
parent[px] = py: root 1 hangs under root 4. Only a root moves. The next find(3) points 3 straight at 4 (path compression).// π REDUNDANT CONNECTION (Union-Find, rank + path compression)
function findRedundantConnection(edges: number[][]): number[] {
const n = edges.length;
const parent = Array.from({length: n + 1}, (_, i) => i); // nodes are 1..n
const rank = Array(n + 1).fill(0);
// let count = n; // for "count components": uncomment, and do count-- before return true
function find(x: number): number {
let root = x;
while(parent[root] !== root) {
root = parent[root];
}
while(parent[x] !== root) { // path compression
const next = parent[x];
parent[x] = root;
x = next;
}
return root;
}
function union(x: number, y: number): boolean {
const px = find(x);
const py = find(y);
if(px === py) return false; // already connected, this edge makes a cycle
if(rank[px] < rank[py]) parent[px] = py;
else if(rank[px] > rank[py]) parent[py] = px;
else {
parent[px] = py;
rank[py]++;
}
return true;
}
for(const [u, v] of edges) {
if(!union(u, v)) return [u, v];
}
return [];
};
// π§ ACCOUNTS MERGE (Union-Find on emails)
function accountsMerge(accounts: string[][]): string[][] {
const parent = new Map<string, string>();
const rank = new Map<string, number>();
const owner = new Map<string, string>(); // email -> account name
function find(x: string): string {
let root = x;
while(parent.get(root) !== root) {
root = parent.get(root)!;
}
while(parent.get(x) !== root) {
const next = parent.get(x)!;
parent.set(x, root);
x = next;
}
return root;
}
function union(x: string, y: string): void {
const px = find(x);
const py = find(y);
if(px === py) return;
const rankX = rank.get(px)!;
const rankY = rank.get(py)!;
if(rankX < rankY) {
parent.set(px, py);
} else if(rankX > rankY) {
parent.set(py, px);
} else {
parent.set(px, py);
rank.set(py, rankY + 1); // bump the new ROOT (py), not px
}
}
for(const [name, ...emails] of accounts) {
for(const email of emails) {
if(!parent.has(email)) {
parent.set(email, email);
rank.set(email, 0);
owner.set(email, name);
}
union(emails[0], email); // join every email to the account's first one
}
}
const groups = new Map<string, string[]>();
for(const email of parent.keys()) {
const root = find(email);
if(!groups.has(root)) groups.set(root, []);
groups.get(root)!.push(email);
}
const res: string[][] = [];
for(const [root, emails] of groups) {
emails.sort();
res.push([owner.get(root)!, ...emails]);
}
return res;
};
| Problem | Trick |
|---|---|
| 323π / 547 count components | Start count = n, do count-- on every union that returns true, return count. |
| 305π Number of Islands II | New land: count++. Each union with a land neighbour that returns true: count--. Ignore repeated positions. |
| 947 Most Stones Removed | Union stones that share a row or column. Answer = stones β groups. |
| 1697 Edge-Length-Limited Paths | Offline: sort the queries and the edges by limit, then add edges as the limit grows. |
r * cols + c (305) Β· new node count++, real union count--."BFS, but always expand the cheapest node next."
Dijkstra works when every weight is β₯ 0. JS heaps can't lower a key in place, so push a new entry and skip stale ones when they pop. Time is O(E log E), which is the same as O(E log V) because E β€ VΒ².
// π‘ NETWORK DELAY TIME (Dijkstra)
function networkDelayTime(times: number[][], n: number, k: number): number {
const graph: number[][][] = Array.from({length: n}, () => []);
for(const [u, v, w] of times) {
const nu = u - 1; // nodes are 1..n, graph is 0-indexed
const nv = v - 1;
graph[nu].push([nv, w]);
}
const pq = new MinPriorityQueue<number[]>(x => x[0]); // [distance, node]
pq.enqueue([0, k - 1]);
const dist = Array(n).fill(Number.MAX_SAFE_INTEGER);
dist[k - 1] = 0;
while(!pq.isEmpty()) {
const [currDist, node] = pq.dequeue();
if(currDist > dist[node]) continue; // stale: a shorter path already won
for(const [nei, w] of graph[node]) {
const nextDist = w + currDist;
if(nextDist < dist[nei]) {
dist[nei] = nextDist;
pq.enqueue([nextDist, nei]);
}
}
}
let maxVal = 0;
for(let i = 0; i < n; i++) {
if(dist[i] === Number.MAX_SAFE_INTEGER) return -1;
maxVal = Math.max(dist[i], maxVal);
}
return maxVal;
};
Not in your tracker, so this is built from your 2290 Minimum Obstacle Removal shape with Math.max swapped in.
// π§ PATH WITH MINIMUM EFFORT (grid Dijkstra, cost = largest step)
function minimumEffortPath(heights: number[][]): number {
const m = heights.length;
const n = heights[0].length;
const dist = Array.from({length: m}, () => Array(n).fill(Number.MAX_SAFE_INTEGER));
const pq = new MinPriorityQueue<number[]>(x => x[2]); // [row, col, effort]
dist[0][0] = 0;
pq.enqueue([0, 0, 0]);
const DIRS = [[-1, 0], [0, 1], [1, 0], [0, -1]];
while(!pq.isEmpty()) {
const [row, col, d] = pq.dequeue();
if(row === m - 1 && col === n - 1) return d; // first time target pops = answer
if(d > dist[row][col]) continue;
for(const [dr, dc] of DIRS) {
const nr = row + dr;
const nc = col + dc;
if(nr >= 0 && nr < m && nc >= 0 && nc < n) {
const newDist = Math.max(d, Math.abs(heights[nr][nc] - heights[row][col])); // β max, not +
if(newDist < dist[nr][nc]) {
dist[nr][nc] = newDist;
pq.enqueue([nr, nc, newDist]);
}
}
}
}
return 0;
};
| Variant | The one change |
|---|---|
| 1631 Min Effort Β· 778 Swim in Rising Water | next = Math.max(cost, edge) instead of cost + edge. |
| 778 Swim in Rising Water (your version) | Binary search on the answer + BFS: lo = 0, hi = n * n; canFinish(maxEle) runs BFS through cells <= maxEle; if(canFinish(mid)) hi = mid; else lo = mid + 1. |
| 1514 Max Probability | Multiply instead of add, and pop the largest first (push -prob). |
| 1976 Number of Ways to Arrive | Keep ways[]. Strictly shorter: ways[next] = ways[node]. Equal: ways[next] += ways[node]. |
| 1928 Cost within a time limit Β· 787 with a heap | The node becomes (city, timeOrStopsLeft), so dist becomes 2-D. |
| 505π The Maze II | Neighbours are the cells where the ball stops; the weight is the cells rolled. |
| Return the actual path | prev[next] = node on every improvement; walk back from the target. |
// βοΈ CHEAPEST FLIGHTS WITHIN K STOPS (Bellman-Ford, one round per flight)
function findCheapestPrice(n: number, edges: number[][], src: number, dst: number, k: number): number {
let dist = Array(n).fill(Number.MAX_SAFE_INTEGER);
dist[src] = 0;
for(let f = 0; f < k + 1; f++) { // k stops = k + 1 flights
const nextDist = [...dist]; // copy: read last round, write this one
let changed = false;
for(const [u, v, p] of edges) {
if(dist[u] + p < nextDist[v]) {
nextDist[v] = dist[u] + p;
changed = true;
}
}
dist = nextDist;
if(!changed) break; // nothing improved: later rounds can't either
}
return dist[dst] === Number.MAX_SAFE_INTEGER ? -1 : dist[dst];
};
Number.MAX_SAFE_INTEGER + p is never smaller than a real cost, so the unreached check stays safe. Don't switch to Infinity without re-checking the comparisons.
V β 1 times, O(VΒ·E). If a V-th pass still improves something, there's a negative cycle.All pairs (1334, n β€ 100): Floyd-Warshall, three loops with k outermost: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]). O(VΒ³) time, O(VΒ²) space.
if (d > dist[u]) continue β TC O(E log E) Β· SC O(V + E).dist each round.Math.min Β· via outermost.dist[node][extra]."Take the cheapest edge that doesn't make a cycle. Repeat."
That's Kruskal: sort the edges, then use Union-Find to skip any edge that joins two nodes already connected. A tree on n nodes has n β 1 edges, so edgesUsed === n - 1 means everything is connected. You can break there to skip the remaining edges.
// π³ MIN COST TO CONNECT ALL POINTS (Kruskal + Union-Find)
function minCostConnectPoints(points: number[][]): number {
const n = points.length;
const parent = Array.from({length: n}, (_, i) => i);
const rank = Array(n).fill(0);
function find(x: number): number {
let root = x;
while(parent[root] !== root) {
root = parent[root];
}
while(parent[x] !== root) {
const next = parent[x];
parent[x] = root;
x = next;
}
return root;
}
function union(x: number, y: number): boolean {
const px = find(x);
const py = find(y);
if(px === py) return false;
if(rank[px] < rank[py]) parent[px] = py;
else if(rank[px] > rank[py]) parent[py] = px;
else {
parent[px] = py;
rank[py]++;
}
return true;
}
const edges: number[][] = []; // [i, j, cost]
for(let i = 0; i < n; i++) {
for(let j = i + 1; j < n; j++) {
const [xi, yi] = points[i];
const [xj, yj] = points[j];
const cost = Math.abs(xi - xj) + Math.abs(yi - yj);
edges.push([i, j, cost]);
}
}
edges.sort((a, b) => a[2] - b[2]);
const eLen = edges.length;
let edgesUsed = 0;
let totalCost = 0;
for(let i = 0; i < eLen; i++) {
const [u, v, w] = edges[i];
if(union(u, v)) { // joins two groups β keep the edge
edgesUsed++;
totalCost += w;
}
}
return edgesUsed === n - 1 ? totalCost : -1; // -1 if graph can't be fully connected
};
w in place of d + w. On 1584 (every pair is an edge) an array-based Prim is O(nΒ²) and beats sorting nΒ² edges.Know these if you have time. They show up in harder rounds, rarely in a typical L3/L4 loop.
π§³ Use every ticket exactly once. Walk until you're stuck; the place you get stuck is where the trip ends, so write it down last-to-first.
// π« RECONSTRUCT ITINERARY (Hierholzer, post-order)
function findItinerary(tickets: string[][]): string[] {
const graph = new Map<string, string[]>();
const res: string[] = [];
for(const [u, v] of tickets) {
if(!graph.has(u)) graph.set(u, []);
graph.get(u)!.push(v);
}
for(const values of graph.values()) {
values.sort().reverse(); // pop() then gives the smallest
}
function dfs(node: string) {
const nei = graph.get(node) ?? [];
while(nei.length > 0) {
const nextNode = nei.pop()!; // use each ticket once
dfs(nextNode);
}
res.push(node); // post-order: add only after every ticket is used
}
dfs('JFK');
return res.reverse();
};
Visiting every edge once (Euler) is O(E). Visiting every node once (Hamiltonian) is NP-hard, so don't try to code it. In a directed graph, a path exists if every node has in = out, except possibly one start (out = in + 1) and one end.
π A bridge is the only road to an island: remove it and the graph splits. An edge is safe when the far side has another way back (a back edge) to above it.
// π CRITICAL CONNECTIONS (bridges, disc/low)
function criticalConnections(n: number, connections: number[][]): number[][] {
const graph: number[][] = Array.from({length: n}, () => []);
for(const [u, v] of connections) {
graph[u].push(v);
graph[v].push(u);
}
// disc[node] = arrival timestamp, FIXED once set
const disc: number[] = Array(n).fill(-1);
// low[node] = earliest timestamp reachable from the node's subtree via a back edge
const low: number[] = Array(n).fill(-1);
const bridges: number[][] = [];
let timer = 0;
function dfs(node: number, par: number): void {
disc[node] = low[node] = timer++;
for(const nei of graph[node]) {
if(nei === par) continue; // skip the edge we just walked in on
if(disc[nei] === -1) {
dfs(nei, node);
low[node] = Math.min(low[node], low[nei]); // nei's low is final now
if(low[nei] > disc[node]) { // nei cannot reach back above node
bridges.push([node, nei]);
}
} else {
low[node] = Math.min(low[node], disc[nei]); // back edge: use disc[nei]
}
}
}
dfs(0, -1);
return bridges;
};
[[0,1],[1,2],[2,0],[1,3]] (the LC example), DFS from 0. The back edge 2 β 0 pulls low of 2 and 1 down to 0, so the triangle edges are safe. 3 has no way back above 1.Articulation points (critical nodes) use the same code with low[next] >= disc[node], plus a rule that the DFS root counts only if it has 2 or more children.
π The node alone isn't enough: being at B having seen {A} is a different situation from being at B having seen {A, C}. So the BFS state is (node, set of visited nodes), and with n β€ 12 that set fits in a bitmask.
// π§ SHORTEST PATH VISITING ALL NODES (BFS over node + bitmask)
function shortestPathLength(graph: number[][]): number {
const n = graph.length;
const target = (1 << n) - 1; // every bit set = every node visited
const visited = Array.from({length: n}, () => Array(target + 1).fill(false));
const queue: number[][] = [];
for(let i = 0; i < n; i++) { // may start anywhere
const mask = 1 << i;
visited[i][mask] = true;
queue.push([i, mask]);
}
let front = 0;
let count = 0;
while(front < queue.length) {
let size = queue.length - front;
while(size--) {
const [node, mask] = queue[front++];
if(mask === target) return count;
for(const nei of graph[node]) {
const neiMask = 1 << nei;
const nextMask = mask | neiMask;
if(!visited[nei][nextMask]) {
visited[nei][nextMask] = true;
queue.push([nei, nextMask]);
}
}
}
count++;
}
return -1;
};
| Name | One line |
|---|---|
| Strongly connected components | Groups in a directed graph where everyone reaches everyone (Tarjan or Kosaraju, O(V + E)). Almost never asked directly. |
| A* | Dijkstra ordered by dist + heuristic; the heuristic must never overestimate. |
| Bidirectional BFS | Search from both ends: about 2Β·b^(d/2) work instead of b^d. |
| 0-1 BFS | Weights of 0 or 1: a deque replaces the heap, O(V + E). |
| Johnson's algorithm | All pairs with negative edges: Bellman-Ford once to reweight, then Dijkstra from every node. |
If you can explain the trick for each in one sentence, you're ready.
| # | Problem | Pattern | The trick, in one line |
|---|---|---|---|
| 200 | Number of Islands | ποΈ DFS sweep | Count when you find a 1, then sink the whole island. |
| 695 | Max Area of Island | ποΈ DFS sweep | dfs returns the size. |
| 133 | Clone Graph | π DFS + map | Put the copy in the map before visiting neighbours. |
| 994 | Rotting Oranges | π Multi-source BFS | All rotten oranges in level 0; levels = minutes; check that no fresh ones are left. |
| 417 | Pacific Atlantic | π Reverse search | Flow uphill from each ocean's border; answer = cells reached by both. |
| 130 | Surrounded Regions | π Reverse search | Mark the O's connected to the border; flip all the others. |
| 127 | Word Ladder | π Bidirectional BFS | Swap so the smaller side expands; meeting = back.has(newWord). |
| 207 / 210 | Course Schedule I / II | π Kahn's | Edge pre β course; fewer than n taken = cycle. |
| 269π | Alien Dictionary | π½ Kahn's on letters | First different letter of neighbouring words = one edge; watch the prefix case. |
| 785 | Is Graph Bipartite | π¨ 2-colouring | Neighbour has my colour β false. Loop over every component. |
| 399 | Evaluate Division | β Weighted BFS | a/b = v β edges v and 1/v; carry the running product in the queue: [node, prod]. |
| 684 | Redundant Connection | π Union-Find | The first edge whose ends already share a root. |
| 721 | Accounts Merge | π Union-Find | Union every email with its account's first email; group emails by root. |
| 743 | Network Delay Time | π£οΈ Dijkstra | Answer = the max of all shortest distances, or β1 if any is unreachable. |
| 1631 | Path With Minimum Effort | β°οΈ Dijkstra with max | Path cost = its largest step: max instead of +. |
| 787 | Cheapest Flights Within K Stops | βοΈ Bellman-Ford | K + 1 rounds, copying the array each round. |
| 1584 | Min Cost to Connect All Points | π MST | Every pair is an edge; Kruskal with Union-Find (or O(nΒ²) Prim). |
| 815 | Bus Routes | π BFS over routes | A level is one bus; map each stop to its routes. |
| 332 | Reconstruct Itinerary | π§³ Hierholzer | Sort destinations, use each ticket once, add the airport after its tickets run out, reverse. |
| 1192 | Critical Connections | π Bridges | low[child] > disc[node] β that edge is a bridge. |
| Trap | Fix |
|---|---|
| Marking visited when you pop in BFS | Mark when you push. |
| Prerequisite edge drawn the wrong way | [a, b] = "b before a" β edge b β a. Draw it. |
| Isolated nodes missing from the graph | Create all n lists first; loop over every node for components and cycles. |
| Dijkstra without the stale check | if (d > dist[node]) continue |
Arrays as Set keys: seen.has([r, c]) is always false | Use r * cols + c or `${r},${c}` (with the comma: "1,23" β "12,3"). |
new Array(n).fill([]) shares one array | Array.from({ length: n }, () => []) |
queue.shift() is O(n) | Use one array and a front index. |
| Union-Find on a directed graph | Use Kahn's or 3-state DFS instead. |
Returning Infinity for unreachable | Convert to -1 (or whatever the problem asks). |
| Algorithm | Time | Space |
|---|---|---|
| BFS / DFS / topological sort / bipartite | O(V + E) | O(V) |
| Grid BFS / DFS | O(R Β· C) | O(R Β· C) |
| Dijkstra (heap) | O(E log E) | O(V + E) |
| Bellman-Ford (K-stop version: K + 1 rounds) | O(V Β· E) (O(K Β· E)) | O(V) |
| Floyd-Warshall | O(VΒ³) | O(VΒ²) |
| Union-Find, per operation | β O(1) | O(V) |
| Kruskal | O(E log E) | O(V + E) |
| BFS over (node, mask) | O(2βΏ Β· nΒ²) | O(2βΏ Β· n) |
Implicit graphs: V = the number of possible states (e.g. rows Γ cols Γ k for 1293), E = V Γ moves per state.
Cover the right column (it's blurred; hover or tap to reveal). Say the answer out loud first.
| Prompt | Answer |
|---|---|
| Fewest moves, every move costs 1? | BFS. The first time you reach a node is the shortest. |
| Why mark visited on push, not pop? | So each node enters the queue once. On pop, many neighbours can push the same node first. |
| Weights differ but are β₯ 0? | Dijkstra with a min-heap, skipping stale entries. |
| Why can't Dijkstra handle negative edges? | It treats a popped node as final; a negative edge found later could still make it cheaper. Use Bellman-Ford. |
| Path cost is the largest step, not the total? | Same Dijkstra, with max(cost, edge) instead of cost + edge. |
| At most K stops? | Bellman-Ford with K + 1 rounds, copying the array each round so one round = one more flight. |
| Order tasks with prerequisites, and detect a cycle? | Kahn's: take indegree-0 nodes; if fewer than n get taken, there's a cycle. |
| Cycle in a directed graph with DFS? | 3 states. Reaching a node still "in progress" means a cycle. |
| Cycle in an undirected graph? | Union-Find: an edge whose ends already share a root. |
| Clone Graph's key line? | Store the copy in the map before recursing into neighbours, or cycles loop forever. |
| Connect all nodes cheaply vs reach one node cheaply? | All nodes β MST (Kruskal). One target β Dijkstra. |
| State is more than a cell (keys, walls left)? | Make the node (cell, extra) and BFS over that. |
| Union-Find or DFS for components? | Fixed graph β DFS. Edges arriving over time, or MST β Union-Find. Never on directed graphs. |
| Alien Dictionary: the 3 traps? | Only the first differing letter makes an edge; "abc" before "ab" is invalid; letters without edges must still appear. |
find + union + 684MinPriorityQueue; in an interview doc you write the heap yourself, see Β§2)Time yourself: each one should take under 10 minutes, heap included.