SwadhyAI Notes

All DSA notes

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

1

Start S = {s}

Arbitrary seed in an undirected connected graph.

2

Safe by the cut property.

3

Add vertex to S

Update keys / push new edges.

4

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

MST Weight: 0
Starting from vertex 0

Priority Queue (Available Edges):

No available edges
42158102
0
1
2
3
4
Visited Vertex
Unvisited Vertex
MST Edge
Current Edge

Algorithm steps

1

Initialize

Start with an arbitrary vertex. Mark it as visited and add all its edges to a priority queue (min-heap).

2

Select Minimum Edge

Extract the edge with minimum weight from the priority queue that connects to an unvisited vertex.

3

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.

4

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

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: 10

Java

java
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.

Loop invariant: immediately before each extraction, the accepted edges are contained in some MST of the current connected component. Accepting a minimum crossing edge preserves that statement. Once every component vertex is active, the accepted tree itself is an MST.

Lazy edge heap, eager key heap, or matrix scan?

Prim algorithm implementation strategy comparison
VariantHeap / key stateStale handlingTimeWhen it shines
Lazy edge heapPush candidate (weight, from, to) edges; duplicates allowedOn pop, skip when to is already activeO(E log E) = O(E log V) for a simple graphShortest robust interview implementation
Eager best-key heapbest[v] and parent[v]; conceptually decrease-keyWithout 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 matrixScan all inactive vertices for minimum keyNo heap and no stale entriesO(V²)Dense or implicit complete graphs
Lazy means intentionally duplicated. Java's PriorityQueue has no efficient decrease-key operation. Pushing a better candidate is correct as long as a stale pop is verified before acceptance.
Do not mark on push. A vertex can receive a cheaper edge later. Mark it active only when its minimum valid candidate is popped and accepted.

Worked heap trace from vertex 0

Worked Prim trace for the graph used in the reference implementations
StepActive set after stepValid minimum poppedRunning totalImportant heap effect
Seed{0}n/a0Push 0–2(2), 0–1(4)
1{0,2}0–2(2)2Push 2–1(1), 2–3(8), 2–4(10)
2{0,1,2}2–1(1)30–1(4) later pops stale; push 1–3(5)
3{0,1,2,3}1–3(5)8Push 3–4(2)
4{0,1,2,3,4}3–4(2)10Four 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.

java
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 int for 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

  1. 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?
  2. Compare lazy Prim vs key-based Prim on a vertex that receives three improving crossing edges. Which heap entries become stale?
  3. 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.