Minimum spanning tree
In short: A minimum spanning tree connects all vertices of a graph at the lowest total cost; Kruskal's algorithm takes edges by cost and skips every edge that would close a cycle.
Problem
The cheapest network
The delivery company has six offices in the city and wants to connect them with fibre. Every office must reach every other one, directly or through other offices. Digging is expensive: every possible cable has a price in thousands of euros (k€).
List<String> offices = List.of("Centre", "North", "South", "East", "West", "Airport");List<Cable> cables = List.of( new Cable("Centre", "North", 4), new Cable("Centre", "South", 6), new Cable("Centre", "East", 8), new Cable("Centre", "West", 5), new Cable("North", "East", 9), new Cable("North", "West", 3), new Cable("South", "West", 7), new Cable("South", "Airport", 11), new Cable("East", "Airport", 2));Laying all nine cables costs 55 k€. But you do not need them all. If three offices are connected in a ring, one cable of the ring is extra: remove it and everything is still connected. The cheapest network has no cycle at all. A connected graph without a cycle is a tree, and a tree with V vertices has exactly V − 1 edges: here 5 cables.
Which five? You are looking for the tree that spans all offices at the lowest total price. That is a minimum spanning tree (MST).
The idea
The cheapest cable that does not close a cycle
Kruskal’s algorithm (Joseph Kruskal, 1956):
- Sort the cables by price, cheapest first.
- For each cable: if its two ends are not connected yet, lay it. If they are already connected through laid cables, the cable would close a cycle: skip it.
- Stop once there are V − 1 cables.
At the start every office is a group of its own. Every laid cable merges two groups into one. The question “are the two ends connected” is exactly the question “are they in one group”, and there is a structure for that: union-find. union(a, b) merges the groups and returns false if a and b are already in one group. One check instead of a search over the map.
See it
Kruskal from the inside
The UnionFind class is the same as in the union-find lesson, unchanged. A cable is a record with two offices and a price, and index turns an office name into a number, because union-find works with numbers.
static List<Cable> kruskal( List<String> offices, List<Cable> cables) { Map<String, Integer> index = new HashMap<>(); for (String o : offices) index.put(o, index.size()); List<Cable> sorted = new ArrayList<>(cables); sorted.sort(Comparator.comparingInt(Cable::cost)); var groups = new UnionFind(offices.size()); List<Cable> tree = new ArrayList<>(); for (Cable c : sorted) { int a = index.get(c.from()); int b = index.get(c.to()); if (!groups.union(a, b)) continue; // a cycle tree.add(c); if (tree.size() == offices.size() - 1) break; } return tree;}sort with Comparator.comparingInt(Cable::cost) is stable: cables with the same price keep their order.int total = 0;for (Cable c : kruskal(offices, cables)) { System.out.println(c.from() + " – " + c.to() + ": " + c.cost() + " k€"); total += c.cost();}System.out.println("Total: " + total + " k€");East – Airport: 2 k€North – West: 3 k€Centre – North: 4 k€Centre – South: 6 k€Centre – East: 8 k€Total: 23 k€static List<Cable> kruskal( List<String> offices, List<Cable> cables) { Map<String, Integer> index = new HashMap<>(); for (String o : offices) index.put(o, index.size()); List<Cable> sorted = new ArrayList<>(cables); sorted.sort(Comparator.comparingInt(Cable::cost)); var groups = new UnionFind(offices.size()); List<Cable> tree = new ArrayList<>(); for (Cable c : sorted) { int a = index.get(c.from()); int b = index.get(c.to()); if (!groups.union(a, b)) continue; // a cycle tree.add(c); if (tree.size() == offices.size() - 1) break; } return tree;}Step by step, in words
- Six offices, nine possible cables, each with a price in thousands of euros. The network has to connect every office as cheaply as possible. On the right the cables get sorted by price: Kruskal looks at them cheapest first. Every office starts as its own group.
- East – Airport, 2: different groups. The cable is laid and union merges the two groups into one (same colour).
- North – West, 3: different groups again. A second, separate group appears: Kruskal builds several pieces at once, not one tree that grows.
- Centre – North, 4: the Centre joins the group of the North and the West.
- Centre – West, 5: both ends are already in one group, through the North. This cable would close a cycle: union returns false and the cable is rejected.
- Centre – South, 6: the South is alone, so the cable is laid.
- South – West, 7: the South and the West are already connected through the Centre. Rejected.
- Centre – East, 8: the cheapest cable between the two groups. It merges them, and the cables are now five: six offices, five cables, everything is connected.
- The loop stops: North – East and South – Airport are never looked at. Total 23 thousand €, against 55 for all nine cables.
Two groups grow separately: the blue one (East, Airport) and the green one, which starts from North and West. The Centre – East cable is the cheapest between them and merges them. The North – East (9) and South – Airport (11) cables are never looked at.
Try it
Predict the output
The motorway to the airport turns out to be a problem: the East – Airport cable now costs 12. What does the new network cost?
List<Cable> changed = new ArrayList<>(cables);changed.set(changed.size() - 1, new Cable("East", "Airport", 12));int total = kruskal(offices, changed).stream().mapToInt(Cable::cost).sum();System.out.println("Total: " + total + " k€");Try it
Fill in
tree.add(c);
if (tree.size() == ) break;A tree with V offices has V − 1 cables. Once there are that many, everything is connected and every further cable would close a cycle, so the loop stops.
The idea
Why greedy is right here
Kruskal is a greedy algorithm: at every step it takes the best option right now (the cheapest cable that fits) and never goes back. Greedy often lies: see when greedy works and when it fails. Here it works, and the reason is the cut property.
Split the offices into two parts in any way, say the East group and everything else. The cables that run between the two parts are the cut. The cheapest cable in the cut is in the minimum tree. Why? The tree needs at least one cable across the cut, or the two parts are not connected. If that cable is more expensive, swap it for the cheapest one: the network is still connected and the price drops.
Every cable Kruskal lays is the cheapest in the cut between the group of one of its ends and everything else: all cheaper cables have already gone by and sit inside groups. Centre – East (8) is the cheapest of Centre – East, North – East (9) and South – Airport (11), the cables between the two groups.
Try it
Find the bug
A colleague decided union is unnecessary and only asks find whether the two ends are in one group:
static List<Cable> kruskal( List<String> offices, List<Cable> cables) { Map<String, Integer> index = new HashMap<>(); for (String o : offices) index.put(o, index.size()); List<Cable> sorted = new ArrayList<>(cables); sorted.sort(Comparator.comparingInt(Cable::cost)); var groups = new UnionFind(offices.size()); List<Cable> tree = new ArrayList<>(); for (Cable c : sorted) { int a = index.get(c.from()); int b = index.get(c.to()); if (groups.find(a) == groups.find(b)) continue; tree.add(c); if (tree.size() == offices.size() - 1) break; } return tree;}int total = 0;for (Cable c : kruskal(offices, cables)) { System.out.println(c.from() + " – " + c.to() + ": " + c.cost() + " k€"); total += c.cost();}System.out.println("Total: " + total + " k€");East – Airport: 2 k€North – West: 3 k€Centre – North: 4 k€Centre – West: 5 k€Centre – South: 6 k€Total: 20 k€20 k€ instead of 23, but East and Airport are cut off, and Centre, North and West form a ring. Which line has the bug?
Pick the line with the bug.
find only asks, it does not merge. The groups never merge, every cable joins “different” groups and goes in until there are five. You need union: it both checks and merges.
The idea
What it costs
Sorting E cables is O(E log E). After that every check with union-find is almost O(1) (see the union-find lesson). In total O(E log E): the sorting is the expensive part.
Without union-find, “are they connected” is asked with DFS over the cables laid so far:
static boolean connected(Map<String, List<String>> net, String from, String to) { Deque<String> stack = new ArrayDeque<>(List.of(from)); Set<String> seen = new HashSet<>(List.of(from)); while (!stack.isEmpty()) { String office = stack.pop(); if (office.equals(to)) return true; for (String next : net.get(office)) { if (seen.add(next)) stack.push(next); } } return false;}
static List<Cable> kruskalSlow(List<String> offices, List<Cable> cables) { Map<String, List<String>> net = new HashMap<>(); for (String office : offices) net.put(office, new ArrayList<>()); List<Cable> sorted = new ArrayList<>(cables); sorted.sort(Comparator.comparingInt(Cable::cost)); List<Cable> tree = new ArrayList<>(); for (Cable c : sorted) { if (connected(net, c.from(), c.to())) continue; net.get(c.from()).add(c.to()); net.get(c.to()).add(c.from()); tree.add(c); if (tree.size() == offices.size() - 1) break; } return tree;}The answer is the same. But every search can walk the whole group, O(V), and it runs for every cable: O(E · V).
Run both versions on offices in a grid: each office can be linked to its right and lower neighbour, at a price from 1 to 1000. Every comparison of two prices while sorting is counted, every neighbour looked at by DFS and every step to a parent in find.
- Kruskal with DFS: a search over the laid cables
O(n²) - Kruskal with union-find
O(n log n)
| Algorithm | n = 100 | n = 1,000 | n = 10,000 | n = 100,000 |
|---|---|---|---|---|
| Kruskal with DFS: a search over the laid cables | 4,945 | 677,351 | ≈ 67,735,100 | ≈ 6,773,510,000 |
| Kruskal with union-find | 1,460 | 23,827 | 321,171 | 3,884,598 |
static List<Cable> kruskal( List<String> offices, List<Cable> cables) { Map<String, Integer> index = new HashMap<>(); for (String o : offices) index.put(o, index.size()); List<Cable> sorted = new ArrayList<>(cables); sorted.sort(Comparator.comparingInt(Cable::cost)); var groups = new UnionFind(offices.size()); List<Cable> tree = new ArrayList<>(); for (Cable c : sorted) { int a = index.get(c.from()); int b = index.get(c.to()); if (!groups.union(a, b)) continue; // a cycle tree.add(c); if (tree.size() == offices.size() - 1) break; } return tree;}At 1000 offices DFS takes 677 351 steps, union-find 23 827. At 100 000 offices union-find makes 3 884 598, and DFS would make billions: Bench does not run them but estimates them from the curve.
The idea
Not the shortest path
In the minimum tree the path from South to Airport is South – Centre – East – Airport: 6 + 8 + 2 = 16. The direct South – Airport cable is 11, but it is not in the network.
A minimum spanning tree saves on the total price of the network. It promises nothing about the path between two offices. If the price is delay and you want the fastest path between two offices, that is a different problem: Dijkstra.
More precisely, if you are curious
Prim is the other classic algorithm for a minimum spanning tree. It starts from one office and each time adds the cheapest cable from the tree to an office outside it. The queue is a priority queue, as in Dijkstra, except the priority is the price of the cable, not the path from the start. With a binary heap it is O(E log V). Kruskal is simpler when the cables are a list, Prim when the graph is given by neighbours and is dense.
If some cables have the same price, there may be several minimum trees, all with the same total price. Kruskal returns one of them, depending on the order of the equal ones.
Where else: power and water networks, roads between villages, circuit boards. If you stop Kruskal before the groups become one (say, at k groups), you get clustering: the points in a group are close, and the groups are far from each other.
Exercise
A cable already laid
Solve it in your editor.
An old cable. There is already a cable between North and East: it costs nothing. Write a method that also takes the laid cables and finds which new ones to add. Print the laid and the new cables and the price of the new ones:
Already laid: North – EastEast – Airport: 2 k€North – West: 3 k€Centre – North: 4 k€Centre – South: 6 k€New cables: 15 k€Hint: before the loop, merge the ends of the laid cables in union-find. Stop once there is one group.
More tasks
- The rejected ones. Also print the rejected cables, each with the reason: which cycle it would close.
- Two networks. The company wants two separate networks. Stop Kruskal when there are two groups. Which offices share a network?
- Impossible. Remove every cable to the Airport. What does Kruskal return? How do you tell that the network does not connect every office?
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Union-find · Graph: adjacency list and matrix · DFS: islands and cycles · Dijkstra
- Continues in: Greedy algorithms and when they work
- Tasks on this topic: Graphs