SwadhyAI Notes

All DSA notes

DSA Graphs

Kruskal's Algorithm

A courier planner hands you a CSV of candidate roads and costs, not an adjacency matrix. Sort once. Keep a road only when it merges two components; that is Kruskal with .

plus builds an MST (or a ).

Algorithm journey

1

Sort edges

Nondecreasing weight.

2

Each vertex alone.

3

Scan lightest-first

Union if different components.

4

Stop at V−1 edges

Or exhaust edges for a forest.

What actually happens

Naive idea: sort edges and take the first V−1 of them. Breaks as soon as those edges form a cycle and leave another vertex isolated.

Better idea: take an edge iff its ends are in different DSU components. That is exactly cycle rejection guided by the cycle property.

The improved design still fails without path compression / union-by-rank on huge E (slow DSU), on directed inputs, or if you assume connectivity when the edge list describes several islands.

Invariant

The accepted edges form a forest contained in some MSF. Each accepted edge merges components and is a lightest edge across that merge cut among remaining candidates in sorted order.

How to choose Kruskal

1. Sparse edge list? Kruskal is the default engineering fit.

2. Dense matrix already in memory? Consider Prim O(V²).

3. Need a forest / offline edges? Kruskal handles disconnection cleanly.

Quantitative check

E=10⁶ edges: sorting dominates (~E log E ≈ 2×10⁷ comparisons). DSU work is comparatively tiny if you use rank + compression.

Interactive demo

MST Weight: 0
Edges sorted by weight. Ready to start!

Sorted Edges (by weight):

1→2 (1)
0→2 (2)
3→4 (2)
0→1 (4)
1→3 (5)
2→3 (8)
2→4 (10)

Connected Components:

{0}
{1}
{2}
{3}
{4}
12245810
0
1
2
3
4
MST Edge
Current Edge
Rejected Edge (Cycle)
Same Component (color-coded)

Algorithm steps

1

Sort All Edges

Sort all edges in non-decreasing order of their weight. This is the most expensive step!

2

Initialize Union-Find

Create a Union-Find (Disjoint Set) data structure where each vertex starts in its own set.

3

Process Edges

For each edge (u, v) in sorted order: if u and v are in different sets (won't create cycle), add edge to MST and union the sets.

4

Stop When Complete

Stop when MST has V-1 edges. If edges are exhausted first, report the minimum spanning forest and its component count.

🔍 Did You Know? Union-Find Magic

The Union-Find data structure is the secret sauce of Kruskal's algorithm! It allows us to check if two vertices are in the same component and merge components in nearly O(1) time (amortized) using path compression and union by rank. Without it, Kruskal's would be much slower!

💻 Complete Python Implementation

Python

python
class UnionFind:
    """Union-Find (Disjoint Set) data structure with path compression and union by rank"""
    
    def __init__(self, n):
        self.parent = list(range(n))  # Each node is its own parent initially
        self.rank = [0] * n  # Rank for union by rank optimization
    
    def find(self, x):
        """Find the root of x with path compression"""
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # Path compression
        return self.parent[x]
    
    def union(self, x, y):
        """Union two sets containing x and y"""
        root_x = self.find(x)
        root_y = self.find(y)
        
        if root_x == root_y:
            return False  # Already in same set
        
        # Union by rank: attach smaller tree under larger tree
        if self.rank[root_x] < self.rank[root_y]:
            self.parent[root_x] = root_y
        elif self.rank[root_x] > self.rank[root_y]:
            self.parent[root_y] = root_x
        else:
            self.parent[root_y] = root_x
            self.rank[root_x] += 1
        
        return True


def kruskals_mst(n, edges):
    """
    Find Minimum Spanning Tree using Kruskal's Algorithm
    
    Args:
        n: Number of vertices (0 to n-1)
        edges: List of edges [(u, v, weight), ...]
    
    Returns:
        List of edges in MST: [(u, v, weight), ...]
    """
    # Step 1: Sort edges by weight
    edges.sort(key=lambda x: x[2])
    
    # Step 2: Initialize Union-Find
    uf = UnionFind(n)
    mst = []
    total_weight = 0
    
    # Step 3: Process edges in sorted order
    for u, v, weight in edges:
        # If u and v are in different components (no cycle)
        if uf.union(u, v):
            mst.append((u, v, weight))
            total_weight += weight
            
            # Stop when we have V-1 edges
            if len(mst) == n - 1:
                break
    
    return mst, total_weight


# Example Usage
n = 5  # 5 vertices (0 to 4)
edges = [
    (0, 1, 4),
    (0, 2, 2),
    (1, 2, 1),
    (1, 3, 5),
    (2, 3, 8),
    (2, 4, 10),
    (3, 4, 2)
]

mst, total_weight = kruskals_mst(n, edges)
print("MST Edges:", mst)
# Output: [(1, 2, 1), (0, 2, 2), (3, 4, 2), (1, 3, 5)]

print(f"Total MST Weight: {total_weight}")
# Output: Total MST Weight: 10

Java

java
import java.util.*;

class Edge implements Comparable<Edge> {
    int src, dest;
    long weight;

    public Edge(int src, int dest, long weight) {
        this.src = src;
        this.dest = dest;
        this.weight = weight;
    }

    @Override
    public int compareTo(Edge other) {
        int byWeight = Long.compare(this.weight, other.weight);
        if (byWeight != 0) return byWeight;
        int bySource = Integer.compare(this.src, other.src);
        return bySource != 0 ? bySource : Integer.compare(this.dest, other.dest);
    }
}

class UnionFind {
    int[] parent, rank;

    public UnionFind(int n) {
        parent = new int[n];
        rank = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            rank[i] = 0;
        }
    }

    public int find(int i) {
        if (parent[i] != i) {
            parent[i] = find(parent[i]);
        }
        return parent[i];
    }

    public boolean union(int i, int j) {
        int rootI = find(i);
        int rootJ = find(j);

        if (rootI != rootJ) {
            if (rank[rootI] < rank[rootJ]) {
                parent[rootI] = rootJ;
            } else if (rank[rootI] > rank[rootJ]) {
                parent[rootJ] = rootI;
            } else {
                parent[rootJ] = rootI;
                rank[rootI]++;
            }
            return true;
        }
        return false;
    }
}

public class Kruskals {
    public static void kruskalsMST(int V, List<Edge> edges) {
        Collections.sort(edges);
        UnionFind uf = new UnionFind(V);
        List<Edge> mst = new ArrayList<>();
        long totalWeight = 0L;

        for (Edge edge : edges) {
            if (uf.union(edge.src, edge.dest)) {
                mst.add(edge);
                totalWeight = Math.addExact(totalWeight, edge.weight);
                if (mst.size() == Math.max(0, V - 1)) break;
            }
        }

        System.out.println("MST Edges:");
        for (Edge edge : mst) {
            System.out.println("(" + edge.src + ", " + edge.dest + ", " + edge.weight + ")");
        }
        System.out.println("Total MST Weight: " + totalWeight);
        if (mst.size() != Math.max(0, V - 1)) {
            System.out.println("No MST: returned a minimum spanning forest");
        }
    }

    public static void main(String[] args) {
        int V = 5;
        List<Edge> edges = new ArrayList<>();
        edges.add(new Edge(0, 1, 4));
        edges.add(new Edge(0, 2, 2));
        edges.add(new Edge(1, 2, 1));
        edges.add(new Edge(1, 3, 5));
        edges.add(new Edge(2, 3, 8));
        edges.add(new Edge(2, 4, 10));
        edges.add(new Edge(3, 4, 2));

        kruskalsMST(V, edges);
        // Output:
        // MST Edges:
        // (1, 2, 1)
        // (0, 2, 2)
        // (3, 4, 2)
        // (1, 3, 5)
        // Total MST Weight: 10
    }
}

🎯 Key Insight: Edge-Centric Greedy Approach

Unlike Prim's (which is vertex-centric), Kruskal's is edge-centric. It considers all edges globally and picks the smallest ones first, regardless of which vertices they connect. This makes it perfect for sparse graphs where sorting edges is cheaper than maintaining a priority queue of vertices!

⚡ Complexity Analysis

Time Complexity

  • •
    O(E log E) overall

    Dominated by sorting edges

  • •
    O(E log V) upper-bound form

    Because log E = O(log V) for a simple graph

  • •
    O(E α(V)) for Union-Find

    α is inverse Ackermann (nearly constant)

Space Complexity

  • •
    O(V + E)

    For storing edges and Union-Find structure

🔄 Kruskal's vs Prim's

AspectKruskal'sPrim's
ApproachEdge-centric (global)Vertex-centric (local)
Data StructureUnion-FindPriority Queue
Best ForSparse graphs (few edges)Dense graphs (many edges)
Time ComplexityO(E log E)O(E log V)
Graph RepresentationEdge listAdjacency list

✅ Pros & ❌ Cons

✅ Advantages

  • ✓Better for sparse graphs (fewer edges)
  • ✓Works on disconnected graphs (finds MST forest)
  • ✓Sorting can use parallel or external-memory machinery
  • ✓Works well with edge list representation

❌ Limitations

  • ✗Requires sorting all edges (expensive for dense graphs)
  • ✗Needs Union-Find implementation
  • ✗Slower than Prim's for very dense graphs
  • ✗Not suitable for adjacency matrix representation

💡 Pro Tip: When to Choose Kruskal's

Choose Kruskal's when you have a sparse graph (E is much less than V²) or when edges are already sorted/partially sorted. It's also great for finding MST in disconnected graphs, where it naturally produces a minimum spanning forest!

The forest invariant and correctness proof

Invariant

Chosen edges form an extendable forest

They are acyclic, and some minimum spanning forest contains every edge accepted so far.

Accept

Different DSU representatives

One current component defines a cut. The globally lightest eligible edge crossing it is safe by the cut property.

Reject

Same DSU representative

A path already connects the endpoints; adding this edge closes a cycle and cannot be required for connectivity.

Exchange step: suppose the next accepted edge e is absent from an optimal forest T that contains prior choices. Adding e to T creates a cycle containing an edge f that crosses the same component cut. Because edges are processed nondecreasingly, w(e) ≤ w(f); replacing f with e preserves optimality and the invariant.

DSU mechanics: what makes cycle checks nearly constant

Disjoint-set optimizations used by Kruskal algorithm
OperationInvariantOptimizationCommon bug
find(x)Returns the representative of x's componentPath compression/halving rewrites ancestors toward the rootComparing immediate parents instead of representatives
union(a,b)Merges exactly two different componentsUnion-by-size attaches the smaller root below the larger rootUpdating size on a non-root or decrementing count on a failed union
component countStarts at V; each successful union subtracts oneGives forest metadata without another traversalAssuming it reaches one on disconnected input

With both optimizations, any sequence of m operations on V elements costs O(m α(V)) amortized. That is not literally O(1), but the inverse Ackermann function is below five at practical scales. Sorting, not DSU, dominates Kruskal.

Worked trace: accept, reject, and merge

Kruskal trace for the graph used in the code example
EdgeRepresentatives beforeDecisionComponents afterTotal
1–2 (1)differentaccept{0} {1,2} {3} {4}1
0–2 (2)differentaccept{0,1,2} {3} {4}3
3–4 (2)differentaccept{0,1,2} {3,4}5
0–1 (4)samereject cycleunchanged5
1–3 (5)differentaccept; complete{0,1,2,3,4}10

The rejected 0–1 edge exposes a frequent trace mistake: choosing the next cheapest weight is not enough. The endpoints must still belong to different components.

Production-safe Kruskal forest in Java

This version sorts a copy instead of mutating caller input, validates endpoints, uses path halving plus union-by-size, accumulates into long, and returns forest metadata rather than silently claiming success on a disconnected graph.

java
import java.util.*;

public final class KruskalMinimumSpanningForest {
    public record Edge(int u, int v, long weight) {}
    public record Result(
            List<Edge> edges, long totalWeight,
            int components, boolean connected) {}

    public static Result build(int vertexCount, List<Edge> input) {
        if (vertexCount < 0) {
            throw new IllegalArgumentException("negative vertex count");
        }

        List<Edge> sorted = new ArrayList<>(input);
        for (Edge edge : sorted) {
            if (edge.u() < 0 || edge.u() >= vertexCount
                    || edge.v() < 0 || edge.v() >= vertexCount) {
                throw new IllegalArgumentException("vertex outside [0, V)");
            }
        }
        sorted.sort(Comparator.comparingLong(Edge::weight)
                .thenComparingInt(edge -> Math.min(edge.u(), edge.v()))
                .thenComparingInt(edge -> Math.max(edge.u(), edge.v())));

        Dsu dsu = new Dsu(vertexCount);
        List<Edge> forest = new ArrayList<>();
        long total = 0L;

        // Do not stop at V-1 unless connectivity is guaranteed by contract.
        for (Edge edge : sorted) {
            if (!dsu.union(edge.u(), edge.v())) continue;
            forest.add(edge);
            total = Math.addExact(total, edge.weight());
        }

        if (forest.size() != vertexCount - dsu.components()) {
            throw new AssertionError("forest edge-count invariant failed");
        }
        boolean connected = vertexCount == 0 || dsu.components() == 1;
        return new Result(
                List.copyOf(forest), total, dsu.components(), connected);
    }

    private static final class Dsu {
        private final int[] parent;
        private final int[] size;
        private int components;

        Dsu(int n) {
            parent = new int[n];
            size = new int[n];
            components = n;
            Arrays.setAll(parent, i -> i);
            Arrays.fill(size, 1);
        }

        int find(int x) {
            while (x != parent[x]) {
                parent[x] = parent[parent[x]]; // path halving
                x = parent[x];
            }
            return x;
        }

        boolean union(int a, int b) {
            int rootA = find(a), rootB = find(b);
            if (rootA == rootB) return false;

            if (size[rootA] < size[rootB]) {
                int temporary = rootA; rootA = rootB; rootB = temporary;
            }
            parent[rootB] = rootA;
            size[rootA] += size[rootB];
            components--;
            return true;
        }

        int components() { return components; }
    }
}

Ties, uniqueness, and edge classification

Critical edge

Appears in every MST. A unique lightest edge across some cut is critical.

Pseudo-critical edge

Appears in at least one MST but not all; equal-weight alternatives commonly create this status.

Excluded edge

Appears in no MST, for example a uniquely heaviest edge on a cycle.

Distinct weights guarantee uniqueness, but repeated weights do not guarantee non-uniqueness. A deeper test processes one weight batch at a time: contract components formed by lighter edges, then inspect the equal-weight candidate edges between those components. Alternative acyclic ways to connect the same contracted region imply multiple optimal choices. Deterministic endpoint tie-breakers choose one reproducibly; they do not make it uniquely optimal.

FAANG-style variants and engineering choices

Recognize the transformation

  • • “Connect every site for minimum total build cost” is MST; “minimum route from one site” is shortest path.
  • • To form exactly k clusters, stop when DSU has k components, or remove the k−1 largest MST edges.
  • • A virtual node can model per-vertex options such as building a well versus laying a pipe.
  • • Sorting edges also powers offline connectivity-by-threshold questions, though not every DSU problem is an MST problem.

Choose around the data

  • • Kruskal stores/sorts Θ(E) edges: excellent for sparse edge lists, expensive for dense complete graphs.
  • • Matrix Prim's O(V²) often beats materializing and sorting Θ(V²) dense edges.
  • • If edges live on disk, external merge sort can feed Kruskal in order; DSU remains in memory when V permits.
  • • Parallel sorting is straightforward; concurrent unions require careful synchronization and are not “independent.”
Edge cases: negative and zero weights are valid; self-loops always fail union; parallel edges are safe; directed input is not an MST; disconnected input yields a forest; normalize endpoints for deterministic undirected ordering; use long and avoid subtraction comparators.

Complexity details & tie-breaking

Sorting dominates: O(E log E) comparisons. DSU with union-by-rank and path compression makes the union/find phase O(E α(V)), where α is the inverse Ackermann function, effectively constant for any realistic graph size.

Equal-weight edges can admit multiple valid MSTs; Kruskal's output depends on the comparator's tie-breaking. A stable sort alone is deterministic only when input order is deterministic. Different tied choices are valid only when they preserve connectivity and acyclicity, not merely because their weights match.

Short takeaway

Kruskal sorts edges and keeps those that merge Union-Find components. Sorting dominates time; DSU enforces acyclicity and yields an MST or minimum spanning forest.

Exercises

  1. Show a sorted edge list where taking the first V−1 edges without DSU fails. Which edge must be rejected?
  2. Prove in one sentence why an accepted Kruskal edge is a lightest edge across the cut of the two components it merges.
  3. E=2×10⁶, V=5×10⁵. Is Kruskal or heap Prim the better fit if the input is already an edge array?

Primary sources

  • Kruskal, J. B. (1956): On the shortest spanning subtree of a graph.
  • Cormen et al., CLRS: Kruskal's algorithm and the cycle property.
  • Tarjan: Union-Find with path compression and union by rank.