Jc-alt logo
jc
data structures and algorithms

LeetCode: Graphs II Kruskal Prim Minimum Spanning Tree

LeetCode: Graphs II Kruskal Prim Minimum Spanning Tree
13 min read
#data structures and algorithms

Kruskal Algorithm Intro

Intro

Kruskal's Algorithm is a greedy algorithm for finding a Minimum Spanning Tree (MST) in a weighted, connected graph.

It connects all vertices with a minimum total edge weight while avoiding cycles.

Graph Requirements

  1. Weight Graph
  2. Directed or Undirected (usually undirected for MST)
  3. Edge weights can be 0 or positive numbers
  4. Represented using:
    • Edge List (most common)
    • Adjacency List or Matrix

Output

A Minimum Spanning Tree: a subset of edges connecting all vertices with minimum total weight

Total weight of the Minimum Spanning Tree

Video Animation

Kruskal: https://www.youtube.com/watch?v=71UQH7Pr9kU

Pseudo Code

    def kruskal(edges, V):

        # Sort edges by weight
        edges.sort(key=lambda x: x[2])

        # Union Find data structure
        parent = [i for i in range(V)]
        rank = [0] * V

        def find(u):
            if parent[u] != u:
                parent[u] = find(parent[u])
            return parent[u]

        def union(u, v):
            pu, pv = find(u), find(v)
            if pu == pv:
                return False
            if rank[pu] < rank[pv]:
                parent[pu] = pv
            else:
                parent[pv] = pu
                if rank[pu] == rank[pv]:
                    rank[pu] += 1
            return True

        mst = []
        total_weight = 0
        for u, v, w in edges:
            if union(u, v):
                mst.append((u, v, w))
                total_weight += w

        return mst, total_weight

Time Complexity

Sorting Edges: O(E log E) Union Find operations: O(E) with path compression and union by rank

Overall: O(E log E)

Space Complexity

Union Find parent and rank arrays: O(V) MST edges storage: O(V)

IRL Use Case

  • Network Design Minimizing the cost of connecting computers, phones, or cables
  • Road Rail Planning: Building a minimal network connecting all cities

Prims Algorithm Intro

Intro

Prim's Algorithm is a greedy algorithm to find a Minimum Spanning Tree (MST) starting from a node and growing the tree edge by edge

Unlike Kruskal, which sorts edges globally, Prim's always expands from the current MST

Graph Requirements

  1. Weight Graph
  2. Directed or Undirected (usually undirected for MST)
  3. Edge weights non-negative
  4. Represented Using:
    • Adjacency List (most efficient)
    • Adjacency Matrix

Output

A Minimum Spanning Tree: subset of edges connecting all vertices with minimal total edge weight

Total weight of the Minimum Spanning Tree.

Video Animation

Prims: www.youtube.com/watch?v=cplfcGZmX7I

Pseudo Code

    def prim(graph, start):
        """
        graph: adjacency list {node: [(neighbor, weight), ...]}
        start: starting vertex
        """

        visited = set()
        min_heap = [(0, start)]
        total_weight = 0
        mst = []

        while min_heap and len(visited) < len(graph):
            w, u = heapq.heappop(min_heap)
            if u in visited:
                continue
            visited.add(u)
            total_weight += w

            for v, weight in graph[u]:
                if v not in visited:
                    heapq.heappush(min_heap, (weight, v))
                    mst.append((u, v, weight))

        return mst, total_weight

Time Complexity

Using Adjacency List + MinHeap: O(E log V)

Each edge is pushed once, each heap operation costs O(log v)

Space Complexity

MinHeap: O(V) Visited Set: O(V) MST Edge: O(V)

IRL Use Case

  • Network Design Minimizing the cost of connecting computers, phones, or cables
  • Road Rail Planning: Building a minimal network connecting all cities

1584. Min Cost to Connect All Points ::2:: - Medium

Topics: Array, Union Find, Graph, Minimum Spanning Tree

Intro

You are given an array points representing integer coordinates of some points on a 2D-plane, where points[i] = [xi, yi]. The cost of connecting two points [xi, yi] and [xj, yj] is the manhattan distance between them: |xi - xj| + |yi - yj|, where |val| denotes the absolute value of val. Return the minimum cost to make all points connected. All points are connected if there is exactly one simple path between any two points.

Example InputOutput
points = [[0,0],[2,2],[3,10],[5,2],[7,0]]20
points = [[3,12],[-2,5],[-4,1]]18

Constraints:

1 ≤ points.length ≤ 1000

-106 ≤ xi, yi ≤ 106

All pairs (xi, yi) are distinct.

Abstraction

Given a graph, determine the min cost to connect all nodes.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Kruskal] Greedy Smallest Edge Verify No Cycle Algorithm Via Union Find - Advanced Graphs/Advanced Graphs

class UnionFind:

    # Defining Our Union Find Class

    def __init__(self, n):

        # Each node starts as its own parent (separate component)
        # sc: O(V)
        self.parent = list(range(n))

        # Rank approximates tree depth (used for balancing)
        # sc: O(V)
        self.rank = [0] * n

    # Find root representative of component with path compression
    # tc: O(α(V))
    def find(self, x):

        # if root of node is not self, recurse upwards
        if self.parent[x] != x:

            # Path Compression:
            # Flattens tree for faster future lookups
            # tc: O(1) amortized
            self.parent[x] = self.find(self.parent[x])

        # return parent of original after path compression
        return self.parent[x]


    # Union by rank: attach tree with less height under the taller one
    # based on union upper bound on height
    # tc: O(α(V))
    def union(self, x, y):

        # Nodes belong to same component: adding edge would create a cycle
        xr, yr = self.find(x), self.find(y)
        if xr == yr:
            return False

        # Union by Rank:
        # Attach smaller tree under larger tree
        if self.rank[xr] < self.rank[yr]:
            self.parent[xr] = yr
        elif self.rank[xr] > self.rank[yr]:
            self.parent[yr] = xr
        else:

            # Only increase rank of trees are of same rank
            self.parent[yr] = xr
            self.rank[xr] += 1

            # Successful Union
        return True

class Solution:

    # Defining Our Kruskal's Algorithm:


    def minCostConnectPoints(self, points: list[list[int]]) -> int:
        
        # Kruskal's Algorithm (Minimum Spanning Tree):
        # 1. Build all possible edges
        # 2. Sort edges by smallest weight
        # 3. Add edges greedily if they do NOT form a cycle,
        #    with cycle detection via Union Find, successful or failed Union operations

        # Cycle detection is handled efficiently using Union Find

        n = len(points)
        edges = []


        # Build All Edges (Manhattan Distance):
        # We are calculating the distance between nodes based on the coordinates,
        # and then using that as the edge weight.
        # tc: O(n^2)
        # sc: O(n^2)
        for i in range(n):
            for j in range(i + 1, n):

                # two points we are getting distance between
                x1, y1 = points[i]
                x2, y2 = points[j]

                # Manhattan Distance = |x1 - x2| + |y1 - y2|
                # where point 1 = (x1, y1)
                #       point 2 = (x2, y2)
                cost = abs(x1 - x2) + abs(y1 - y2)

                # Add edge weight to edge map
                edges.append((cost, i, j))

        # Sort edges by cost:
        # tc: O(E log E)
        edges.sort()

        # Kruskal Add Edges Greedily:
        # Add the smallest edge first and check if it does NOT form a cycle via
        # successful or failed Union Find operations

        # Initializing Our Union Find
        # 
        uf = UnionFind(n)
        
        # Global Cost From All Edges Added
        total_cost = 0

        # Global Number of Total Edges
        edges_used = 0

        # Iterate over all edges (already sorted smallest to largest)
        for cost, i, j in edges:

            # Union succeeds ONLY if no cycle formed
            if uf.union(i, j):

                # No cycle, we have found smallest edge to add
                total_cost += cost
                edges_used += 1


                # MST Exit:
                # MST has been generated once the smallest n-1 edges have been chosen
                if edges_used == n - 1:
                    break


        # overall: tc O(n^2 log n)
        # overall: sc: P(n^2)
        return total_cost

Solution 2: [Prim] Algorithm + MinHeap - Advanced Graphs/Advanced Graphs

    def minCostConnectPoints(self, points: List[List[int]]) -> int:

        # Prim's Algorithm (Minimum Spanning Tree)
        # Node based greedy expansion (no explicit cycle detection)
        # Always add the cheapest edge connecting the current MST to a new node

        # Difference from Kruskal:
        # Node based expansion (not edge based)
        # No explicit cycle detection needed

        n = len(points)

        # Connection Weight Map:
        # To check if a cheaper edge connecting node to MST has been found,
        # initialize weight to each node to 'inf' to start
        # sc: O(V)
        min_dist = [float('inf')] * n
        
        # Track nodes already included in MST
        # to avoid cycles?
        # sc: O(V)
        visited = [False] * n

        # Initialize MST:
        # starting node has weight of 0
        min_dist[0] = 0

        # MinHeap: (edge_cost, node)
        # Will keep track of edges tracking the min weight,
        # ensures the smallest connecting edge is expanded to next
        # sc: O(V)
        minHeap = []

        minHeap.append((0,0))

        # Global weight cost between all edges
        total_cost = 0
    
        # Prim Traversal (Greedy Expansion)
        # always expand lowest cost frontier edge
        # tc: O(n^2 log n)
        while minHeap:

            # Grab cheapest node on the minHeap
            cost, u = heapq.heappop(minHeap)
            
            # Skip if node are already connected to MST
            if visited[u]:
                continue

            # New node found:

            # Process Node: 
            # add node to tree, add edge cost to global cost
            visited[u] = True
            total_cost += cost

            # Explore New Node Neighbor Edges:
            # Dynamically compute distance to all unvisited nodes 
            for v in range(n):

                # New node has neighbor not in MST
                if not visited[v]:

                    # Calculate distance between new node and its neighbor
                    # two points we are getting distance between
                    x1, y1 = points[u]
                    x2, y2 = points[v]

                    # Manhattan Distance = |x1 - x2| + |y1 - y2|
                    # where point 1 = (x1, y1)
                    #       point 2 = (x2, y2)
                    dist = abs(x1 - x2) + abs(y1 - y2)
                    
                    # if cheaper connection to neighbor found than current connection,
                    # push edge to heap for processing
                    if dist < min_dist[v]:

                        # weight to connect to neighbor
                        min_dist[v] = dist

                        # push edge to heap for processing
                        heapq.heappush(minHeap,(dist, v))

        # overall: tc O(n^2 log n)
        # overall: sc O(n)
        return total_cost

3613. Minimize Maximum Component Cost ::2:: - Medium

Topics: Binary Search, Union Find, Graph Theory, Sorting, Minimum Spanning Tree

Intro

You are given an undirected connected graph with n nodes labeled from 0 to n - 1 and a 2D integer array edges where edges[i] = [ui, vi, wi] denotes an undirected edge between node ui and node vi with weight wi, and an integer k. You are allowed to remove any number of edges from the graph such that the resulting graph has at most k connected components. The cost of a component is defined as the maximum edge weight in that component. If a component has no edges, its cost is 0. Return the minimum possible value of the maximum cost among all components after such removals.

Example InputOutput
n = 5, edges = [[0,1,4],[1,2,3],[1,3,2],[3,4,6]], k = 24
n = 4, edges = [[0,1,5],[1,2,5],[2,3,5]], k = 15

Constraints:

1 ≤ n ≤ 5 * 10^4

0 ≤ edges.length ≤ 10^5

edges[i].length == 3

0 ≤ ui, vi < n

1 ≤ wi ≤ 10^6

1 ≤ k ≤ n

The input graph is connected.

Abstraction

Given a graph, create a MST and analyze it.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Kruskal] Greedy Smallest Edge Verify No Cycle Algorithm Via Union Find - Advanced Graphs/Advanced Graphs

    class UnionFind:
        def __init__(self, n):
            # Each node starts as its own parent (separate component)
            # sc: O(n)
            self.parent = list(range(n))
            # Rank approximates tree depth (for balancing)
            # sc: O(n)
            self.rank = [0] * n

        def find(self, x):
            # Find root representative of component
            if self.parent[x] != x:
                self.parent[x] = self.find(self.parent[x])  # Path compression
            return self.parent[x]

        def union(self, x, y):
            xr, yr = self.find(x), self.find(y)
            if xr == yr:
                return False
            if self.rank[xr] < self.rank[yr]:
                self.parent[xr] = yr
            elif self.rank[xr] > self.rank[yr]:
                self.parent[yr] = xr
            else:
                self.parent[yr] = xr
                self.rank[xr] += 1
            return True

    class Solution:
        def minCost(self, n: int, edges: list[list[int]], k: int) -> int:
            # Sort edges by weight (ascending)
            # tc: O(E log E)
            edges.sort(key=lambda x: x[2])

            uf = UnionFind(n)
            mst_edges = []

            # Build MST using Kruskal
            # tc: O(E α(n)) ~ O(E)
            for u, v, w in edges:
                if uf.union(u, v):
                    mst_edges.append(w)

            # MST has n-1 edges; remove the k-1 largest to form k components
            mst_edges.sort(reverse=True)  # Sort descending to remove largest
            if k - 1 >= len(mst_edges):
                return 0  # All components are isolated nodes

            # Remove k-1 largest edges
            result = mst_edges[k - 1]  # The new maximum edge in remaining MST
            return result

    # Time Complexity:
    #   - Sorting edges: O(E log E)
    #   - Kruskal union-find: O(E α(n)) ~ O(E)
    #   => overall: O(E log E)
    # Space Complexity:
    #   - Union-Find: O(n)
    #   - MST edges list: O(n)
    #   => overall: O(n)

Solution 2: [Prim] Algorithm + MinHeap - Advanced Graphs/Advanced Graphs

    def minCostConnectPoints(self, points: List[List[int]]) -> int:

        import heapq

                # Build adjacency list
                # tc: O(E)
                # sc: O(E)
                adj = [[] for _ in range(n)]
                for u, v, w in edges:
                    adj[u].append((v, w))
                    adj[v].append((u, w))

                visited = [False] * n
                minHeap = [(0, 0, -1)]  # (weight, node, parent)
                mst_edges = []

                while minHeap:
                    w, u, parent = heapq.heappop(minHeap)
                    if visited[u]:
                        continue
                    visited[u] = True
                    if parent != -1:
                        mst_edges.append(w)  # Collect MST edge weights

                    for v, weight in adj[u]:
                        if not visited[v]:
                            heapq.heappush(minHeap, (weight, v, u))

                # MST built, now remove k-1 largest edges
                mst_edges.sort(reverse=True)
                if k - 1 >= len(mst_edges):
                    return 0

                return mst_edges[k - 1]

        # Time Complexity:
        #   - Heap operations: O(E log V)
        #   - Sorting MST edges: O(V log V)
        #   => overall: O(E log V)
        # Space Complexity:
        #   - Adjacency list: O(E)
        #   - MinHeap: O(V)
        #   => overall: O(E + V)