Tutorials › Graph Theory (Undergraduate) › Prim’s Adjacency-List Implementation

Minimum Spanning Trees · Tutorial 143 of 799

Prim's Adjacency-List Implementation

Learn how adjacency-list scans and a priority queue implement Prim’s key updates, including how to recognize and discard stale entries.

Intermediate 9 min read

What You'll Learn

  • Initialize connection keys by scanning the chosen root’s adjacency list.
  • Maintain each outside vertex’s cheapest known edge into the growing tree.
  • Use a min-priority queue with lazy insertion instead of decrease-key.
  • Identify stale entries and explain why they cannot change the selected vertex.
  • Trace Prim’s implementation on graphs with updates and tied weights.
  • Detect when the queue empties before a disconnected graph is spanned.

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.

Definition: For a current tree vertex set \(T\) and an outside vertex \(v\notin T\), the key of \(v\) is the minimum weight of an edge joining \(v\) to a vertex in \(T\), if such an edge exists. If no such edge exists, its key is \(+\infty\). The parent of \(v\), when its key is finite, is a vertex \(u\in T\) such that \(uv\) has weight \(\operatorname{key}[v]\). The parent records which edge will add \(v\) to the tree.

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.

1
Initialize.
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.
2
Take a valid minimum.
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.
3
Add the vertex and scan its neighbors.
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.
4
Repeat.
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.

Proposition (key invariant): After initialization, and after each iteration has added a vertex and scanned its adjacency list, every outside vertex has the minimum weight of an edge to the current tree as its key, with a parent realizing that minimum whenever it is finite.

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\).

Lemma (stale-entry test): Removing and discarding an entry whose vertex is already in \(T\), or whose recorded key differs from that vertex’s current key, cannot change the next vertex selected by Prim’s rule.

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)\).

ActionTree vertices after scanQueue after scan, in increasing orderKey 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 unnecessaryNo 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.

  1. Why is the key invariant stated only after the root’s adjacency list has been scanned?
  2. 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?
  3. What should the algorithm do when an examined edge offers a key equal to the neighbor’s current key?
  4. 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\)?
  5. What does an empty queue before all vertices have been added tell you about the graph?