Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Graph Algorithms

Builds on Union-Find, which you haven’t finished yet.

P2MediumMental model~6 min speedrun

Minimum Spanning Tree (Kruskal's, built on Union-Find)

The problem

Connecting Cities With Minimum Cost (LC 1135)(opens leetcode.com)

There are n cities, labeled 1 to n. Each connections[i] = [xi, yi, cost_i] says you can build a two-way link between city xi and city yi for cost_i.

Choose links so that every city can reach every other city through built links, and pay as little as possible in total. Return that smallest total, or -1 if even building every link leaves some cities cut off.

Example 1
Input:n = 3, connections = [[1,2,5],[1,3,6],[2,3,1]]
Output:6
Any two links connect three cities, so take the two cheapest: 2-3 for 1 and 1-2 for 5.
561123

Kruskal's MST

edge list sorted by weight ➔ union by rank/size ➔ connect components
Spot it when: You must connect all points, cities or computers at the lowest total edge cost, with every node reachable in the end.
Not this one if…
  • You need the cheapest route from one source to the other nodes, as in Network Delay Time (LC 743), not the cheapest set of edges that joins everything. See Dijkstra
Kruskal's MST
Python
1from typing import List
2 
3def kruskal_mst(n: int, edges: List[List[int]]) -> int:
4 """Kruskal's MST (Connecting Cities With Minimum Cost): cheapest total cost to join
5 nodes 1..n, or -1.
6 Time: O(E log E) | Space: O(V)
7 """
8 parent = list(range(n + 1))
9 
10 def find(x: int) -> int:
11 while parent[x] != x:
12 parent[x] = parent[parent[x]] # Path halving: skip a level on every step
13 x = parent[x]
14 return x
15 
16 edges.sort(key=lambda e: e[2]) # Cheapest edges first
17 mst_cost = 0
18 edges_count = 0
19 
20 for u, v, w in edges:
21 root_u, root_v = find(u), find(v)
22 if root_u != root_v: # Different groups: this edge is safe to take
23 parent[root_u] = root_v # Join the roots, not the nodes
24 mst_cost += w
25 edges_count += 1
26 if edges_count == n - 1:
27 break # A spanning tree has exactly n - 1 edges
28 
29 return mst_cost if edges_count == n - 1 else -1 # Fewer edges: not connected
Invariant: The edges taken so far form a forest that is part of some minimum spanning tree, and find gives one root to exactly the nodes they connect.

Sort edges by weight and take each edge whose ends have different roots, find(u) != find(v), joining the two groups. The cheapest edge between two separate groups is always safe to take: any full wiring must cross that gap somewhere, and swapping in the cheaper edge never costs more; an edge whose ends already share a root would only close a cycle, so skipping it loses nothing. The trap is the final check: fewer than n - 1 edges taken means the graph is not connected, so return -1 instead of the partial cost.

See it run

Kruskal's minimum spanning tree, step by step

parent [1, 2, 3, 4] · mst_cost 0 · edges_count 0

parent [1, 2, 3, 4] · mst_cost 0 · edges_count 0

parent [1, 3, 3, 4] · mst_cost 1 · edges_count 1

parent [1, 3, 3, 4] · mst_cost 1 · edges_count 1

parent [1, 3, 4, 4] · mst_cost 4 · edges_count 2

parent [1, 3, 4, 4] · mst_cost 4 · edges_count 2

parent [1, 3, 4, 4] · mst_cost 4 · edges_count 2

parent [1, 4, 4, 4] · mst_cost 4 · edges_count 2

parent [4, 4, 4, 4] · mst_cost 10 · edges_count 3

parent [4, 4, 4, 4] · mst_cost 10 · edges_count 3 · answer 10

Each line is a road with its cost beside it. A node shows its number, a slash, then its root. The row under the graph is edges sorted by weight, written as u-v:cost.

  • test the edge being checked, and its two end nodes
  • take an edge added to the tree
  • skip an edge whose ends already share a root

edges.sort puts the cheapest edge first. Every node is its own root, so the groups are 4 single nodes.

Edge 2-3 costs 1. find(2) = 2 and find(3) = 3.

The roots 2 and 3 differ, so take the edge. parent[2] = 3, mst_cost is 1, edges_count is 1.

Edge 2-4 costs 3. find(2) = 3 and find(4) = 4.

The roots 3 and 4 differ, so take the edge. parent[3] = 4, mst_cost is 4, edges_count is 2.

Edge 3-4 costs 4. find(3) = 4 and find(4) = 4.

Both ends have root 4. The edge would close a cycle, so skip it. mst_cost stays 4.

Edge 1-2 costs 6. find(1) = 1 and find(2) = 4.

The roots 1 and 4 differ, so take the edge. parent[1] = 4, mst_cost is 10, edges_count is 3.

edges_count is 3, which is n - 1, so the loop stops. The answer is 10.

61341/12/23/34/4
edges sorted by weight
2-3:1
2-4:3
3-4:4
1-2:6
1 / 10
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Returning mst_cost without checking edges_count

    Mistake
    return mst_cost
    Fix
    return mst_cost if edges_count == n - 1 else -1

    What happens: A tree that joins n nodes has exactly n - 1 edges. If the edges run out with fewer, some cities are still in separate groups. mst_cost is then the cost of a partial wiring. The problem asks for -1 in that case.

    Example: Connecting Cities With Minimum Cost (LC 1135): n = 4, connections = [[1, 2, 3], [3, 4, 4]]
    Mistake: returns 7Fix: returns -1
  2. 2

    Gotcha 2: Writing find without path halving

    Mistake
    while parent[x] != x:
        x = parent[x]
    Fix
    while parent[x] != x:
        parent[x] = parent[parent[x]]
        x = parent[x]

    What happens: Without parent[x] = parent[parent[x]], the parent links are never shortened. The joins can build one long chain. Each find then walks the whole chain, which is O(n) per call. The answer stays the same, so this mistake has no example. Union by size keeps the trees short too.

  3. 3

    Gotcha 3: Sorting the edges by e[0] instead of the weight e[2]

    Mistake
    edges.sort(key=lambda e: e[0])
    Fix
    edges.sort(key=lambda e: e[2])

    What happens: Each edge is [u, v, w], so e[0] is a city number. The greedy step is only safe when the cheapest edges come first. Sorted by city, an expensive edge can join two groups before a cheap one. The total cost is then too high.

    Example: n = 3, connections = [[1, 2, 5], [1, 3, 6], [2, 3, 1]]
    Mistake: returns 11Fix: returns 6
  4. 4

    Gotcha 4: Joining the nodes parent[u] = root_v instead of the roots

    Mistake
    parent[u] = root_v
    Fix
    parent[root_u] = root_v

    What happens: u may not be the root of its group. Setting parent[u] moves only u, and the part of the group below u. The rest of the group keeps its old root. Two groups that were joined can then look separate. So an edge inside one group is taken, and it closes a cycle. It still counts toward n - 1, so the loop can stop before every city is joined.

    Example: n = 4, connections = [[1, 2, 6], [2, 3, 1], [2, 4, 3], [3, 4, 4]]
    Mistake: returns 8Fix: returns 10

Deep Dive

Why the cheapest edge between two groups is safe

A spanning tree joins all n nodes with n - 1 edges and no cycle. We want the one with the lowest total cost.

At any moment find splits the nodes into groups. Take the cheapest edge e that joins two different groups. Look at any full wiring that does not use e. That wiring must connect the two groups somewhere, so it has some edge f that crosses between them. Now remove f and add e. The wiring is still connected, and its cost did not go up, because e is no more expensive than f.

So some cheapest wiring uses e. Taking e never rules out a cheapest wiring, and the same argument holds after every join.

An edge whose two ends share a root is different. Both ends are already connected, so the edge only closes a cycle. Skipping it loses nothing.

One trace: an edge taken and an edge skipped

The board runs kruskal_mst on n = 4 and connections = [[1, 2, 6], [2, 3, 1], [2, 4, 3], [3, 4, 4]]. After the sort the order is 2-3:1, 2-4:3, 3-4:4 and 1-2:6.

Edge 2-3 has roots 2 and 3, which differ, so it is taken and parent[2] = 3. Edge 2-4 has roots 3 and 4, so it is taken and parent[3] = 4. Now nodes 2, 3 and 4 share root 4.

Edge 3-4 has root 4 on both ends. Taking it would close the triangle 2-3-4, so it is skipped. This is the step marked as skipped on the board.

Edge 1-2 has roots 1 and 4, so it is taken, and edges_count reaches n - 1 = 3. The cost is 1 + 3 + 6 = 10. The loop stops at once.

The trap: a graph that is not connected

The loop ends when edges_count is n - 1, or when the edges run out. If the edges run out first, some nodes are in separate groups, and no wiring can join them all.

Example: Connecting Cities With Minimum Cost (LC 1135) with n = 4 and connections = [[1, 2, 3], [3, 4, 4]]. Both edges are taken, so mst_cost is 7 and edges_count is 2. But n - 1 is 3. The cities 1 and 2 never join the cities 3 and 4. The right answer is -1, and return mst_cost would return 7.

The other trap is the join itself. parent[root_u] = root_v links the roots. Writing parent[u] = root_v moves only u, so a group can look split. For the graph in the trace, that mistake returns 8 and not 10.

The cost, and how it differs from Dijkstra

The sort costs O(E log E). Each find with path halving is close to constant. So the total is O(E log E) time, and parent takes O(V) space.

Min Cost to Connect All Points (LC 1584) has no edge list. Every pair of points is an edge, so 1000 points give 499500 edges. Build them all, and then run the same loop.

Dijkstra (the cheapest-first card) and this card both pick cheap edges, but they answer different questions. Dijkstra finds the cheapest route from one source to each node. Kruskal finds the cheapest set of edges that joins all nodes.

Example: take edges 1-2:2, 2-3:2 and 1-3:3. The cheapest wiring uses 1-2 and 2-3, and costs 4. The cheapest routes from node 1 use 1-2 and 1-3, because node 3 is cheapest to reach directly. Those two edges cost 5.

60-Second Pitch

"I need the cheapest set of edges that joins every node, so I use Kruskal's algorithm with Union-Find. I sort the edges by weight. For each edge, I check whether its two ends already have the same root. If the roots differ, this is the cheapest edge between two groups, so I link the roots and add its weight. If the roots are equal, the edge would close a cycle, so I skip it. This is safe because any full wiring must cross between two groups somewhere, and swapping in the cheaper edge never raises the cost. I stop at n - 1 edges. If I run out of edges before that, the graph is not connected and I return -1. The time is O(E log E) for the sort."

Common Questions

Why does taking the cheapest edge between two groups never hurt?

Any full wiring must cross between those two groups with some edge. Replace that edge with the cheapest one. The wiring stays connected and costs no more. So a cheapest wiring that uses the edge always exists.

What are the time and space complexity?

O(E log E) time for the sort. The loop makes E calls to find, each close to constant with path halving. The space is O(V) for parent. For Min Cost to Connect All Points (LC 1584), E is about V squared divided by 2.

How do you know the graph is not connected?

A spanning tree has exactly n - 1 edges. If the loop ends with edges_count below n - 1, some nodes are in separate groups. Return -1.

Kruskal or Prim: which would you pick?

Kruskal is short when the input is an edge list, because it needs only a sort and Union-Find. Prim grows one tree with a heap, and it suits a dense graph given as a matrix. Both find a minimum spanning tree.

Is the minimum spanning tree unique?

Not always. If two edges have the same weight, two different trees can have the same lowest cost. The total cost is always the same. Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree (LC 1489) asks which edges are in every tree.

SWE Reasoning

Spot it

  • The task is to connect all points, cities, computers or pipes at the lowest total cost.
  • The answer is one total cost, not a path between two nodes.
  • The input is a list of weighted edges, or every pair of points can be an edge.
  • The wiring needs no cycle, because a cycle only repeats a link that already exists.
  • The statement says to return -1 or false when the nodes cannot all be joined.

The invariant, step by step

  1. parent starts with every node as its own root, so every node is its own group. mst_cost and edges_count are 0.
  2. edges.sort puts the cheapest edge first.
  3. For each edge, compute find(u) and find(v).
  4. If the roots differ, this is the cheapest edge left between two groups. Link the roots, add w to mst_cost and add 1 to edges_count.
  5. If the roots are equal, the ends are already connected. Skip the edge.
  6. Stop when edges_count == n - 1. If the edges run out first, return -1.

Traps

  • No final check. Returning mst_cost when edges_count is below n - 1 gives the cost of a partial wiring. Return -1.
  • Sorting by the wrong field. The weight is e[2]. Sorting by a city number breaks the cheapest-edge rule.
  • Linking nodes, not roots. Write parent[root_u] = root_v. Writing parent[u] = root_v can leave a group split.
  • No path halving. The answer is the same, but each find can walk a long chain.
  • Using it for a path. An MST does not give the cheapest path between two nodes. Use Dijkstra for that.

Practice

Canonical LeetCode:Min Cost to Connect All Points (LC 1584)Connecting Cities With Minimum Cost (LC 1135)Optimize Water Distribution in a Village (LC 1168)Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree (LC 1489)
Theory Context•Graph Algorithms

Builds on Union-Find, which you haven’t finished yet.

P2MediumMental model~6 min speedrun

Minimum Spanning Tree (Kruskal's, built on Union-Find)

The problem

Connecting Cities With Minimum Cost (LC 1135)(opens leetcode.com)

There are n cities, labeled 1 to n. Each connections[i] = [xi, yi, cost_i] says you can build a two-way link between city xi and city yi for cost_i.

Choose links so that every city can reach every other city through built links, and pay as little as possible in total. Return that smallest total, or -1 if even building every link leaves some cities cut off.

Example 1
Input:n = 3, connections = [[1,2,5],[1,3,6],[2,3,1]]
Output:6
Any two links connect three cities, so take the two cheapest: 2-3 for 1 and 1-2 for 5.
561123

Kruskal's MST

edge list sorted by weight ➔ union by rank/size ➔ connect components
Spot it when: You must connect all points, cities or computers at the lowest total edge cost, with every node reachable in the end.
Not this one if…
  • You need the cheapest route from one source to the other nodes, as in Network Delay Time (LC 743), not the cheapest set of edges that joins everything. See Dijkstra
Kruskal's MST
Python
1from typing import List
2 
3def kruskal_mst(n: int, edges: List[List[int]]) -> int:
4 """Kruskal's MST (Connecting Cities With Minimum Cost): cheapest total cost to join
5 nodes 1..n, or -1.
6 Time: O(E log E) | Space: O(V)
7 """
8 parent = list(range(n + 1))
9 
10 def find(x: int) -> int:
11 while parent[x] != x:
12 parent[x] = parent[parent[x]] # Path halving: skip a level on every step
13 x = parent[x]
14 return x
15 
16 edges.sort(key=lambda e: e[2]) # Cheapest edges first
17 mst_cost = 0
18 edges_count = 0
19 
20 for u, v, w in edges:
21 root_u, root_v = find(u), find(v)
22 if root_u != root_v: # Different groups: this edge is safe to take
23 parent[root_u] = root_v # Join the roots, not the nodes
24 mst_cost += w
25 edges_count += 1
26 if edges_count == n - 1:
27 break # A spanning tree has exactly n - 1 edges
28 
29 return mst_cost if edges_count == n - 1 else -1 # Fewer edges: not connected
Invariant: The edges taken so far form a forest that is part of some minimum spanning tree, and find gives one root to exactly the nodes they connect.

Sort edges by weight and take each edge whose ends have different roots, find(u) != find(v), joining the two groups. The cheapest edge between two separate groups is always safe to take: any full wiring must cross that gap somewhere, and swapping in the cheaper edge never costs more; an edge whose ends already share a root would only close a cycle, so skipping it loses nothing. The trap is the final check: fewer than n - 1 edges taken means the graph is not connected, so return -1 instead of the partial cost.

See it run

Kruskal's minimum spanning tree, step by step

parent [1, 2, 3, 4] · mst_cost 0 · edges_count 0

parent [1, 2, 3, 4] · mst_cost 0 · edges_count 0

parent [1, 3, 3, 4] · mst_cost 1 · edges_count 1

parent [1, 3, 3, 4] · mst_cost 1 · edges_count 1

parent [1, 3, 4, 4] · mst_cost 4 · edges_count 2

parent [1, 3, 4, 4] · mst_cost 4 · edges_count 2

parent [1, 3, 4, 4] · mst_cost 4 · edges_count 2

parent [1, 4, 4, 4] · mst_cost 4 · edges_count 2

parent [4, 4, 4, 4] · mst_cost 10 · edges_count 3

parent [4, 4, 4, 4] · mst_cost 10 · edges_count 3 · answer 10

Each line is a road with its cost beside it. A node shows its number, a slash, then its root. The row under the graph is edges sorted by weight, written as u-v:cost.

  • test the edge being checked, and its two end nodes
  • take an edge added to the tree
  • skip an edge whose ends already share a root

edges.sort puts the cheapest edge first. Every node is its own root, so the groups are 4 single nodes.

Edge 2-3 costs 1. find(2) = 2 and find(3) = 3.

The roots 2 and 3 differ, so take the edge. parent[2] = 3, mst_cost is 1, edges_count is 1.

Edge 2-4 costs 3. find(2) = 3 and find(4) = 4.

The roots 3 and 4 differ, so take the edge. parent[3] = 4, mst_cost is 4, edges_count is 2.

Edge 3-4 costs 4. find(3) = 4 and find(4) = 4.

Both ends have root 4. The edge would close a cycle, so skip it. mst_cost stays 4.

Edge 1-2 costs 6. find(1) = 1 and find(2) = 4.

The roots 1 and 4 differ, so take the edge. parent[1] = 4, mst_cost is 10, edges_count is 3.

edges_count is 3, which is n - 1, so the loop stops. The answer is 10.

61341/12/23/34/4
edges sorted by weight
2-3:1
2-4:3
3-4:4
1-2:6
1 / 10
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Returning mst_cost without checking edges_count

    Mistake
    return mst_cost
    Fix
    return mst_cost if edges_count == n - 1 else -1

    What happens: A tree that joins n nodes has exactly n - 1 edges. If the edges run out with fewer, some cities are still in separate groups. mst_cost is then the cost of a partial wiring. The problem asks for -1 in that case.

    Example: Connecting Cities With Minimum Cost (LC 1135): n = 4, connections = [[1, 2, 3], [3, 4, 4]]
    Mistake: returns 7Fix: returns -1
  2. 2

    Gotcha 2: Writing find without path halving

    Mistake
    while parent[x] != x:
        x = parent[x]
    Fix
    while parent[x] != x:
        parent[x] = parent[parent[x]]
        x = parent[x]

    What happens: Without parent[x] = parent[parent[x]], the parent links are never shortened. The joins can build one long chain. Each find then walks the whole chain, which is O(n) per call. The answer stays the same, so this mistake has no example. Union by size keeps the trees short too.

  3. 3

    Gotcha 3: Sorting the edges by e[0] instead of the weight e[2]

    Mistake
    edges.sort(key=lambda e: e[0])
    Fix
    edges.sort(key=lambda e: e[2])

    What happens: Each edge is [u, v, w], so e[0] is a city number. The greedy step is only safe when the cheapest edges come first. Sorted by city, an expensive edge can join two groups before a cheap one. The total cost is then too high.

    Example: n = 3, connections = [[1, 2, 5], [1, 3, 6], [2, 3, 1]]
    Mistake: returns 11Fix: returns 6
  4. 4

    Gotcha 4: Joining the nodes parent[u] = root_v instead of the roots

    Mistake
    parent[u] = root_v
    Fix
    parent[root_u] = root_v

    What happens: u may not be the root of its group. Setting parent[u] moves only u, and the part of the group below u. The rest of the group keeps its old root. Two groups that were joined can then look separate. So an edge inside one group is taken, and it closes a cycle. It still counts toward n - 1, so the loop can stop before every city is joined.

    Example: n = 4, connections = [[1, 2, 6], [2, 3, 1], [2, 4, 3], [3, 4, 4]]
    Mistake: returns 8Fix: returns 10

Deep Dive

Why the cheapest edge between two groups is safe

A spanning tree joins all n nodes with n - 1 edges and no cycle. We want the one with the lowest total cost.

At any moment find splits the nodes into groups. Take the cheapest edge e that joins two different groups. Look at any full wiring that does not use e. That wiring must connect the two groups somewhere, so it has some edge f that crosses between them. Now remove f and add e. The wiring is still connected, and its cost did not go up, because e is no more expensive than f.

So some cheapest wiring uses e. Taking e never rules out a cheapest wiring, and the same argument holds after every join.

An edge whose two ends share a root is different. Both ends are already connected, so the edge only closes a cycle. Skipping it loses nothing.

One trace: an edge taken and an edge skipped

The board runs kruskal_mst on n = 4 and connections = [[1, 2, 6], [2, 3, 1], [2, 4, 3], [3, 4, 4]]. After the sort the order is 2-3:1, 2-4:3, 3-4:4 and 1-2:6.

Edge 2-3 has roots 2 and 3, which differ, so it is taken and parent[2] = 3. Edge 2-4 has roots 3 and 4, so it is taken and parent[3] = 4. Now nodes 2, 3 and 4 share root 4.

Edge 3-4 has root 4 on both ends. Taking it would close the triangle 2-3-4, so it is skipped. This is the step marked as skipped on the board.

Edge 1-2 has roots 1 and 4, so it is taken, and edges_count reaches n - 1 = 3. The cost is 1 + 3 + 6 = 10. The loop stops at once.

The trap: a graph that is not connected

The loop ends when edges_count is n - 1, or when the edges run out. If the edges run out first, some nodes are in separate groups, and no wiring can join them all.

Example: Connecting Cities With Minimum Cost (LC 1135) with n = 4 and connections = [[1, 2, 3], [3, 4, 4]]. Both edges are taken, so mst_cost is 7 and edges_count is 2. But n - 1 is 3. The cities 1 and 2 never join the cities 3 and 4. The right answer is -1, and return mst_cost would return 7.

The other trap is the join itself. parent[root_u] = root_v links the roots. Writing parent[u] = root_v moves only u, so a group can look split. For the graph in the trace, that mistake returns 8 and not 10.

The cost, and how it differs from Dijkstra

The sort costs O(E log E). Each find with path halving is close to constant. So the total is O(E log E) time, and parent takes O(V) space.

Min Cost to Connect All Points (LC 1584) has no edge list. Every pair of points is an edge, so 1000 points give 499500 edges. Build them all, and then run the same loop.

Dijkstra (the cheapest-first card) and this card both pick cheap edges, but they answer different questions. Dijkstra finds the cheapest route from one source to each node. Kruskal finds the cheapest set of edges that joins all nodes.

Example: take edges 1-2:2, 2-3:2 and 1-3:3. The cheapest wiring uses 1-2 and 2-3, and costs 4. The cheapest routes from node 1 use 1-2 and 1-3, because node 3 is cheapest to reach directly. Those two edges cost 5.

60-Second Pitch

"I need the cheapest set of edges that joins every node, so I use Kruskal's algorithm with Union-Find. I sort the edges by weight. For each edge, I check whether its two ends already have the same root. If the roots differ, this is the cheapest edge between two groups, so I link the roots and add its weight. If the roots are equal, the edge would close a cycle, so I skip it. This is safe because any full wiring must cross between two groups somewhere, and swapping in the cheaper edge never raises the cost. I stop at n - 1 edges. If I run out of edges before that, the graph is not connected and I return -1. The time is O(E log E) for the sort."

Common Questions

Why does taking the cheapest edge between two groups never hurt?

Any full wiring must cross between those two groups with some edge. Replace that edge with the cheapest one. The wiring stays connected and costs no more. So a cheapest wiring that uses the edge always exists.

What are the time and space complexity?

O(E log E) time for the sort. The loop makes E calls to find, each close to constant with path halving. The space is O(V) for parent. For Min Cost to Connect All Points (LC 1584), E is about V squared divided by 2.

How do you know the graph is not connected?

A spanning tree has exactly n - 1 edges. If the loop ends with edges_count below n - 1, some nodes are in separate groups. Return -1.

Kruskal or Prim: which would you pick?

Kruskal is short when the input is an edge list, because it needs only a sort and Union-Find. Prim grows one tree with a heap, and it suits a dense graph given as a matrix. Both find a minimum spanning tree.

Is the minimum spanning tree unique?

Not always. If two edges have the same weight, two different trees can have the same lowest cost. The total cost is always the same. Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree (LC 1489) asks which edges are in every tree.

SWE Reasoning

Spot it

  • The task is to connect all points, cities, computers or pipes at the lowest total cost.
  • The answer is one total cost, not a path between two nodes.
  • The input is a list of weighted edges, or every pair of points can be an edge.
  • The wiring needs no cycle, because a cycle only repeats a link that already exists.
  • The statement says to return -1 or false when the nodes cannot all be joined.

The invariant, step by step

  1. parent starts with every node as its own root, so every node is its own group. mst_cost and edges_count are 0.
  2. edges.sort puts the cheapest edge first.
  3. For each edge, compute find(u) and find(v).
  4. If the roots differ, this is the cheapest edge left between two groups. Link the roots, add w to mst_cost and add 1 to edges_count.
  5. If the roots are equal, the ends are already connected. Skip the edge.
  6. Stop when edges_count == n - 1. If the edges run out first, return -1.

Traps

  • No final check. Returning mst_cost when edges_count is below n - 1 gives the cost of a partial wiring. Return -1.
  • Sorting by the wrong field. The weight is e[2]. Sorting by a city number breaks the cheapest-edge rule.
  • Linking nodes, not roots. Write parent[root_u] = root_v. Writing parent[u] = root_v can leave a group split.
  • No path halving. The answer is the same, but each find can walk a long chain.
  • Using it for a path. An MST does not give the cheapest path between two nodes. Use Dijkstra for that.

Practice

Canonical LeetCode:Min Cost to Connect All Points (LC 1584)Connecting Cities With Minimum Cost (LC 1135)Optimize Water Distribution in a Village (LC 1168)Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree (LC 1489)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•