The Story That Explains Prim's Algorithm
You do not try every combination — there are too many. Instead you start at one town, look at every cable leaving your already-connected region, and lay the single cheapest cable that reaches a town you haven't connected yet. Then you repeat. Your network grows outward like a crystal, one cheapest edge at a time, until every town is on the grid.
That greedy, grow-the-tree-outward strategy is Prim's algorithm.
Prim's algorithm finds the Minimum Spanning Tree (MST) of a connected, weighted, undirected graph: the cheapest set of edges that connects all vertices with no cycles. It was first described by Vojtěch Jarník in 1930, then independently by Robert C. Prim in 1957 and Edsger Dijkstra in 1959 — which is why you'll sometimes see it called the Jarník–Prim algorithm.
Maintain one growing tree. At every step, look only at edges that cross from inside the tree to outside it, and greedily grab the lightest one. Because of the cut property, that locally cheapest choice is always part of some globally minimum spanning tree — so greedy is provably optimal here.
The Foundation — What Is a Minimum Spanning Tree?
Before the algorithm, you need three definitions. A spanning tree of a graph with V vertices is a set of edges that touches every vertex, stays connected, and contains no cycle — which means it always has exactly V − 1 edges. A graph can have many spanning trees; the minimum spanning tree is simply the one whose edge weights add up to the smallest total.
A common trap: the MST minimises the total weight of the whole tree, not the distance between any two specific vertices. The path from A to D inside an MST may be far longer than the shortest A–D path in the original graph. If you need point-to-point shortest paths, you want Dijkstra, not Prim.
Why Greedy Works — The Cut Property
Prim's is a greedy algorithm: it never reconsiders a choice. The reason it still lands on the optimal answer is a small piece of graph theory called the cut property.
For any partition of the vertices into two non-empty sets, the minimum-weight edge crossing the partition is contained in at least one MST. Since Prim always adds the lightest edge crossing the cut between visited and unvisited vertices, every edge it picks is provably part of a valid MST. Greedy never has to backtrack.
The Algorithm in Five Steps
Hands-On: Watch Every Line of Code Build the Tree
This is the heart of the tutorial. Below, the graph on the left and the Python on the right are wired together. Press Play (or step manually) and watch the highlighted line of code drive exactly one change in the graph: the active line is the task happening on the canvas. Candidate edges glow blue, the edge under consideration pulses amber, accepted MST edges turn solid green, and cycle-forming edges flash red before being discarded.
The graph above is the exact one used in the Python code in Section 06 — starting vertex is A. Final MST cost is 16.
Python Implementation
Heap-Based Prim — O(E log V)
This is the implementation the visualiser above animates, using Python's built-in
heapq min-heap so the cheapest candidate edge is always popped first.
Each commented line corresponds to one highlighted step in the animation.
import heapq
def prim(graph, start):
visited = {start} # the tree starts at the source vertex
mst, total = [], 0 # chosen edges + running cost
frontier = [] # a min-heap of candidate edges
for nbr, w in graph[start]: # seed the heap with the source's edges
heapq.heappush(frontier, (w, start, nbr))
while frontier: # keep going while candidates remain
w, u, v = heapq.heappop(frontier) # cheapest edge leaving the tree
if v in visited: # both ends already in? skip the cycle
continue
visited.add(v) # bring v into the tree
mst.append((u, v, w)) # lock in the chosen edge
total += w # grow the total cost
for nbr, w2 in graph[v]: # expand the frontier from v
if nbr not in visited:
heapq.heappush(frontier, (w2, v, nbr))
return mst, total # V-1 edges + minimum cost
Running It on Our Six-Town Graph
graph = {
'A': [('B', 4), ('F', 6), ('E', 8)],
'B': [('A', 4), ('F', 2), ('C', 5)],
'C': [('B', 5), ('F', 3), ('D', 4)],
'D': [('C', 4), ('F', 7), ('E', 3)],
'E': [('D', 3), ('F', 5), ('A', 8)],
'F': [('A', 6), ('B', 2), ('C', 3), ('D', 7), ('E', 5)],
}
edges, cost = prim(graph, 'A')
for u, v, w in edges:
print(f"{u} -- {v} (weight {w})")
print("Total MST cost:", cost)
Dense-Graph Variant — O(V²) with an Adjacency Matrix
When the graph is dense (close to every-pair-connected), a plain array scan
beats a heap. No priority queue, just a key[] array holding the cheapest known
edge weight to reach each vertex.
import math
def prim_dense(W): # W is a V x V weight matrix (inf = no edge)
n = len(W)
key = [math.inf] * n # cheapest edge reaching each vertex
parent = [-1] * n # which tree-vertex it attaches to
in_mst = [False] * n
key[0] = 0 # start from vertex 0
for _ in range(n):
u = min((k, i) for i, k in enumerate(key)
if not in_mst[i])[1] # pick closest non-tree vertex
in_mst[u] = True
for v in range(n): # relax neighbours of u
if not in_mst[v] and W[u][v] < key[v]:
key[v], parent[v] = W[u][v], u
return parent, sum(key)
The Run, Traced Step by Step
Here is exactly what the animation does, written out as a table. Starting at A, the heap always serves the cheapest crossing edge. Cycle-forming edges are popped but skipped.
| Step | Edge Popped | Decision | Tree After | Total |
|---|---|---|---|---|
| 1 | A–B (4) | Add B | A, B | 4 |
| 2 | B–F (2) | Add F | A, B, F | 6 |
| 3 | F–C (3) | Add C | A, B, F, C | 9 |
| 4 | C–D (4) | Add D | A, B, F, C, D | 13 |
| 5 | D–E (3) | Add E | all 6 vertices | 16 |
| 6–10 | B–C, F–E, A–F, F–D, A–E | Skip — both ends already in tree (cycle) | unchanged | 16 |
The MST has exactly V−1 = 5 edges and a total weight of
16. Every other crossing edge was eventually popped and rejected because
adding it would have closed a loop. That is the cycle-check (if v in visited)
earning its keep.
Time & Space Complexity
| Implementation | Find-Min Strategy | Time | Best For |
|---|---|---|---|
| Adjacency matrix | Linear scan of key[] | O(V²) | Dense graphs (E ≈ V²) |
| Binary heap + adj. list | heapq pop | O(E log V) | Sparse graphs (E « V²) |
| Fibonacci heap | Amortised decrease-key | O(E + V log V) | Theoretical optimum; rarely worth the constant factors |
In a dense graph E grows like V², so O(E log V) becomes O(V² log V) — worse than the matrix method's flat O(V²). On sparse graphs the heap wins easily. Space is O(V + E) for the adjacency-list versions. This is exactly the situation where Prim's beats Kruskal's: dense graphs, where you never want to sort all E edges.
Benefits of Prim's Algorithm
Both produce a correct MST. Prim grows one tree from a vertex and shines on dense graphs; Kruskal sorts all edges and merges a forest using union-find, shining on sparse graphs. Pick based on graph density and which data structures you already have.
Real-World Use Cases
Prim vs Kruskal — Side by Side
| Property | Prim's Algorithm | Kruskal's Algorithm |
|---|---|---|
| Grows by | One tree, outward from a vertex | A forest that merges into one tree |
| Core data structure | Priority queue / key array | Sorted edges + union-find (DSU) |
| Typical time | O(E log V) or O(V²) | O(E log E) |
| Best on | Dense graphs | Sparse graphs |
| Partial result is | Always one connected tree | A disconnected forest until the end |
| Handles disconnected graph | No — needs a connected graph | Yes — finds a minimum spanning forest |
Golden Rules
if v in visited
— that lazy deletion is what keeps the simple heap version correct.