πŸ•ΈοΈ Graph

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.

🧠 0 · How to think

"Things and connections. If you can say β€œX connects to Y”, it's a graph, even when the word graph never appears."

abundirectedboth waysabdirectedone way5abweightededge has a cost
A graph is just nodes (things) and edges (connections). Every problem is one of these three edge kinds, or a mix.

There are only two ways to walk a graph, and every algorithm below is one of them with a twist:

🌊 BFS: ring by ringA1B2C3D4E5F6visit order: A B C D E F
πŸŒ€ DFS: deep firstA1B2C5D3E4F6visit order: A B D E C F
Same graph, two walks. 🌊 BFS finishes each ring before the next (darker = closer to A), so it finds the fewest steps. πŸŒ€ DFS runs down one branch to the end, then backs up: good for structure (islands, cycles, copies, order).
🎯 Every graph problem = 3 moves 1️⃣ Name the nodes and the edges. 2️⃣ Pick the walk: 🌊 BFS for fewest steps Β· πŸŒ€ DFS for structure Β· πŸ›£οΈ Dijkstra when edges have costs. 3️⃣ Keep a visited so no node is processed twice.

🧭 1 · Spot it

Is it secretly a graph? If you can name the nodes and the edges, yes.

NodesEdgesExample
🟩 grid cellsup / down / left / right200 Islands, 994 Oranges
πŸ“š courses / tasks"do A before B" (one-way)207, 210
πŸ”€ words / lock codesone letter / one wheel turn apart127, 752
πŸ‘₯ accounts / peopleshare an email / are friends721, 547
βž— variablesa / b = 2 (weighted, both ways)399
πŸ—οΈ (cell, extra info)a move that also changes the extra info1293, 864, 847
Ask these 4 first 1. Directed or undirected? 2. Weighted? Can a weight be 0 or negative? 3. Can there be cycles? (β†’ you need 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…UseLC
🌊 fewest steps / minimum moves, every move costs the sameBFS1091, 127, 752
🍊 spreads from many places at once (rot, fire, gates)Multi-source BFS994, 542, 286πŸ”’
🏝️ count islands / regions / groupsDFS sweep or Union-Find200, 695, 547
πŸ‘ deep-copy a graphDFS + oldβ†’new map133
🎨 split into 2 groups with no conflict2-colouring785, 886
βž— ratios: a/b, b/c β†’ a/c?Weighted DFS, multiply399
πŸ‘• prerequisites, order of tasks, "can you finish?"Topological sort (Kahn's)207, 210
πŸ‘½ order of letters from a sorted word listTopological sort on letters269πŸ”’
πŸ‘‘ merge groups / "are x and y connected?" / extra edgeUnion-Find721, 684, 323πŸ”’
πŸ›£οΈ shortest path, weights β‰₯ 0Dijkstra743, 1514
⛰️ minimise the largest step on the pathDijkstra with max1631, 778
✈️ at most K stops / K edgesBellman-Ford, K+1 rounds787
πŸ”Œ cheapest way to connect everythingMST (Kruskal)1584
πŸ—οΈ state is more than a position (keys held, walls left)BFS over (cell, extra)1293, 864
πŸ“ˆ longest increasing path in a gridDFS + memo (values only go up β†’ no cycles)329
🧳 use every edge once / itineraryHierholzer332
πŸŒ‰ edge whose removal splits the graphBridges (low-link)1192
πŸŽ’ visit every node, n ≀ 12BFS over (node, bitmask)847
πŸ’­ Your thoughts
⚠️ MST vs Dijkstra "Connect all nodes as cheaply as possible" β†’ MST. "Cheapest path from A to B" β†’ Dijkstra. They give different answers on the same graph (picture in Β§8).
🎀 Say this before coding "I'll model this as a [directed/undirected] [weighted/unweighted] graph where nodes are X and edges are Y. We need [fewest steps / an order / cheapest path], so I'll use [BFS / topo sort / Dijkstra]. That's O(V + E) time." Then ask: can weights be 0 or negative? Can the input be disconnected?

πŸ”’ = 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).

🧱 2 · Setup

🧩 The idea πŸ“‡ An adjacency list is a contact list: each person points to their friends. To walk the graph you only ever ask "who are my neighbours?", and graph[u] answers it instantly.
0123edges = [[0,1],[0,2],[1,2],[2,3]]graph[0] = [1, 2]graph[1] = [0, 2]graph[2] = [0, 1, 3]graph[3] = [2]
Each node keeps a list of its neighbours. Undirected, so every edge is written twice (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])
⚠️ Edge direction "To take course 1 you must first take 0" means [1, 0], and the edge goes 0 β†’ 1. Draw two nodes before you code; getting the direction wrong gives silent wrong answers.
πŸ’­ Your thoughts

A heap for Dijkstra and Kruskal

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;
    }
}

🌊 3 · BFS: fewest steps

"Explore in rings. The first time you reach a node is the shortest way there."

🧩 The idea 🌊 Drop a stone in a pond: the ripple reaches close things first. BFS finishes every node 1 step away before it touches any node 2 steps away, so the first time it reaches a node is the fewest steps there. 🍊 Many sources (rotten oranges, fires) = many stones dropped at once: put them all in the first ring.

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;
};
minute each orange rots01212emptyempty34queue, one level per passlevel 0[0,0]level 1[0,1] [1,0]level 2[0,2] [1,1]level 3[2,1]level 4[2,2]
Grid [[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.
πŸ”‘ Mark when you ADD Mark a node visited when you push it, not when you pop it (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.

πŸ’­ Your thoughts

Implicit graph: generate the neighbours

// πŸͺœ 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;
};
VariationWhat 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 lengthStore parent[next] = node when you push. Walk back from the target, then reverse.
752 Open the LockNode = 4-digit string, 8 neighbours (each wheel Β±1). Dead ends start in seen.
815 Bus RoutesBFS 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 endBidirectional 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.
🎀 Interview lens Probed: why BFS gives the shortest path (rings), why you mark on push, and multi-source = put every source in the first level. Escalation: grid BFS β†’ multi-source β†’ BFS with extra state β†’ weights appear β†’ Dijkstra.

πŸŒ€ 4 Β· DFS: islands, copies, cycles

"Go as deep as you can, then back up."

🧩 The idea πŸŒ€ Explore a maze with one rule: keep walking until you hit a dead end, then step back to the last fork and try the next way. 🏝️ One DFS from a land cell sinks the whole island, so "number of DFS starts" = number of islands. DFS answers "what is connected / is there a cycle / copy it". It does not give the shortest path.

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.

The grid family

ProblemThe twist
695 Max Area of Islanddfs returns 1 + sum of the 4 calls; keep the max.
130 Surrounded Regions Β· 417 Pacific AtlanticStart from the border and mark what's reachable; everything else is the answer.
934 Shortest BridgeDFS to mark the first island, then multi-source BFS from all of it to the second.
827 Making a Large IslandLabel 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 ProvincesThe same sweep on an adjacency matrix instead of a grid.

Clone a graph

// 🧬 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);
};

Cycle in a directed graph

// πŸ” 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;
}
Diamond: D reached twiceABCDC β†’ D: D is done (2), no cycle βœ“
Loop: A β†’ B β†’ C β†’ AABCC β†’ A: A is on the path (1), cycle βœ—
🟑 1 = on the current path · 🟒 2 = done. Only an edge back into a 🟑 node is a cycle. A plain visited set would flag the diamond too.
⚠️ Why not one visited set? In a directed graph, Aβ†’B, Aβ†’C, Bβ†’D, Cβ†’D reaches D twice with no cycle. A plain 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.

πŸ’­ Your thoughts

Two groups: bipartite

// 🎨 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.

Weighted BFS: Evaluate Division

// βž— 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;
};
🎀 Interview lens Probed: recursion depth on a 300Γ—300 grid (90,000 calls, so mention an explicit stack), and modifying the input grid (ask first, or use a seen set). Escalation: count islands β†’ largest island β†’ islands added one at a time (305πŸ”’, Union-Find) β†’ change one 0 to 1 (827).

πŸ“Š 5 Β· Topological sort: order with prerequisites

"Repeatedly take a task that nothing is waiting on."

🧩 The idea πŸ‘• Getting dressed: socks before shoes, shirt before tie. Anything with nothing left before it can go now; doing it may free the next thing. A cycle (shoes need socks, socks need shoes) means nobody can ever start, which is how Kahn's spots it.

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?"

0123in 0in 1in 1in 2take 0 β†’ in[1] = 0, in[2] = 0 β†’ queue 1, 2take 1 β†’ in[3] = 1 Β· take 2 β†’ in[3] = 0 β†’ queue 3
Start with every in 0 node. Taking a node lowers its neighbours' counts. Order: 0, 1, 2, 3. Node 3 waits for both 1 and 2.
// πŸ“š 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
};
VariationWhat changes
207 Course ScheduleReturn 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 StatesReverse every edge, then run Kahn's from the terminal nodes. Or use the 3-state DFS: safe = finishes with no cycle.
310 Minimum Height TreesThe 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.
🎀 Interview lens Probed: edge direction, how Kahn's detects a cycle, and the Alien Dictionary traps (prefix case, first difference only, letters with no edges still appear). Escalation: 207 β†’ 210 β†’ 269 β†’ a minimum number of rounds.
πŸ’­ Your thoughts

πŸ”— 6 Β· Union-Find: who is in which group

"Every group has a root. Two nodes are connected if they have the same root."

🧩 The idea πŸ‘‘ Every group has a leader. 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.

before: union(3, 5)12345find(3) = 1 Β· find(5) = 4
after: parent[1] = 441523
Arrows point to the parent. Same rank, so your code does 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;
};
ProblemTrick
323πŸ”’ / 547 count componentsStart count = n, do count-- on every union that returns true, return count.
305πŸ”’ Number of Islands IINew land: count++. Each union with a land neighbour that returns true: count--. Ignore repeated positions.
947 Most Stones RemovedUnion stones that share a row or column. Answer = stones βˆ’ groups.
1697 Edge-Length-Limited PathsOffline: sort the queries and the edges by limit, then add edges as the limit grows.
πŸ’­ Your thoughts
🎀 Union-Find or DFS? If the graph is fixed and you need one answer, DFS is simpler. If edges keep arriving and you query in between, or you're building an MST, use Union-Find. If the graph is directed, Union-Find is the wrong tool.

πŸ›£οΈ 7 Β· Weighted shortest path

"BFS, but always expand the cheapest node next."

🧩 The idea πŸ›£οΈ Like a GPS: keep a heap of routes and always extend the cheapest one so far. When a node pops for the first time, every other route to it already costs at least as much (no negative edges), so its distance is final.

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Β².

BFS: fewest edges115ABCA β†’ C: 1 edge, cost 5 βœ—
Dijkstra: cheapest115ABCA β†’ B β†’ C: cost 2 βœ“
BFS counts edges, so it stops at the 1-edge path. Dijkstra always expands the cheapest node next, so it finds cost 2.
// πŸ“‘ 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;
};

The variation interviewers love: cost is a max, not a sum

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;
};
VariantThe one change
1631 Min Effort Β· 778 Swim in Rising Waternext = 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 ProbabilityMultiply instead of add, and pop the largest first (push -prob).
1976 Number of Ways to ArriveKeep ways[]. Strictly shorter: ways[next] = ways[node]. Equal: ways[next] += ways[node].
1928 Cost within a time limit Β· 787 with a heapThe node becomes (city, timeOrStopsLeft), so dist becomes 2-D.
505πŸ”’ The Maze IINeighbours are the cells where the ball stops; the weight is the cells rolled.
Return the actual pathprev[next] = node on every improvement; walk back from the target.

At most K stops: Bellman-Ford

// ✈️ 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.

⚠️ Negative weights Dijkstra assumes a popped node is final, and a negative edge can break that later. With negative weights use Bellman-Ford: relax every edge 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.

πŸ’­ Your thoughts
🎀 Interview lens Probed: why the stale check exists, why there are no negative weights, and why not just BFS (weights differ). Escalation: 743 β†’ 1631 (max) β†’ 787 (K stops) β†’ 1976 (count paths) β†’ return the path.

🌳 8 · Minimum spanning tree

"Take the cheapest edge that doesn't make a cycle. Repeat."

🧩 The idea πŸ”Œ Wire every house with the least cable. Go through the cables cheapest first and keep one only if it joins two houses that aren't connected yet. A cable between already-connected houses closes a loop, so it's wasted money.

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.

MST: connect everyone223ABCtotal 2 + 2 = 4
Shortest A β†’ C223ABCdirect edge = 3 (MST path costs 4)
Same graph, different answers. The MST drops the 3-edge, which is exactly the cheapest way from A to C.
// 🌳 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
};
πŸ’­ Your thoughts

πŸš€ 9 Β· Stretch topics L5 / HARD

Know these if you have time. They show up in harder rounds, rarely in a typical L3/L4 loop.

Use every edge once: Hierholzer

🧳 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();
};
β‘’β‘ β‘‘JFKABdead endroute: JFK β‘  B β‘‘ JFK β‘’ A
Tickets JFK→A, JFK→B, B→JFK. DFS tries A first (A < B) and gets stuck there. A is where the trip ends, so post-order writes it first.
🧠 Why post-order Pre-order breaks: tickets JFKβ†’A, JFKβ†’B, Bβ†’JFK. Pre-order gives JFK A B JFK (there's no ticket Aβ†’B). Post-order gives [A, JFK, B, JFK]; reversed, JFK B JFK A. The dead end is the finish line: write it first, read it backwards.
πŸ’­ Your thoughts

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.

Bridges: critical connections

πŸŒ‰ 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;
};
back edgebridge01230 / 01 / 02 / 03 / 3label = disc / lowlow[3] = 3 > disc[1] = 1 β†’ 1–3 is a bridge
Graph [[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.

Visit all nodes: BFS over (node, bitmask)

πŸŽ’ 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-drop only

NameOne line
Strongly connected componentsGroups 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 BFSSearch from both ends: about 2Β·b^(d/2) work instead of b^d.
0-1 BFSWeights of 0 or 1: a deque replaces the heap, O(V + E).
Johnson's algorithmAll pairs with negative edges: Bellman-Ford once to reweight, then Dijkstra from every node.

⭐ 10 · The must-know 20

If you can explain the trick for each in one sentence, you're ready.

#ProblemPatternThe trick, in one line
200Number of Islands🏝️ DFS sweepCount when you find a 1, then sink the whole island.
695Max Area of Island🏝️ DFS sweepdfs returns the size.
133Clone GraphπŸ‘ DFS + mapPut the copy in the map before visiting neighbours.
994Rotting Oranges🍊 Multi-source BFSAll rotten oranges in level 0; levels = minutes; check that no fresh ones are left.
417Pacific AtlanticπŸ”™ Reverse searchFlow uphill from each ocean's border; answer = cells reached by both.
130Surrounded RegionsπŸ”™ Reverse searchMark the O's connected to the border; flip all the others.
127Word Ladder🌊 Bidirectional BFSSwap so the smaller side expands; meeting = back.has(newWord).
207 / 210Course Schedule I / IIπŸ‘• Kahn'sEdge pre β†’ course; fewer than n taken = cycle.
269πŸ”’Alien DictionaryπŸ‘½ Kahn's on lettersFirst different letter of neighbouring words = one edge; watch the prefix case.
785Is Graph Bipartite🎨 2-colouringNeighbour has my colour β†’ false. Loop over every component.
399Evaluate Divisionβž— Weighted BFSa/b = v β†’ edges v and 1/v; carry the running product in the queue: [node, prod].
684Redundant ConnectionπŸ‘‘ Union-FindThe first edge whose ends already share a root.
721Accounts MergeπŸ‘‘ Union-FindUnion every email with its account's first email; group emails by root.
743Network Delay TimeπŸ›£οΈ DijkstraAnswer = the max of all shortest distances, or βˆ’1 if any is unreachable.
1631Path With Minimum Effort⛰️ Dijkstra with maxPath cost = its largest step: max instead of +.
787Cheapest Flights Within K Stops✈️ Bellman-FordK + 1 rounds, copying the array each round.
1584Min Cost to Connect All PointsπŸ”Œ MSTEvery pair is an edge; Kruskal with Union-Find (or O(nΒ²) Prim).
815Bus Routes🚌 BFS over routesA level is one bus; map each stop to its routes.
332Reconstruct Itinerary🧳 HierholzerSort destinations, use each ticket once, add the airport after its tickets run out, reverse.
1192Critical ConnectionsπŸŒ‰ Bridgeslow[child] > disc[node] β†’ that edge is a bridge.

⚠️ 11 · Traps

TrapFix
Marking visited when you pop in BFSMark when you push.
Prerequisite edge drawn the wrong way[a, b] = "b before a" β†’ edge b β†’ a. Draw it.
Isolated nodes missing from the graphCreate all n lists first; loop over every node for components and cycles.
Dijkstra without the stale checkif (d > dist[node]) continue
Arrays as Set keys: seen.has([r, c]) is always falseUse r * cols + c or `${r},${c}` (with the comma: "1,23" β‰  "12,3").
new Array(n).fill([]) shares one arrayArray.from({ length: n }, () => [])
queue.shift() is O(n)Use one array and a front index.
Union-Find on a directed graphUse Kahn's or 3-state DFS instead.
Returning Infinity for unreachableConvert to -1 (or whatever the problem asks).

Complexity

AlgorithmTimeSpace
BFS / DFS / topological sort / bipartiteO(V + E)O(V)
Grid BFS / DFSO(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-WarshallO(VΒ³)O(VΒ²)
Union-Find, per operationβ‰ˆ O(1)O(V)
KruskalO(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.

πŸ’­ Your thoughts

βœ… 12 Β· Self-check

Cover the right column (it's blurred; hover or tap to reveal). Say the answer out loud first.

PromptAnswer
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.

Write these cold (no peeking)

Time yourself: each one should take under 10 minutes, heap included.