The eight towns of the last lesson, to be joined by the cheapest set of roads. Watch a tree grow from town 0, one cheapest road at a time.
Growing the cheapest network
The same eight towns, joined at the least total cost
bool in_tree[V] = { false }; int total = 0, added = 0; heap_push((struct item){ 0, START, -1 }); printf("added:"); while (size > 0 && added < V) { struct item x = heap_pop(); if (in_tree[x.v]) continue; in_tree[x.v] = true; added++; total += x.key; if (x.from >= 0) printf(" %d-%d (%d)", x.from, x.v, x.key); for (const struct wnode *e = adj[x.v]; e != NULL; e = e->next) if (!in_tree[e->to]) heap_push((struct item){ e->w, e->to, x.v }); } printf("\ntotal %d, %d pushes\n", total, pushes); for (int u = 0; u < V; u++) {The same eight towns and twelve roads as before.
- total
- –
- in the tree
- –
- popped
- –
Output
Describe this picture
The picture has two parts, one above the other. On top are the eight towns of Dijkstra’s algorithm, in the same places, with the same twelve roads and their lengths: 0–1 4, 0–2 2, 1–2 1, 1–3 5, 2–3 8, 2–4 10, 3–4 2, 3–5 6, 4–5 3, 4–6 4, 5–7 1 and 6–7 2. Under them is a strip of boxes, heap, the program’s binary heap in array order, each box holding an item as “length, town”; empty places are dashed. Under the picture are the lines of prim.c the clip runs, with the line about to run marked, one sentence on the current step, three readouts (the total length so far, how many towns are in the tree, and the item last taken from the heap), the control, and the output. The clip plays once, about 20 seconds. Town 0 joins first. Then, again and again, each road from the town that just joined to a town outside the tree lights as a candidate, and its far town turns to the frontier tone, as a new item appears in the strip; the item with the shortest road leaves the heap, its town is ringed and turns to the settled tone, and its road turns to the tree tone while the total climbs. A road whose two towns are both in the tree, and is not a tree road, fades. The towns join in the order 0, 2, 1, 3, 4, 5, 7, 6. Once, the item (4, 1), the road 0–1, comes out after town 1 has joined; the readout calls it stale, and it is skipped. The clip rests on the output, added: 0-2 (2) 2-1 (1) 1-3 (5) 3-4 (2) 4-5 (3) 5-7 (1) 7-6 (2) and total 16, 13 pushes, with four items still in the strip, never taken. The control, “Start town”, is a slider over the 8 towns: a new start grows the tree in a different order and reaches the same seven roads and the same total, 16. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as prim.step, and the start as prim.start.
Joining every town
Suppose each road can be built at a cost equal to its length, and every town must be reachable from every other. The cheapest choice never includes a loop: one road of a loop could be dropped and every town would still be connected. So the answer is a spanning tree, a set of roads that joins all V towns with no loop. A tree on V towns has exactly V − 1 roads. Its weight is the sum of their lengths, and the goal is a spanning tree of least weight.
Prim’s algorithm grows one tree. It starts with a single town, and at every step adds the cheapest road that leads from a town in the tree to a town outside it.
The code and its cost
The program
Everything before main in prim.c is the same as in dijkstra.c: the towns, the roads, add_arc, and the binary heap of items ordered by key, then town. Only the key changes. In Dijkstra’s algorithm an item’s key was the whole distance from the start; here it is the length of the one road that would join the town:
while (size > 0 && added < V) {
struct item x = heap_pop();
if (in_tree[x.v])
continue;
in_tree[x.v] = true;
added++;
total += x.key;
if (x.from >= 0)
printf(" %d-%d (%d)", x.from, x.v, x.key);
for (const struct wnode *e = adj[x.v]; e != NULL; e = e->next)
if (!in_tree[e->to])
heap_push((struct item){ e->w, e->to, x.v });
}
Each town that joins offers every road out of it to a town not yet in the tree. The heap holds all the roads on offer, and gives back the shortest. An item whose town has joined since it was pushed is stale: its road would close a loop, and it is skipped. The loop stops as soon as all eight towns are in, so items still on the heap are never taken.
added: 0-2 (2) 2-1 (1) 1-3 (5) 3-4 (2) 4-5 (3) 5-7 (1) 7-6 (2)
total 16, 13 pushes
Why the cheapest road is safe
Split the towns into two groups, any way at all. The cut property says that if one road crossing between the groups is shorter than every other crossing road, it belongs to every least-cost spanning tree. If a tree left it out, adding it would close a loop that crosses between the groups somewhere else too; dropping that other, longer crossing road would give a spanning tree of smaller weight. Prim’s algorithm uses this split at every step: the towns in the tree against the towns outside it.
Here the tree is the only least-cost one: each of the five roads left out is longer than every tree road on the loop it would close. So every start finds the same seven roads.
Heap or array
With a heap, a road is pushed only from the end that joins first; when its other end joins, the first is already in the tree. So there are at most E + 1 pushes, counting the start, and as many pops, each about log E steps: O(E log V), since log E is less than 2 log V. Here E + 1 = 13, and the program makes 13 pushes.
Without a heap, an array can hold each outside town’s cheapest road into the tree. Each step scans the array for the least, V steps, and the joining town updates its neighbours’ entries. V steps of V each is O(V²), with no log. When nearly every pair of towns is joined, E is close to V²/2 and the array version is faster.
Prim against Dijkstra
The two programs share every line but one. Dijkstra’s algorithm settles the town nearest to the start, adding up distances along routes. Prim’s algorithm adds the town nearest to the tree, looking at one road at a time. The tree of shortest routes and the least-cost tree are different trees in general.
Running it yourself
gcc -std=c11 -Wall -Wextra -pedantic prim.c -o prim
./prim
gcc -std=c11 -Wall -Wextra -pedantic -DSTART=5 prim.c -o prim5
./prim5
Worked example
Least cost is not shortest route. In the tree, the route from 0 to 7 is 0–2–1–3–4–5–7, 2 + 1 + 5 + 2 + 3 + 1 = 14, the same as Dijkstra’s shortest. But the tree’s route from 0 to 6 goes round through 7: 14 + 2 = 16. The shortest route, 0–2–1–3–4–6, costs 2 + 1 + 5 + 2 + 4 = 14 and uses the road 4–6, which the tree leaves out.
The same tree from every start. From town 5 the roads join in the order 5–7, 7–6, 5–4, 4–3, 3–1, 1–2, 2–0: the same seven roads, total 16.
A dense graph. With V = 1,000 towns and every pair joined, E = 1,000 · 999 / 2 = 499,500 roads. The array version takes about V² = 10⁶ steps. The heap version takes about E log₂ V ≈ 499,500 × 10 ≈ 5 × 10⁶.
Two checks. Why is the road 0–1, length 4, never added? (By the time its item is the shortest on offer, town 1 has joined through 2–1.) How many roads are in a spanning tree of 100 towns? (99.)
Where it’s used
Networks at least cost. Laying out cable, pipes or roads so that every site is connected and the total length is as small as possible.
Circuit wiring. Joining the points that must share a signal with the least total wire.
Clustering. Building the least-cost tree over data points and cutting its longest roads splits the points into groups.
Reference card
| Code or idea | Meaning |
|---|---|
| spanning tree | V − 1 roads joining all V towns, no loop |
| weight | the sum of its road lengths |
| cut property | the cheapest road across a split is in every least-cost tree |
key = e->w | Prim: the one road that would join the town |
key = x.key + e->w | Dijkstra: the distance from the start |
if (in_tree[x.v]) continue; | a stale item: its road would close a loop |
| binary heap | O(E log V) |
| array of cheapest roads | O(V²), better for dense graphs |
Platform: C11, gcc on x86-64 Linux: int 4 bytes, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.