DSA Graphs
Prim's Algorithm
The campus backbone already lives as an adjacency matrix. You need a spanning tree of minimum cable cost, grown from the data center outward, always taking a light edge across the current .
Prim maintains a S and extends it with the cheapest edge into V∖S. Heap form uses a ; pushes edges and skips stale pops.
Algorithm journey
Start S = {s}
Arbitrary seed in an undirected connected graph.
Safe by the cut property.
Add vertex to S
Update keys / push new edges.
Stop at V−1 edges
Or report a forest if stuck early.
What actually happens
Naive idea: from the seed, always follow the globally cheapest unused edge in the whole graph. Breaks: that edge may lie entirely outside S and create a disconnected mess, or close a cycle inside S.
Better idea: only consider the cut (S, V∖S). The lightest crossing edge is MST-safe. Grow S until it spans the component.
The improved design still fails if the graph is directed, if you forget to skip edges into S (cycles), or if you use heap Prim on a tiny dense matrix where O(V²) scanning is simpler and faster.
Invariant
The edges inside S form a subtree of some MST. Each expansion adds a cut-light edge, so the claim remains true after every growth step.
How to choose Prim
1. Dense adjacency? Matrix Prim O(V²) often beats sorting all edges.
2. Sparse lists? Binary-heap Prim O(E log V), or prefer Kruskal.
3. Need online growth from a site? Prim's cut view matches the story.
Quantitative check
V=5·10³ dense: matrix Prim ≈ 2.5×10⁷ edge probes. Building an explicit edge list of size ~V²/2 already costs huge memory before Kruskal sorts.
Interactive demo
Priority Queue (Available Edges):
Algorithm steps
Initialize
Start with an arbitrary vertex. Mark it as visited and add all its edges to a priority queue (min-heap).
Select Minimum Edge
Extract the edge with minimum weight from the priority queue that connects to an unvisited vertex.
Add to MST
Add this edge to the MST and mark the new vertex as visited. Add all edges from this new vertex to unvisited vertices to the priority queue.
Repeat
Repeat steps 2-3 until all vertices are visited (MST has V-1 edges).
💡 Pro Tip: When to Use Prim's
Prim's is especially attractive when the graph already comes as adjacency data. For a truly dense graph, the adjacency-matrix version runs in O(V²) and avoids heap overhead; binary-heap Prim runs in O(E log V). For a sparse edge list, Kruskal's is often the simpler engineering choice.
💻 Python and Java implementation
Python
import heapq
def prims_mst(graph, start=0):
"""
Find Minimum Spanning Tree using Prim's Algorithm
Args:
graph: Adjacency list { node: [(neighbor, weight), ...] }
start: Starting vertex (default 0)
Returns:
List of edges in MST: [(from, to, weight), ...]
"""
mst = [] # Store MST edges
visited = set([start]) # Track visited vertices
# Priority queue: (weight, from_vertex, to_vertex)
edges = [
(weight, start, neighbor)
for neighbor, weight in graph[start]
]
heapq.heapify(edges) # Convert to min-heap
while edges and len(visited) < len(graph):
weight, frm, to = heapq.heappop(edges)
# Skip if vertex already visited (would create cycle)
if to in visited:
continue
# Add edge to MST
visited.add(to)
mst.append((frm, to, weight))
# Add all edges from new vertex to unvisited vertices
for neighbor, edge_weight in graph[to]:
if neighbor not in visited:
heapq.heappush(edges, (edge_weight, to, neighbor))
return mst
# Example Usage
graph = {
0: [(1, 4), (2, 2)],
1: [(0, 4), (2, 1), (3, 5)],
2: [(0, 2), (1, 1), (3, 8), (4, 10)],
3: [(1, 5), (2, 8), (4, 2)],
4: [(2, 10), (3, 2)]
}
mst = prims_mst(graph, start=0)
print("MST Edges:", mst)
# Output: [(0, 2, 2), (2, 1, 1), (1, 3, 5), (3, 4, 2)]
total_weight = sum(weight for _, _, weight in mst)
print(f"Total MST Weight: {total_weight}")
# Output: Total MST Weight: 10Java
import java.util.*;
class Edge implements Comparable<Edge> {
int weight;
int from;
int to;
public Edge(int weight, int from, int to) {
this.weight = weight;
this.from = from;
this.to = to;
}
@Override
public int compareTo(Edge other) {
return Integer.compare(this.weight, other.weight);
}
}
public class Prims {
public static void primsMST(Map<Integer, List<int[]>> graph, int start) {
List<String> mst = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
PriorityQueue<Edge> pq = new PriorityQueue<>();
// Initialize with start node
visited.add(start);
if (graph.containsKey(start)) {
for (int[] neighbor : graph.get(start)) {
pq.offer(new Edge(neighbor[1], start, neighbor[0]));
}
}
int totalWeight = 0;
while (!pq.isEmpty() && visited.size() < graph.size()) {
Edge edge = pq.poll();
if (visited.contains(edge.to)) {
continue;
}
visited.add(edge.to);
mst.add("(" + edge.from + ", " + edge.to + ", " + edge.weight + ")");
totalWeight += edge.weight;
if (graph.containsKey(edge.to)) {
for (int[] neighbor : graph.get(edge.to)) {
if (!visited.contains(neighbor[0])) {
pq.offer(new Edge(neighbor[1], edge.to, neighbor[0]));
}
}
}
}
System.out.println("MST Edges: " + mst);
System.out.println("Total MST Weight: " + totalWeight);
if (visited.size() != graph.size()) {
System.out.println("No MST: output covers only the start component");
}
}
public static void main(String[] args) {
// Graph representation: Map<Node, List<[Neighbor, Weight]>>
Map<Integer, List<int[]>> graph = new HashMap<>();
// Helper to add edges easily
graph.put(0, Arrays.asList(new int[]{1, 4}, new int[]{2, 2}));
graph.put(1, Arrays.asList(new int[]{0, 4}, new int[]{2, 1}, new int[]{3, 5}));
graph.put(2, Arrays.asList(new int[]{0, 2}, new int[]{1, 1}, new int[]{3, 8}, new int[]{4, 10}));
graph.put(3, Arrays.asList(new int[]{1, 5}, new int[]{2, 8}, new int[]{4, 2}));
graph.put(4, Arrays.asList(new int[]{2, 10}, new int[]{3, 2}));
primsMST(graph, 0);
// Output:
// MST Edges: [(0, 2, 2), (2, 1, 1), (1, 3, 5), (3, 4, 2)]
// Total MST Weight: 10
}
}🎯 Key Insight: Greedy Choice Property
Prim's algorithm works because of the greedy choice property: at each step, choosing the minimum weight edge that doesn't create a cycle is guaranteed to be part of some MST. This is proven by the cut property in graph theory!
⚡ Complexity Analysis
Time Complexity
- •O(E log V) with binary heap
Each edge operation takes O(log V)
- •O(E + V log V) with Fibonacci heap
Faster for dense graphs
Space Complexity
- •O(V + E)
For storing graph and priority queue
✅ Pros & ❌ Cons
✅ Advantages
- ✓Better for dense graphs (many edges)
- ✓Simpler to implement than Kruskal's
- ✓Works well with adjacency list representation
- ✓Can start from any vertex
❌ Limitations
- ✗Often less convenient than Kruskal's when input is already a sparse edge list
- ✗Requires priority queue implementation
- ✗A single run covers only the start component when the graph is disconnected
- ✗Must restart from every unvisited vertex to return a minimum spanning forest
💼 Real World Application: Network Design
Telecommunications companies use Prim's algorithm to design fiber optic networks. Starting from a central hub, they expand the network by adding the cheapest connection to reach each new building, ensuring all locations are connected with minimum cable cost. This is especially efficient in urban areas where buildings are densely packed!
Fibonacci heap nuance & lazy heaps
With a binary heap, Prim is typically O(E log V). Theoretical improvements use Fibonacci heaps to support decrease-key in amortized O(1), yielding O(E + V log V): rarely implemented in practice due to complexity constants. Many libraries use a lazy priority queue (push duplicate entries, skip stale pops) instead of an explicit decrease-key.
The invariant that matters
State
One connected active set S
The accepted edges form a tree spanning exactly S. Initially S contains one seed.
Choice
Lightest edge crossing (S, V−S)
An edge fully inside S is stale. An edge fully outside S cannot grow the current tree.
Guarantee
The edge is safe by the cut property
Exchange it into an MST containing prior choices; the replacement cannot increase total weight.
Lazy edge heap, eager key heap, or matrix scan?
| Variant | Heap / key state | Stale handling | Time | When it shines |
|---|---|---|---|---|
| Lazy edge heap | Push candidate (weight, from, to) edges; duplicates allowed | On pop, skip when to is already active | O(E log E) = O(E log V) for a simple graph | Shortest robust interview implementation |
| Eager best-key heap | best[v] and parent[v]; conceptually decrease-key | Without native decrease-key, push a new pair and skip when popped key ≠ best[v] | O(E log V) | Per-vertex keys and bounded logical queue state |
| Adjacency matrix | Scan all inactive vertices for minimum key | No heap and no stale entries | O(V²) | Dense or implicit complete graphs |
PriorityQueue has no efficient decrease-key operation. Pushing a better candidate is correct as long as a stale pop is verified before acceptance.Worked heap trace from vertex 0
| Step | Active set after step | Valid minimum popped | Running total | Important heap effect |
|---|---|---|---|---|
| Seed | {0} | n/a | 0 | Push 0–2(2), 0–1(4) |
| 1 | {0,2} | 0–2(2) | 2 | Push 2–1(1), 2–3(8), 2–4(10) |
| 2 | {0,1,2} | 2–1(1) | 3 | 0–1(4) later pops stale; push 1–3(5) |
| 3 | {0,1,2,3} | 1–3(5) | 8 | Push 3–4(2) |
| 4 | {0,1,2,3,4} | 3–4(2) | 10 | Four edges = V−1; stop |
Disconnected-safe lazy Prim in Java
Restarting from every unvisited seed yields a minimum spanning forest. The result exposes its component count and never labels a disconnected forest an MST. Candidate weights and the total use long; deterministic tie-breakers make equal-weight output reproducible.
import java.util.*;
public final class PrimsMinimumSpanningForest {
public record Arc(int to, long weight) {}
public record Edge(int from, int to, long weight) {}
public record Result(
List<Edge> edges, long totalWeight,
int components, boolean connected) {}
private record Candidate(int from, int to, long weight) {}
public static Result build(List<List<Arc>> graph) {
int n = graph.size();
boolean[] active = new boolean[n];
List<Edge> forest = new ArrayList<>();
long total = 0L;
int components = 0;
PriorityQueue<Candidate> heap = new PriorityQueue<>(
Comparator.comparingLong(Candidate::weight)
.thenComparingInt(Candidate::from)
.thenComparingInt(Candidate::to));
for (int seed = 0; seed < n; seed++) {
if (active[seed]) continue;
components++;
heap.offer(new Candidate(-1, seed, 0L));
while (!heap.isEmpty()) {
Candidate candidate = heap.poll();
int vertex = candidate.to();
if (active[vertex]) continue; // stale edge into the tree
active[vertex] = true; // mark only on a valid pop
if (candidate.from() != -1) {
forest.add(new Edge(
candidate.from(), vertex, candidate.weight()));
total = Math.addExact(total, candidate.weight());
}
for (Arc arc : graph.get(vertex)) {
if (arc.to() < 0 || arc.to() >= n) {
throw new IllegalArgumentException("invalid neighbor");
}
if (!active[arc.to()]) {
heap.offer(new Candidate(vertex, arc.to(), arc.weight()));
}
}
}
}
boolean connected = n == 0 || components == 1;
// A correct forest has n - components edges.
if (forest.size() != n - components) throw new AssertionError("not a forest");
return new Result(List.copyOf(forest), total, components, connected);
}
}The adjacency list must represent every undirected edge in both directions. With lazy duplicates the heap can hold O(E) candidates; the runtime is O(E log E), conventionally written O(E log V) for simple graphs because E ≤ V². Graph storage plus heap uses O(V + E) space.
FAANG-style recognition and failure modes
Reach for Prim when…
- • The question minimizes the total cost to connect all vertices, not routes from one source.
- • Neighbors are generated naturally from each chosen vertex.
- • The graph is dense or implicit, such as connecting all points by Manhattan distance.
- • Adding a virtual vertex converts choices such as “build locally or connect” into ordinary edges.
Bugs to call out aloud
- • Running on a directed or one-way adjacency list: the MST problem requires undirected edges.
- • Marking a vertex when pushed, which can freeze a non-minimum incoming edge.
- • Forgetting stale-pop checks or adding weight before that check.
- • Returning V−1 as a goal without verifying reachability; isolated vertices require forest semantics.
- • Using
intfor total cost or subtraction inside a comparator.
Start vertex: on a connected graph it does not change the optimum total, but ties can change which valid MST is returned. Negative and zero-weight edges are fine; self-loops become stale immediately, and parallel edges are handled naturally.
Short takeaway
Prim grows an MST from a seed by always taking a lightest edge across the tree cut. Use matrix Prim on dense graphs and a heap (lazy or key-based) on sparse ones.
Exercises
- Trace Prim on a 5-vertex graph where the globally cheapest edge is not incident to the seed. Which edge is chosen first, and why is that still safe?
- Compare lazy Prim vs key-based Prim on a vertex that receives three improving crossing edges. Which heap entries become stale?
- When is O(V²) Prim preferable to Kruskal at V=3000?
Primary sources
- Prim, R. C. (1957): Shortest connection networks and some generalizations.
- Cormen et al., CLRS: Prim's algorithm and the cut property.
- Sedgewick & Wayne: Lazy vs eager Prim implementations.