From Prim’s Rule to an Implementation
Prim’s algorithm grows a tree from a chosen starting vertex. At each step it adds an outside vertex using a cheapest edge from that vertex to the current tree. An adjacency list lets the algorithm examine only the edges incident to a vertex just added. A priority queue lets it choose the outside vertex with the smallest current connection cost.
Here we consider finite, connected, simple, undirected graphs with a real weight \(w(e)\) on each edge. A spanning tree is a connected, cycle-free subgraph containing every vertex. This tutorial focuses on implementing Prim’s choices; the proof that those choices produce a minimum spanning tree is a separate correctness question. The implementation also detects when a graph is disconnected, in which case a spanning tree does not exist.
The priority queue stores pairs \((\operatorname{key}[v],v)\), ordered by key; ties may be settled by vertex label. When a key decreases, a standard priority queue could update the old entry using a decrease-key operation. Instead, we use lazy insertion: insert a new pair and leave the old one in the queue. An entry is stale if its recorded key no longer equals the vertex’s current key, or if that vertex has already joined the tree.
Initialization and the Key Invariant
Let \(r\) be the chosen root. Start with \(T=\{r\}\), set every other key to \(+\infty\), and scan the adjacency list of \(r\). For each neighbor \(v\), set \(\operatorname{key}[v]=w(rv)\), set \(\operatorname{parent}[v]=r\), and insert \((w(rv),v)\) into the queue. Only after this initial scan do we claim the key invariant: for every \(v\notin T\), its key equals the minimum weight of an edge from \(v\) to \(T\), or \(+\infty\) if there is no such edge. A finite key has a parent in \(T\) that realizes it.
This timing matters. Before the root’s adjacency list is scanned, an outside neighbor still has key \(+\infty\), even though an edge to the root exists. Initialization is therefore a separate step, not an instance of the invariant.
Put \(r\) in \(T\), set all other keys to \(+\infty\), and scan \(r\)’s adjacency list to set neighbor keys and parents and insert their queue entries.
Remove queue entries until the minimum entry \((k,v)\) satisfies \(v\notin T\) and \(k=\operatorname{key}[v]\). If no entry remains before all vertices are added, the graph is disconnected.
Add \(v\) to \(T\) and include \(v\operatorname{parent}[v]\) in the tree. For each neighbor \(x\notin T\), if \(w(vx)<\operatorname{key}[x]\), replace the key by \(w(vx)\), set the parent to \(v\), and insert the new pair.
Continue until all vertices are in \(T\). Entries left in the queue need not be processed once the tree is complete.
The update condition is strict. If an edge has the same weight as the current key, it is also a cheapest connection, but the algorithm can keep the existing parent. This avoids unnecessary queue entries and makes the chosen output depend consistently on the tie-breaking rule.
Proof. After initialization, \(T=\{r\}\). Scanning all edges incident to \(r\) assigns each outside neighbor the weight of its edge to \(r\); every other outside vertex has no edge to \(T\) and keeps key \(+\infty\). Thus the invariant holds. Now suppose it holds at the start of an iteration, and a valid queue entry selects \(v\notin T\). Before adding \(v\), every outside vertex \(x\) has the minimum weight of an edge from \(x\) to the old \(T\). After \(v\) is added, the only new possible edge from an outside \(x\) to the enlarged tree is \(xv\). If \(xv\) is an edge, scanning \(v\)’s adjacency list compares \(w(xv)\) with the old key. A strict decrease replaces the key and records \(v\) as parent; otherwise the old key remains a minimum. If \(xv\) is not an edge, the old minimum is unchanged. These cases cover every outside \(x\), so the invariant holds after the scan. \(\square\)
Why Lazy Queue Entries Are Safe
A vertex may have several entries in the queue because its key may have decreased more than once. The queue does not need to delete an older entry when this happens. On removal, the algorithm compares the recorded value with the vertex’s current key and checks whether the vertex is already in \(T\).
Proof. If the vertex is already in \(T\), it is not eligible to be selected again. If the recorded key differs from the current key, that entry does not represent the vertex’s current connection cost. Every outside vertex with a finite current key has an entry for that key: initialization inserts entries for root neighbors, and every later strict key decrease inserts a new entry. Therefore, after stale entries are discarded, the first valid entry removed has minimum current key among the outside vertices with finite keys. Discarding the stale entry cannot remove that current minimum. \(\square\)
Keys only decrease, and each change inserts an entry with the new value. Thus an older entry for the same vertex has a larger recorded key and fails the equality test. Once a vertex has been added, all its entries fail the membership test. The algorithm can stop immediately when all vertices have been added rather than emptying the queue.
Worked Traces
Worked Example: Several Decreases and Stale Entries
Let \(G\) have vertex set \(V=\{A,B,C,D,E\}\), edge set \(E=\{AB,AC,BC,BD,CD,CE,DE\}\), and weights \(w(AB)=4\), \(w(AC)=2\), \(w(BC)=1\), \(w(BD)=5\), \(w(CD)=8\), \(w(CE)=10\), and \(w(DE)=2\). Use \(A\) as the root. Queue entries below are written as \((\text{vertex},\text{key})\), in increasing order.
Initialize \(T=\{A\}\). Scanning \(AB\) gives key \(4\) for \(B\); scanning \(AC\) gives key \(2\) for \(C\). The queue is \((C,2),(B,4)\).
| Action | Tree vertices after scan | Queue after scan, in increasing order | Key changes |
|---|---|---|---|
| Remove \((C,2)\), add \(C\); scan \(C\) | \(\{A,C\}\) | \((B,1),(B,4),(D,8),(E,10)\) | \(B:4\to1\) via \(CB\); \(D:\infty\to8\) via \(CD\); \(E:\infty\to10\) via \(CE\) |
| Remove \((B,1)\), add \(B\); scan \(B\) | \(\{A,B,C\}\) | \((D,5),(B,4),(D,8),(E,10)\) | \(D:8\to5\) via \(BD\) |
| Remove \((D,5)\), add \(D\); scan \(D\) | \(\{A,B,C,D\}\) | \((E,2),(B,4),(D,8),(E,10)\) | \(E:10\to2\) via \(DE\) |
| Remove \((E,2)\), add \(E\); scan \(E\) | \(\{A,B,C,D,E\}\) | Remaining entries are unnecessary | No outside vertices remain |
The entries \((B,4)\), \((D,8)\), and \((E,10)\) are stale: \(B,D,E\) have already been added to \(T\). They can be discarded if removed, but the algorithm stops as soon as all five vertices are in the tree. The selected parent edges are \(AC,CB,BD,DE\), with total weight \(2+1+5+2=10\).
Check: The selected edges form the path \(A-C-B-D-E\), so they connect all five vertices without a cycle. There are four selected edges for five vertices, as required for a tree. Their recomputed total weight is \(w(AC)+w(CB)+w(BD)+w(DE)=2+1+5+2=10\).
Worked Example: Equal Keys Do Not Require an Update
Let \(G\) have vertex set \(V=\{1,2,3,4\}\), edge set \(E=\{12,13,23,24,34\}\), and weights \(w(12)=1\), \(w(13)=1\), \(w(23)=1\), \(w(24)=3\), and \(w(34)=2\). Start at vertex \(1\), and break equal queue priorities by smaller vertex label.
After scanning \(1\), the keys are \(\operatorname{key}[2]=1\) with parent \(1\), and \(\operatorname{key}[3]=1\) with parent \(1\). The queue is \((2,1),(3,1)\). Remove \((2,1)\) and add \(2\). Edge \(23\) offers weight \(1\) to vertex \(3\), equal to its existing key \(1\), so its key and parent remain unchanged. Edge \(24\) sets \(\operatorname{key}[4]=3\) with parent \(2\). The queue is now \((3,1),(4,3)\).
Remove \((3,1)\) and add \(3\). Edge \(34\) has weight \(2<3\), so set \(\operatorname{key}[4]=2\), change its parent to \(3\), and insert \((4,2)\). The old entry \((4,3)\) is now stale. Remove \((4,2)\) and add \(4\). The selected edges are \(12,13,34\), of total weight \(1+1+2=4\).
Check: The selected edges connect all four vertices, and they contain no cycle: vertex \(2\) is attached to \(1\), while \(3\) and \(4\) form the path \(1-3-4\). There are three edges on four vertices, and the total weight recomputes as \(w(12)+w(13)+w(34)=1+1+2=4\). Keeping parent \(1\) for vertex \(3\) after the equal offer from \(2\) is valid because both connecting edges have weight \(1\).
Worked Example: Detecting a Disconnected Graph
Let \(G\) have vertex set \(V=\{p,q,r\}\), edge set \(E=\{pq\}\), and weight \(w(pq)=7\). Start at \(p\). Initialization puts \(p\) in \(T\), sets \(\operatorname{key}[q]=7\) with parent \(p\), and inserts \((q,7)\). Vertex \(r\) has key \(+\infty\), because there is no edge from \(r\) to \(T\).
Remove \((q,7)\), add \(q\), and scan its only edge, \(qp\), whose other endpoint is already in \(T\). The queue is empty, but \(r\notin T\). The graph is disconnected, so the algorithm reports that no spanning tree exists; it must not treat \(r\)’s infinite key as the weight of a selectable edge.
Check: The only edge joins \(p\) and \(q\), while \(r\) has degree \(0\). No sequence of edges can connect \(r\) to either other vertex, confirming that a spanning tree is impossible.
Implementation Details and Common Pitfalls
For an undirected graph, an adjacency list stores each edge \(uv\) in the lists of both \(u\) and \(v\), with its weight. When a vertex joins \(T\), scan its list and ignore neighbors already in \(T\). For an outside neighbor, compare the edge weight with that neighbor’s current key and update only on a strict decrease. The parent edge recorded at that moment is the edge used when the neighbor is eventually selected.
The most important initialization pitfall is claiming that all keys already represent minimum edges to \(T\) before scanning the root. For example, with \(V=\{r,a\}\), \(E=\{ra\}\), and \(w(ra)=6\), setting \(\operatorname{key}[a]=+\infty\) and immediately asserting that it is the minimum edge weight to \(T=\{r\}\) is false: that minimum is \(6\). The correction is to scan \(r\)’s adjacency list first, set \(\operatorname{key}[a]=6\), and only then use the invariant.
Other errors to avoid include treating an old queue entry as current after a decrease, changing a parent when an equal-weight edge is found despite using strict updates, and continuing to claim a spanning tree exists when the queue empties with vertices still outside \(T\). A one-vertex graph is a valid special case: with \(V=\{r\}\) and \(E=\varnothing\), initialization leaves no outside vertices, and the spanning tree has no edges.
The adjacency-list implementation makes the connection-cost rule explicit: each vertex stores its best known way into the current tree, and each newly added vertex can improve those costs only through its incident edges. Lazy insertion keeps the queue logic simple, while the stale-entry test ensures that only current keys determine the next choice.
Check Your Understanding
For each question, distinguish the current key from entries that may remain in the queue.
- Why is the key invariant stated only after the root’s adjacency list has been scanned?
- A vertex has current key \(5\), but the queue contains entries with keys \(8\) and \(5\). Which entry can select it, and what happens to the other?
- What should the algorithm do when an examined edge offers a key equal to the neighbor’s current key?
- In the four-vertex tied-weight example, why is it valid to keep vertex \(3\)’s parent as \(1\) when edge \(23\) also has weight \(1\)?
- What does an empty queue before all vertices have been added tell you about the graph?