Topics › Graphs › MinimumSpanningTree/Disjoint Set and Problems
Prim's Algorithm
Java source ↗ Edit this article ↗
Jump to code:
Problem
Given a connected, undirected, weighted graph with V vertices numbered 0 to V - 1 and a list of edges [u, v, w], return the total weight of a minimum spanning tree: a set of V - 1 edges that connects every vertex with the smallest possible total weight. Also list the edges of that tree. (GeeksforGeeks passes the graph as an adjacency list; an edge list carries the same information and is easier to write in tests.) If the graph turns out to be disconnected, no spanning tree exists and the Java methods return -1.
Constraints (GeeksforGeeks “Minimum Spanning Tree”): 2 ≤ V ≤ 1000, V - 1 ≤ E ≤ V(V-1)/2, 1 ≤ w ≤ 1000, no self-loops or parallel edges.
Example 1
V = 3, edges = [[0,1,5], [1,2,3], [0,2,1]]
answer = 4 // edges 0-2 (1) and 1-2 (3)
Example 2
V = 5, edges = [[0,1,2], [0,2,1], [1,2,1], [2,3,2], [3,4,1], [4,2,2]]
answer = 5 // 0-2 (1), 2-1 (1), 2-3 (2) or 2-4 (2), 3-4 (1)
The idea behind Prim’s algorithm
Prim’s algorithm grows a single tree outward from a start vertex. At every step the tree covers some set S of vertices, and the algorithm adds the cheapest edge that leaves S, together with the new vertex at its far end. After V - 1 additions, every vertex is in the tree.
Why is the greedy choice safe? It is the cut property: for the cut (S, everything else), the lightest crossing edge belongs to some minimum spanning tree. Suppose the edges chosen so far are part of some MST T, but T does not use the newly chosen edge e = (u, v) with u in S. The path in T from u to v must leave S through some edge f, and w(f) ≥ w(e) because e was the cheapest edge leaving S. Replacing f by e gives a spanning tree that is no heavier and contains everything chosen so far. By induction, the final tree is an MST.
The two approaches below differ only in how they find “the cheapest edge leaving S”.
Approach 1: Array-based Prim, O(V²)
Intuition. Maintain key[v], the weight of the cheapest edge from the current tree to each outside vertex v, and parent[v], the tree vertex at the other end of that edge. Each step scans all outside vertices for the smallest key, adds that vertex, and then updates the keys of its neighbours, since the new vertex may offer cheaper connections. With an adjacency matrix the update is one pass over a row.
Algorithm.
- Build a weight matrix, keeping the lighter of any parallel edges. Set
key[0] = 0and every other key to infinity. - Repeat
Vtimes: pick the outside vertexuwith the smallest key. If that key is infinite, the graph is disconnected, so return-1. - Add
uto the tree, addkey[u]to the total, and record the edgeparent[u] - u. - For every outside
v, ifw[u][v] < key[v], setkey[v] = w[u][v]andparent[v] = u.
static int primArray(int n, int[][] edges, List<int[]> mstEdges) {
int[][] w = new int[n][n];
for (int[] row : w) Arrays.fill(row, INF);
for (int[] e : edges) { // keep the cheapest of parallel edges
w[e[0]][e[1]] = Math.min(w[e[0]][e[1]], e[2]);
w[e[1]][e[0]] = Math.min(w[e[1]][e[0]], e[2]);
}
int[] key = new int[n], parent = new int[n]; // key[v] = cheapest edge from the tree to v
boolean[] inTree = new boolean[n];
Arrays.fill(key, INF);
Arrays.fill(parent, -1);
key[0] = 0;
int total = 0;
for (int step = 0; step < n; step++) {
int u = -1;
for (int v = 0; v < n; v++) if (!inTree[v] && (u == -1 || key[v] < key[u])) u = v;
if (key[u] == INF) return -1; // nothing outside the tree is reachable
inTree[u] = true;
total += key[u];
if (parent[u] != -1) mstEdges.add(new int[]{parent[u], u, key[u]});
for (int v = 0; v < n; v++) {
if (!inTree[v] && w[u][v] < key[v]) {
key[v] = w[u][v];
parent[v] = u;
}
}
}
return total;
}
Complexity. Time O(V²): V steps, each scanning V keys and one matrix row. Space O(V²) for the matrix. On dense graphs, where E is close to V², this is optimal, because just reading the edges takes that long. On sparse graphs it wastes time scanning vertices that have no nearby edges.
Approach 2: Lazy Prim with a min-heap, O(E log E)
Intuition. Instead of scanning for the minimum key, keep candidate edges in a min-heap ordered by weight. Each entry is {weight, node, parent}: “you can attach node to the tree through parent at this cost”. When a vertex joins the tree, push one entry for each of its edges to outside vertices. When an entry is popped, the vertex may already have joined through a cheaper edge. In that case the entry is outdated and is ignored. This is the lazy version: we never delete or decrease entries, we just skip the stale ones.
Popping the smallest entry whose node is outside the tree is exactly “the cheapest edge leaving S”. Every edge from S to the outside was pushed when its tree endpoint joined, and every entry pointing into S is skipped.
Algorithm.
- Build an adjacency list. Push
{0, 0, -1}: vertex0joins at cost0with no parent. - Pop the lightest entry
{wt, u, parent}. Ifuis already in the tree, skip it. - Otherwise mark
u, addwtto the total, record edgeparent - u(unlessparentis-1), and push{w, v, u}for every neighbourvnot yet in the tree. - When the heap is empty, return the total if all
Vvertices joined, else-1.
static int primHeap(int n, int[][] edges, List<int[]> mstEdges) {
List<List<int[]>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(new int[]{e[1], e[2]});
adj.get(e[1]).add(new int[]{e[0], e[2]});
}
boolean[] inTree = new boolean[n];
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
pq.offer(new int[]{0, 0, -1}); // start anywhere: vertex 0 with no parent
int total = 0, added = 0;
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int wt = cur[0], u = cur[1], parent = cur[2];
if (inTree[u]) continue; // a cheaper edge already pulled u in
inTree[u] = true;
total += wt;
added++;
if (parent != -1) mstEdges.add(new int[]{parent, u, wt});
for (int[] e : adj.get(u)) {
if (!inTree[e[0]]) pq.offer(new int[]{e[1], e[0], u});
}
}
return added == n ? total : -1;
}
Dry run on Example 1 (0-1 (5), 1-2 (3), 0-2 (1)):
| pop | in tree already? | action | heap after |
|---|---|---|---|
| {0, 0, -1} | no | total = 0; push {5,1,0}, {1,2,0} | {1,2,0}, {5,1,0} |
| {1, 2, 0} | no | total = 1, edge 0-2; push {3,1,2} | {3,1,2}, {5,1,0} |
| {3, 1, 2} | no | total = 4, edge 2-1 | {5,1,0} |
| {5, 1, 0} | yes | skip | empty |
The total is 4, with edges 0-2 and 2-1.
Complexity. Time O(E log E): each undirected edge is pushed at most twice (once from each endpoint), and each push or pop costs O(log E). Since E ≤ V², log E ≤ 2 log V, so this is also written O(E log V). Space O(V + E) for the adjacency list and the heap.
An eager variant keeps at most one heap entry per vertex and decreases its key in place (an indexed heap), which bounds the heap at V entries. Java’s PriorityQueue has no decrease-key operation, so the lazy version is the usual choice in Java.
Choosing between the two
For sparse graphs (E around V), the heap version’s O(E log E) is far below O(V²). For dense graphs the array version avoids the heap’s overhead and log factor. Kruskal’s algorithm, which sorts all edges and uses a disjoint-set structure, is the other standard option. It is usually simpler when the input is already an edge list.
Edge cases and pitfalls
- Disconnected graph. Prim’s only spans the component of the start vertex. Check that all
Vvertices joined. The array version notices when the smallest key is infinite; the heap version counts added vertices. - Skipping stale entries. Forgetting the
if (inTree[u]) continuecheck adds the same vertex twice and overcounts the weight. - Mark on pop, not on push. Marking a vertex when it is first pushed locks in whichever edge reached it first, not the cheapest one.
- Parallel edges and self-loops. The array version keeps the lighter parallel edge. The heap version pushes both and the heavier one is skipped as stale. A self-loop points back into the tree and is ignored. The tests cover both.
- Single vertex. The MST is empty and its weight is
0. - Overflow. Totals stay small here (
V · 1000at most), but with large weights accumulate in along. - The start vertex does not matter. Any vertex gives an MST of the same weight, because the weight of an MST is unique.
Related problems
- MST theory proves the cut property that Prim’s relies on.
- Find the MST weight solves the same problem with Kruskal’s algorithm.
- Disjoint Set is the structure Kruskal’s uses instead of a heap.
- Djisktra’s Algorithm has the same heap-driven shape, but orders by distance from the source rather than by single-edge weight.