Python Algorithms & Data Structures track

Lesson 26 · Part 7: Advanced Graphs

Minimum Spanning Trees

Kruskal with union-find, and Prim

Part 7: Advanced Graphs4 min readFree lesson

Part 07 Advanced Graphs | Previous: 25_Floyd_Warshall | Next: 27_Practical_Patterns


Introduction

A Minimum Spanning Tree (MST) is a tree that connects all vertices in a weighted, connected, undirected graph with the minimum total edge weight. It is fundamentally used in network design, clustering, and approximation algorithms.

  • Tree conditions: V vertices, V-1 edges, no cycles
  • Representative algorithms: Kruskal (O(E log E)), Prim (O((V+E) log V))

Learning Goals

  1. The Kruskal algorithm can be implemented with Union-Find.
  2. The Prim algorithm can be implemented with heapq and used for dense graphs.
  3. The Cut Property and Cycle Property of MST can be explained and applied to practical problems.

Key Concepts

1. MST Properties

Cut Property: When a graph is divided into two sets S and V-S, the lightest edge connecting the two sets must be included in the MST.

Cycle Property: In any cycle, the heaviest edge is not included in the MST.

2. Kruskal Algorithm

1. Sort all edges in ascending order of weight      O(E log E)
2. Initialize each vertex as an independent set    O(V)
3. Process the sorted edges in order:
- If the two endpoints are in different sets:
→ Add the edge to the MST (Union)
→ Stop when V-1 edges are added
- If they are in the same set, a cycle is formed → Skip

Time: O(E log E) + O(E α(V)) ≈ O(E log E)
Space: O(V + E)

3. Prim Algorithm

1. Add an arbitrary starting vertex to the MST
2. Select the minimum weight edge among the edges adjacent to the MST that have not been selected
3. Add the selected vertex to the MST
4. Repeat until V-1 edges have been added

Time: O((V+E) log V)  — heapq implementation
Space: O(V + E)

4. Union-Find (Disjoint Set) Implementation

Path Compression:
find(x): While finding the root, directly connect x's parent to the root
→ Subsequent find operations become close to O(1)

Union by Rank:
union(x, y): Attach the tree with lower rank (estimated tree height) under the taller tree
→ Ensures tree height O(log V)

Amortized complexity: O(α(V)) ≈ O(1)  (α is the inverse Ackermann function)

5. Text Art: Step-by-Step Example of Kruskal

Graph (Undirected):
  1 ──4── 2 ──2── 4
  │      / \      │
  3    1    3     6
  │  /       \    │
  3 ────5──── 5──7──6

Edges sorted:
  (2,3): 1  (1,3): 3  (2,4): 2  (2,5): 3  (1,2): 4  (3,5): 5  (4,5): 6  (5,6): 7

Steps:
┌─────┬───────┬─────────────────┬──────────────────┐
│ Order │ Edge   │ Same set?       │ MST Edge        │
├─────┼───────┼─────────────────┼──────────────────┤
│  1    │ 2-3:1 │ No → Add       │ {2-3}           │
│  2    │ 2-4:2 │ No → Add       │ {2-3, 2-4}      │
│  3    │ 1-3:3 │ No → Add       │ {2-3, 2-4, 1-3} │
│  4    │ 2-5:3 │ No → Add       │ {… , 2-5}       │
│  5    │ 1-2:4 │ Yes (Cycle) → Skip│ No change    │
│  6    │ 3-5:5 │ Yes (Cycle) → Skip│ No change    │
│  7    │ 5-6:7 │ No → Add       │ {… , 5-6} V-1=5 │
└─────┴───────┴─────────────────┴──────────────────┘
Total MST weight: 1+2+3+3+7 = 16

6. Algorithm Comparison

Item Kruskal Prim (heapq)
Time Complexity O(E log E) O((V+E) log V)
Space Complexity O(V + E) O(V + E)
Suitable Scenario Sparse Graph (E ≈ V) Dense Graph (E ≈ V²)
Implementation Difficulty Easy (Sorting + Union-Find) Moderate (heapq)
Sorting Required Required Not Required

Example

File Content Core Concept
src/01_union_find.py Union-Find Implementation Path Compression, Union by Rank
src/02_kruskal.py Kruskal MST Edge Sorting + Union-Find
src/03_prim.py Prim MST heapq + visited
src/04_mst_applications.py MST Weight, Second MST, Bottleneck Tree MST Applications
python src/01_union_find.py
python src/02_kruskal.py
python src/03_prim.py
python src/04_mst_applications.py

Common Mistakes

  1. Missing Union-Find Path Compression: In find, if you do not recursively update the parent to the root, the worst case is O(V) → possible TLE.

  2. Missing Early Termination at V-1 Edges in Kruskal: It works even when all edges are processed, but it is efficient to terminate immediately once V-1 edges are selected.

  3. Missing visited Check in Prim: Skip the vertex if it has already been visited when popped from the heap. if in_mst[u]: continue

  4. Undirected graph input in one direction: Since MST is a problem for undirected graphs, you need to add edges in both directions.

  5. Assumption of a connected graph: MST is only defined for connected graphs. If the graph is disconnected, you should consider a "Minimum Spanning Forest (MSF)."


Summary

MST Algorithm Summary
━━━━━━━━━━━━━━━━━━━━━
Kruskal: O(E log E), Union-Find, Sparse Graph
Prim   : O((V+E) log V), heapq, Dense Graph
Condition: Weighted Connected Undirected Graph
Properties: Cut Property, Cycle Property
Applications: Network Design, Clustering, Second MST

Try It Yourself

  1. Try manually tracing the above text-art graph using Kruskal's algorithm.
  2. Solve the same graph using Prim's algorithm as well (the result should be the same).
  3. Submit Baekjoon 1197 (Minimum Spanning Tree) and get an AC.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/01_union_find.pyPython
"""Chapter 26 — MST: Union-Find (Union-Find / Disjoint Set Union)
Topic: Union-Find with Path Compression and Union by Rank
Time: O(α(V)) per operation ≈ O(1) amortized
Space: O(V)

Description:
    Path Compression: During find, directly connect the parent of every node to the root.
    Union by Rank: Attach the tree with lower rank under the tree with higher rank.
    Using both optimizations together results in amortized complexity O(α(V)) ≈ O(1)."""


class UnionFind:
    """Union-Find with path compression + union by rank.

    Time: find/union O(α(n)) amortized
    Space: O(n)"""

    def __init__(self, n: int) -> None:
        """Initialize n elements. Each element is its own root.

        Time: O(n)
        Space: O(n)"""
        self.parent = list(range(n))
        self.rank = [0] * n
        self.size = [1] * n   #Set Size (Bonus)
        self.num_components = n

    def find(self, x: int) -> int:
        """Returns the root (representative element) of the set x belongs to.
        Apply path compression: update the parent of all nodes to the root while finding the root.

        Time: O(α(n)) amortized"""
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  #Path Compression
        return self.parent[x]

    def union(self, x: int, y: int) -> bool:
        """Merge the sets containing x and y.

        Returns:
            True  if the two sets are merged (they were different sets)
            False if they are already in the same set (cycle formation)

        Time: O(α(n)) amortized"""
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return False   #Already in the Same Set

        #Union by Rank: Attach Lower Rank Under Higher Rank
        if self.rank[rx] < self.rank[ry]:
            rx, ry = ry, rx   #rx Always Has Higher Rank
        self.parent[ry] = rx
        self.size[rx] += self.size[ry]
        if self.rank[rx] == self.rank[ry]:
            self.rank[rx] += 1

        self.num_components -= 1
        return True

    def connected(self, x: int, y: int) -> bool:
        """Check if x and y are in the same set. Time O(α(n))."""
        return self.find(x) == self.find(y)

    def component_size(self, x: int) -> int:
        """Return the size of the set to which x belongs. Time O(α(n))."""
        return self.size[self.find(x)]


def main() -> None:
    print("=" * 55)
    print("Union-Find Basic Operation Test")
    print("=" * 55)

    uf = UnionFind(7)   #Nodes 0~6
    print(f"Initial Number of Sets: {uf.num_components}")

    #Union Operation
    ops = [(0, 1), (1, 2), (3, 4), (5, 6), (2, 3)]
    for u, v in ops:
        merged = uf.union(u, v)
        print(f"union({u}, {v}): {'Merged' if merged else 'Already in the Same Set'}"
              f"→ Number of Sets={uf.num_components}")

    print()
    print("Connected Status:")
    pairs = [(0, 4), (0, 5), (5, 6)]
    for u, v in pairs:
        print(f"  connected({u}, {v}): {uf.connected(u, v)}")

    print()
    print("Set Size:")
    for i in range(7):
        print(f"node {i}: root={uf.find(i)}, Set Size={uf.component_size(i)}")

    #── Path Compression Visualization ──
    print()
    print("=" * 55)
    print("Path Compression Example (Deep Chain → Compressed in 1 Step)")
    print("=" * 55)

    uf2 = UnionFind(6)
    #0→1→2→3→4→5 Chain
    for i in range(5):
        uf2.parent[i + 1] = i  #Intentionally Create Chain Bypassing Rank
    uf2.parent[0] = 0

    print(f"Parent Before Compression: {uf2.parent}")
    root = uf2.find(5)   #Path Compression Occurs
    print(f"find(5) = {root}")
    print(f"Parent After Compression: {uf2.parent}")
    print("(All Nodes Point Directly to Root)")

    #── Cycle Detection Application ──
    print()
    print("=" * 55)
    print("Cycle Detection Application")
    print("=" * 55)

    uf3 = UnionFind(4)
    edges = [(0, 1), (1, 2), (2, 3), (3, 0)]  #Last Edge Forms Cycle
    for u, v in edges:
        result = uf3.union(u, v)
        label = "Additional" if result else "Cycle Detected!"
        print(f"Edge ({u}-{v}): {label}")


if __name__ == "__main__":
    main()
src/02_kruskal.pyPython
"""Unit 26 — MST: Kruskal's Algorithm
Topic : Kruskal's MST Algorithm
Time  : O(E log E)
Space : O(V + E)

Description:
    After sorting edges in ascending order of weight,
    select edges that do not form a cycle to construct the MST.
    Use Union-Find to determine cycle existence in O(α(V)) time."""


class UnionFind:
    """Union-Find with path compression + rank union. Time O(α(n)) per operation."""

    def __init__(self, n: int) -> None:
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x: int) -> int:
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x: int, y: int) -> bool:
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return False
        if self.rank[rx] < self.rank[ry]:
            rx, ry = ry, rx
        self.parent[ry] = rx
        if self.rank[rx] == self.rank[ry]:
            self.rank[rx] += 1
        return True


def kruskal(
    n: int,
    edges: list[tuple[int, int, int]],
) -> tuple[int, list[tuple[int, int, int]]]:
    """Calculate the MST using Kruskal's algorithm.

    Args:
        n    : Number of vertices (0-indexed: 0 ~ n-1)
        edges: List of undirected edges (u, v, weight)

    Returns:
        (total_weight, mst_edges)
        total_weight : Total weight of the MST
        mst_edges    : List of edges included in the MST

    Time : O(E log E)
    Space: O(V + E)"""
    #1. Sort edges in ascending order of weight
    sorted_edges = sorted(edges, key=lambda e: e[2])

    uf = UnionFind(n)
    total = 0
    mst_edges: list[tuple[int, int, int]] = []

    #2. Check each edge
    for u, v, w in sorted_edges:
        if uf.union(u, v):          #No cycle → add to MST
            total += w
            mst_edges.append((u, v, w))
            if len(mst_edges) == n - 1:  #V-1 edges selected
                break

    return total, mst_edges


def main() -> None:
    #── Example 1: Lecture main graph ──
    print("=" * 55)
    print("Example 1: 6-vertex undirected graph")
    print("=" * 55)

    edges1 = [
        (0, 1, 4),   # 1-2
        (0, 2, 3),   # 1-3
        (1, 2, 1),   # 2-3
        (1, 3, 2),   # 2-4
        (1, 4, 3),   # 2-5
        (2, 4, 5),   # 3-5
        (3, 4, 6),   # 4-5
        (4, 5, 7),   # 5-6
        (3, 5, 4),   # 4-6
    ]
    total1, mst1 = kruskal(6, edges1)
    print(f"Total MST weight: {total1}")
    print("MST edges:")
    for u, v, w in mst1:
        print(f"{u+1} -- {v+1}  (weight {w})")

    #── Example 2: Complete graph (5 nodes) ──
    print()
    print("=" * 55)
    print("Example 2: Complete graph K5")
    print("=" * 55)
    import itertools
    import random
    random.seed(42)
    n2 = 5
    edges2 = []
    for i, j in itertools.combinations(range(n2), 2):
        w = random.randint(1, 20)
        edges2.append((i, j, w))

    print("Edge list:")
    for u, v, w in edges2:
        print(f"  {u}-{v}: {w}")

    total2, mst2 = kruskal(n2, edges2)
    print(f"Total MST weight: {total2}")
    print("MST edges:")
    for u, v, w in mst2:
        print(f"  {u}-{v}: {w}")

    #── Example 3: Disconnected (non-connected graph) ──
    print()
    print("=" * 55)
    print("Example 3: Disconnected graph — MSF (Minimum Spanning Forest)")
    print("=" * 55)
    edges3 = [(0, 1, 5), (1, 2, 3), (3, 4, 2)]  #0-1-2 and 3-4 are separated
    total3, mst3 = kruskal(5, edges3)
    print(f"Total MSF weight: {total3}")
    print(f"Number of selected edges: {len(mst3)} (less than V-1={4} → disconnected)")
    for u, v, w in mst3:
        print(f"  {u}-{v}: {w}")

    #── Complexity summary ──
    print()
    print("=" * 55)
    print("Kruskal complexity")
    print("=" * 55)
    print("Edge sorting: O(E log E)")
    print("  Union-Find: O(E α(V)) ≈ O(E)")
    print("Overall    : O(E log E)")
    print("Space      : O(V + E)")


if __name__ == "__main__":
    main()
src/03_prim.pyPython
"""Unit 26 — MST: Prim's Algorithm (Prim)
Topic: Prim's MST Algorithm using heapq
Time: O((V + E) log V)
Space: O(V + E)

Description:
    Start from an arbitrary vertex and repeatedly select the minimum weight edge adjacent to the MST.
    Use heapq (min-heap) to quickly select the minimum weight edge among unvisited vertices.
    Lazy deletion: If a vertex popped from the heap is already included in the MST, skip it."""

import heapq
import sys

INF = sys.maxsize


def prim(
    graph: list[list[tuple[int, int]]],
    start: int = 0,
) -> tuple[int, list[tuple[int, int, int]]]:
    """Calculate the MST using Prim's algorithm.

    Args:
        graph: graph[u] = [(v, weight), ...] adjacency list (undirected)
        start: starting vertex (0-indexed)

    Returns:
        (total_weight, mst_edges)

    Time: O((V + E) log V)
    Space: O(V + E)"""
    n = len(graph)
    in_mst = [False] * n
    total = 0
    mst_edges: list[tuple[int, int, int]] = []

    #(weight, current node, previous node)
    heap: list[tuple[int, int, int]] = [(0, start, -1)]

    while heap and len(mst_edges) < n - 1:
        w, u, prev = heapq.heappop(heap)

        if in_mst[u]:       #lazy deletion: already included in MST
            continue

        in_mst[u] = True
        total += w

        if prev != -1:
            mst_edges.append((prev, u, w))

        #add only unvisited adjacent vertices to the heap
        for v, weight in graph[u]:
            if not in_mst[v]:
                heapq.heappush(heap, (weight, v, u))

    return total, mst_edges


def build_undirected_graph(
    n: int,
    edges: list[tuple[int, int, int]],
) -> list[list[tuple[int, int]]]:
    """Create an undirected adjacency list. Time O(E), Space O(V+E)."""
    graph: list[list[tuple[int, int]]] = [[] for _ in range(n)]
    for u, v, w in edges:
        graph[u].append((v, w))
        graph[v].append((u, w))
    return graph


def main() -> None:
    #── Example 1: Basic Graph ──
    print("=" * 55)
    print("Example 1: Prim MST (5 vertices)")
    print("=" * 55)

    #Nodes 0~4
    # 0──4──1──2──3
    # |      \  |
    # 8       1 3
    # |          \
    # 4 ────5──── 2
    edges1 = [
        (0, 1, 4), (0, 4, 8),
        (1, 2, 2), (1, 3, 1),
        (2, 3, 3), (3, 4, 5),
    ]
    graph1 = build_undirected_graph(5, edges1)
    total1, mst1 = prim(graph1, start=0)

    print(f"Total MST weight: {total1}")
    print("MST edges:")
    for u, v, w in mst1:
        print(f"{u} -- {v}  (weight {w})")

    #── Example 2: Comparison with Kruskal ──
    print()
    print("=" * 55)
    print("Example 2: Prim vs Kruskal Result Comparison")
    print("=" * 55)

    edges2 = [
        (0, 1, 7), (0, 3, 5),
        (1, 2, 8), (1, 3, 9), (1, 4, 7),
        (2, 4, 5),
        (3, 4, 15), (3, 5, 6),
        (4, 5, 8), (4, 6, 9),
        (5, 6, 11),
    ]
    n2 = 7
    graph2 = build_undirected_graph(n2, edges2)
    total_prim, mst_prim = prim(graph2, start=0)

    #Kruskal (comparison with simple implementation)
    parent = list(range(n2))

    def find(x: int) -> int:
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x

    def union(x: int, y: int) -> bool:
        rx, ry = find(x), find(y)
        if rx == ry:
            return False
        parent[ry] = rx
        return True

    sorted_e = sorted(edges2, key=lambda e: e[2])
    total_kr = 0
    for u, v, w in sorted_e:
        if union(u, v):
            total_kr += w

    print(f"Prim    MST weight: {total_prim}")
    print(f"Kruskal MST weight: {total_kr}")
    print(f"Results identical: {total_prim == total_kr}")

    #── Example 3: Coordinate-based Complete Graph (Prim Advantage) ──
    print()
    print("=" * 55)
    print("Example 3: Coordinate-based Complete Graph MST")
    print("— Weights set by Euclidean distance between coordinates")
    print("=" * 55)

    import math
    coords = [(0, 0), (3, 0), (6, 1), (1, 4), (4, 5)]
    m = len(coords)
    edges3 = []
    for i in range(m):
        for j in range(i + 1, m):
            x1, y1 = coords[i]
            x2, y2 = coords[j]
            dist = math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
            edges3.append((i, j, dist))

    graph3_list: list[list[tuple[int, float]]] = [[] for _ in range(m)]
    for u, v, w in edges3:
        graph3_list[u].append((v, w))  # type: ignore[arg-type]
        graph3_list[v].append((u, w))  # type: ignore[arg-type]

    #prim function expects int weight; float version inline
    in_mst = [False] * m
    total3 = 0.0
    heap3: list[tuple[float, int, int]] = [(0.0, 0, -1)]
    mst3: list[tuple[int, int, float]] = []

    while heap3 and len(mst3) < m - 1:
        w, u, prev = heapq.heappop(heap3)
        if in_mst[u]:
            continue
        in_mst[u] = True
        total3 += w
        if prev != -1:
            mst3.append((prev, u, w))
        for v, weight in graph3_list[u]:
            if not in_mst[v]:
                heapq.heappush(heap3, (weight, v, u))

    print(f"Total MST distance: {total3:.4f}")
    for u, v, w in mst3:
        print(f"{coords[u]} -- {coords[v]}  distance={w:.4f}")


if __name__ == "__main__":
    main()
src/04_mst_applications.pyPython
"""Chapter 26 — MST: Applications (MST Weights, Second MST, Bottleneck Spanning Tree)
Topic: MST Applications — Second MST, Bottleneck Spanning Tree
Time: MST O(E log E), Second MST O(E * V)
Space: O(V + E)

Description:
    1. Calculate the total weight of the MST (basic)
    2. Second Minimum Spanning Tree
       — Remove one edge from the MST and add a replacement edge
    3. Bottleneck Spanning Tree
       — A spanning tree with the minimum possible maximum edge weight (same as MST)"""


class UnionFind:
    """Path Compression + Union by Rank. Time O(α(n)), Space O(n)."""

    def __init__(self, n: int) -> None:
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x: int) -> int:
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x: int, y: int) -> bool:
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return False
        if self.rank[rx] < self.rank[ry]:
            rx, ry = ry, rx
        self.parent[ry] = rx
        if self.rank[rx] == self.rank[ry]:
            self.rank[rx] += 1
        return True


def kruskal(
    n: int,
    edges: list[tuple[int, int, int]],
) -> tuple[int, list[tuple[int, int, int]]]:
    """Kruskal MST. Time O(E log E), Space O(V+E)."""
    sorted_edges = sorted(edges, key=lambda e: e[2])
    uf = UnionFind(n)
    total = 0
    mst_edges: list[tuple[int, int, int]] = []
    for u, v, w in sorted_edges:
        if uf.union(u, v):
            total += w
            mst_edges.append((u, v, w))
            if len(mst_edges) == n - 1:
                break
    return total, mst_edges


# ──────────────────────────────────────────────
#Second Minimum Spanning Tree
# ──────────────────────────────────────────────

def second_mst(
    n: int,
    edges: list[tuple[int, int, int]],
) -> int:
    """Returns the weight of the Second Minimum Spanning Tree.

    Algorithm:
    1. Compute the MST.
    2. Remove each edge e from the MST one by one.
    3. Compute the MST again with the remaining edges (Kruskal without e).
    4. Return the minimum weight among the valid spanning trees.

    Time: O(E * (V + E log E))  Repeating MST E times
    Space: O(V + E)

    Note: There is a more efficient O(E log E) algorithm, but it is complicated to implement."""
    _, mst_edges = kruskal(n, edges)
    mst_set = set(map(id, mst_edges))  #Use index since it is difficult to identify the same object

    #Practical implementation: track MST edge indices
    sorted_edges = sorted(edges, key=lambda e: e[2])
    uf_init = UnionFind(n)
    mst_idx: list[int] = []
    for idx, (u, v, w) in enumerate(sorted_edges):
        if uf_init.union(u, v):
            mst_idx.append(idx)
            if len(mst_idx) == n - 1:
                break

    best = float("inf")
    #Exclude one MST edge at a time and calculate a new MST
    for skip in mst_idx:
        uf2 = UnionFind(n)
        total2 = 0
        count2 = 0
        for idx, (u, v, w) in enumerate(sorted_edges):
            if idx == skip:
                continue
            if uf2.union(u, v):
                total2 += w
                count2 += 1
                if count2 == n - 1:
                    break
        if count2 == n - 1:
            best = min(best, total2)

    return int(best) if best != float("inf") else -1


# ──────────────────────────────────────────────
#Bottleneck Spanning Tree
# ──────────────────────────────────────────────

def bottleneck_mst(
    n: int,
    edges: list[tuple[int, int, int]],
) -> int:
    """Returns the maximum edge weight of the Bottleneck Spanning Tree.

    Note: The MST is also a Bottleneck Spanning Tree.
    (Among all spanning trees, the one with the minimum maximum edge = MST)

    Time : O(E log E)
    Space: O(V + E)"""
    _, mst_edges = kruskal(n, edges)
    if len(mst_edges) < n - 1:
        return -1  #Cannot connect
    return max(w for _, _, w in mst_edges)


def main() -> None:
    #── Common Example Graph ──
    n = 5
    edges = [
        (0, 1, 1), (0, 2, 3),
        (1, 2, 1), (1, 3, 4), (1, 4, 2),
        (2, 3, 5),
        (3, 4, 7),
    ]

    #1. MST Weight
    print("=" * 55)
    print("1. MST Weight")
    print("=" * 55)
    total, mst = kruskal(n, edges)
    print(f"MST Weight: {total}")
    print("MST edges:")
    for u, v, w in mst:
        print(f"  {u}--{v}: {w}")

    #2. Second MST
    print()
    print("=" * 55)
    print("2. Second Minimum Spanning Tree")
    print("=" * 55)
    second = second_mst(n, edges)
    print(f"Second MST Weight: {second}")
    print(f"(Difference from the first MST: {second - total})")

    #3. Bottleneck Spanning Tree
    print()
    print("=" * 55)
    print("3. Maximum Edge of Bottleneck Spanning Tree")
    print("=" * 55)
    bottleneck = bottleneck_mst(n, edges)
    print(f"Maximum edge weight of MST (Bottleneck): {bottleneck}")

    #Compare with another spanning tree
    #For comparison: one of the spanning trees including all edges arbitrarily
    other_st = [(0, 1, 1), (1, 2, 1), (1, 4, 2), (1, 3, 4)]
    other_max = max(w for _, _, w in other_st)
    print(f"Maximum edge of other spanning tree: {other_max}")
    print(f"MST is bottleneck-optimal: {bottleneck <= other_max}")

    #── Application Summary ──
    print()
    print("=" * 55)
    print("MST Application Pattern Summary")
    print("=" * 55)
    print("MST Weight    : kruskal/prim basic")
    print("Second MST    : recalculate after removing MST edges")
    print("Bottleneck Spanning Tree : MST = minimum bottleneck tree")
    print("Clustering    : remove k-1 largest edges → k clusters")
    print("Network Cost  : MST = minimum connection cost")


if __name__ == "__main__":
    main()

Assignment

Solve it directly on Baekjoon Online Judge first, then check the reference solution below.


Problem List

# Baekjoon Number Title Difficulty Key Technique
1 1197 Minimum Spanning Tree Gold IV Kruskal
2 1922 Network Connection Gold IV Kruskal
3 4386 Making a Constellation Gold III Prim (Coordinate-based)

Problem 1: Baekjoon 1197 — Minimum Spanning Tree (Gold IV)

Problem Summary

Find the sum of weights of the minimum spanning tree of an undirected graph with V vertices and E edges.

Input

V E          (Number of vertices, number of edges)
A B C        (Weight C for A-B)
...

Constraints

  • 1 ≤ V ≤ 10,000
  • 1 ≤ E ≤ 100,000
  • Weight -10,000 ≤ C ≤ 10,000 (negative values allowed!)

Solution Strategy

  • Kruskal MST
  • Negative weights are handled without issue in Kruskal

Solution Files

boj_1197.py


Problem 2: Baekjoon 1922 — Network Connection (Gold IV)

Problem Summary

Connect N computers with minimum cost. Output the minimum cost.

Input

N            (Number of computers)
M            (Number of edges)
a b c        (Cost c between a-b)
...

Constraints

  • 1 ≤ N ≤ 1,000
  • 1 ≤ M ≤ 100,000

Solution Strategy

  • Same structure as Baekjoon 1197 (output MST weight sum)

Solution Files

boj_1922.py


Problem 3: Baekjoon 4386 — Making a Constellation (Gold III)

Problem Summary

Connect N stars given as 2D coordinates at minimum cost. Distance between two stars = Euclidean distance.

Input

N            (Number of stars)
x y          (Star coordinates)
...

Constraints

  • 1 ≤ N ≤ 100
  • Coordinates 0.0 ≤ x, y ≤ 1000.0

Solution Strategy

  • Constructing a complete graph gives E = N*(N-1)/2 ≤ 4,950 → sufficient
  • Prim algorithm (favorable for coordinate-based complete graphs)
  • Or Kruskal (after generating all edges and sorting)

Solution Files

boj_4386.py


Pre-submission Checklist

  • Apply Union-Find path compression (self.parent[x] = self.find(self.parent[x]))
  • 1-indexed input: UnionFind(V + 1) or convert to 0-indexed
  • Check if negative weights are allowed (Kruskal allows them)
  • 4386: Use math.sqrt or math.dist, output with 2 decimal places
  • Handle disconnected graph exceptions (number of selected edges < V-1)
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

boj_1197.pyPython
"""Baekjoon 1197 — Minimum Spanning Tree
https://www.acmicpc.net/problem/1197

Difficulty: Gold IV
Time  : O(E log E)
Space : O(V + E)

Solution:
    Kruskal MST.
    Negative weights are also handled correctly."""

import sys

input = sys.stdin.readline


def solve() -> None:
    V, E = map(int, input().split())
    edges = []
    for _ in range(E):
        a, b, c = map(int, input().split())
        edges.append((c, a, b))   #Stored in the form of (weight, u, v)

    edges.sort()

    # Union-Find
    parent = list(range(V + 1))
    rank = [0] * (V + 1)

    def find(x: int) -> int:
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    def union(x: int, y: int) -> bool:
        rx, ry = find(x), find(y)
        if rx == ry:
            return False
        if rank[rx] < rank[ry]:
            rx, ry = ry, rx
        parent[ry] = rx
        if rank[rx] == rank[ry]:
            rank[rx] += 1
        return True

    total = 0
    count = 0
    for c, a, b in edges:
        if union(a, b):
            total += c
            count += 1
            if count == V - 1:
                break

    print(total)


if __name__ == "__main__":
    solve()
boj_1922.pyPython
"""Baekjoon 1922 — Network Connection
https://www.acmicpc.net/problem/1922

Difficulty: Gold IV
Time: O(E log E)
Space: O(V + E)

Solution:
    Kruskal MST. Same structure as Baekjoon 1197.
    Connect all N computers with minimum cost."""

import sys

input = sys.stdin.readline


def solve() -> None:
    N = int(input())
    M = int(input())
    edges = []
    for _ in range(M):
        a, b, c = map(int, input().split())
        edges.append((c, a, b))

    edges.sort()

    parent = list(range(N + 1))
    rank = [0] * (N + 1)

    def find(x: int) -> int:
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    def union(x: int, y: int) -> bool:
        rx, ry = find(x), find(y)
        if rx == ry:
            return False
        if rank[rx] < rank[ry]:
            rx, ry = ry, rx
        parent[ry] = rx
        if rank[rx] == rank[ry]:
            rank[rx] += 1
        return True

    total = 0
    count = 0
    for c, a, b in edges:
        if union(a, b):
            total += c
            count += 1
            if count == N - 1:
                break

    print(total)


if __name__ == "__main__":
    solve()
boj_4386.pyPython
"""Baekjoon 4386 — Making a Constellation
https://www.acmicpc.net/problem/4386

Difficulty: Gold III
Time: O(N² log N)  N ≤ 100
Space: O(N²)

Solution:
    N star coordinates → construct a complete graph (E = N*(N-1)/2)
    Calculate MST using Prim's algorithm (suitable for dense graphs based on coordinates).
    Output: 2 decimal places."""

import heapq
import math
import sys

input = sys.stdin.readline


def solve() -> None:
    N = int(input())
    stars: list[tuple[float, float]] = []
    for _ in range(N):
        x, y = map(float, input().split())
        stars.append((x, y))

    #Create adjacency list (complete graph)
    graph: list[list[tuple[float, int]]] = [[] for _ in range(N)]
    for i in range(N):
        for j in range(i + 1, N):
            x1, y1 = stars[i]
            x2, y2 = stars[j]
            dist = math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
            graph[i].append((dist, j))
            graph[j].append((dist, i))

    #Prim MST
    in_mst = [False] * N
    total = 0.0
    #(Distance, current node)
    heap: list[tuple[float, int]] = [(0.0, 0)]

    while heap:
        d, u = heapq.heappop(heap)
        if in_mst[u]:
            continue
        in_mst[u] = True
        total += d
        for w, v in graph[u]:
            if not in_mst[v]:
                heapq.heappush(heap, (w, v))

    print(f"{total:.2f}")


if __name__ == "__main__":
    solve()

Learn this lesson with a mentor

Work through the Python Algorithms & Data Structures track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now