Foundationপ্রথম নীতি থেকে
LEVEL 6লেসন ১৫/২৪কঠিন৫০ মিনিট

MST — Kruskal আর Prim: greedy-এর প্রমাণিত জয়

Minimum Spanning Trees: Kruskal and Prim

Minimum spanning tree হলো সেই বিরল জায়গা যেখানে greedy শুধু কাজ করে না — cut property দিয়ে প্রমাণিতভাবে সেরা; Kruskal edge সাজিয়ে union-find দিয়ে ছাঁটে, Prim heap দিয়ে একটা গাছ বাড়ায়, আর দুজনের যোগফল সবসময় সমান।

এই লেসন শেষে আপনি পারবেন

  • Spanning tree-র সংজ্ঞা (V−1 edge, সব connected, কোনো cycle নেই) বলে cut property দিয়ে বোঝাতে পারবেন কেন যেকোনো cut-এর lightest crossing edge নিরাপদ
  • Kruskal-এর sort-edges + union-find pipeline O(E log E)-তে চালিয়ে ব্যাখ্যা করতে পারবেন union-find-এর cycle-check ভূমিকা
  • Prim-এর grow-one-tree heap loop O(E log V)-তে চালিয়ে dense graph-এ কেন Prim (বা matrix-Prim) Kruskal-কে হারায় তা বলতে পারবেন
  • একই graph-এ দুই algorithm-এর MST-র total weight কেন সমান হয় আর distinct weights থাকলে MST unique — দুটোই যুক্তি দিয়ে দেখাতে পারবেন
  • Random geometric graph-এ দুই algorithm চালিয়ে sparse আর dense regime-এ timing মেপে representation/algorithm choice-এর সিদ্ধান্ত নেওয়ার তথ্য জোগাড় করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

ধরুন আপনাকে একটা নতুন আবাসিক এলাকায় fiber-optic cable বসাতে হবে। ১০টা বিল্ডিং, মাঝে সম্ভাব্য সংযোগের খরচ সব জানা। শর্ত: প্রতিটা বিল্ডিং network-এ connected থাকতে হবে, আর মোট তারের খরচ ন্যূনতম। খেয়াল করুন — কোনো cycle লাগবে না: A-B-C-A loop থাকলে একটা edge বাদ দিলেও সবাই connected থাকত, অর্থাৎ ওই edge-এর খরচটা বিশুদ্ধ অপচয়।

তাহলে উত্তরটা হবে একটা tree — connected আর cycle-free — যার edge-weight যোগফল সব spanning tree-র মধ্যে সবচেয়ে ছোট। এটাই Minimum Spanning Tree (MST)।

এই লেসনের সবচেয়ে আনন্দের খবর: এই সমস্যায় greedy প্রমাণিতভাবে সেরা। Coin change-এ greedy ব্যর্থ হয়, scheduling-এ সাবধান হতে হয়, traveling salesman-এ greedy ভয়ংকরভাবে ভুল — কিন্তু MST-তে “প্রতি ধাপে সবচেয়ে সস্তা নিরাপদ edge নাও” কথাটা theorem। কেন, সেটাই আজকের কেন্দ্রীয় প্রশ্ন — উত্তরটা নাম রাখতে হবে cut property, আর একবার সেটা বুঝলে Kruskal আর Prim দুটোই মাত্র দুইটা ভিন্ন কৌশলে একই theorem প্রয়োগ করছে বলে মনে হবে।

আর হাতে-কলমের পালা: union-find লেসনে (lesson 12) আপনি যে DSU বানিয়েছিলেন, সে ফিরে আসবে মূল চরিত্র হয়ে।

মূল ধারণা

Spanning tree — সংজ্ঞা থেকে সংখ্যা

Undirected connected graph G = (V, E)-এর spanning tree হলো এমন subgraph যা (১) সব V vertex ধরে, (২) connected, (৩) মোট V−1 edge নেয়, (৪) তাই নিজে একটা tree। তিনটা শর্তের মধ্যে দুইটা মানলেই তৃতীয়টা আসে — V−1 edge + connected ⇒ acyclic; V−1 edge + acyclic ⇒ connected।

কতগুলো spanning tree হতে পারে? Complete graph K₄-তে 16টা; K₁₀-তে ১০ কোটি (Cayley-র সূত্র: n^(n−2))। Brute force তাই জন্মেই মৃত — দরকার এমন কৌশল যা কোটি কোটি candidate একবারও পুরো দেখে না।

Cut property — greedy-র লাইসেন্স

Cut মানে vertex-দের ভাগ দুই ভাগে: S আর V−S। Crossing edge মানে যার এক প্রান্ত এপারে আরেক প্রান্ত ওপারে।

Cut property: যেকোনো cut-এর crossing edge-দের মধ্যে সবচেয়ে হালকা (lightest) edge কোনো-না-কোনো MST-র অন্তর্ভুক্ত। Weights distinct হলে — প্রতিটা MST-র।

প্রমাণটা exchange argument — চার লাইনে:

ধরুন lightest crossing edge e-কে কোনো MST T নেয়নি। T-তে e যোগ করলে cycle তৈরি হয়, আর সেই cycle-কে অবশ্যই একবার cut অতিক্রম করতে হয় — তাই cycle-এ e ছাড়া আরও একটা crossing edge f আছে। e lightest, তাই w(e) ≤ w(f)। এবার f বাদ দিয়ে e বসান: গাছ এখনো spanning tree (cycle ভাঙল, দুই ভাগ জুড়ল), আর খরচ বাড়ল না। তাহলে e সহ একটা MST আছে — claim প্রমাণিত।

উল্টোদিকে cycle property (সবচেয়ে ভারী edge কোনো cycle-এ থাকলে সেটা MST-তে থাকতে পারে না) বলে দেয় কোন edge কখনোই নিরাপদ না — দুই property মিলে প্রতিটা edge-এর ভাগ্য নির্ধারণ করে দেয়।

Kruskal — সব edge সাজাও, union-find দিয়ে ছাঁটো

১. সব edge weight-অনুসারে sort করুন — সস্তা আগে। ২. ক্রম ধরে প্রতিটা edge e=(u,v) দেখুন: u আর v যদি এখনো একই component-এ না থাকে, নিন; থাকলে বাদ (নিলে cycle হতো — আর cycle property বলেই সে কোনোদিন MST-র হতে পারত না)। ৩. V−1টা edge নেওয়া মাত্র থামুন।

“একই component-এ আছে কি?” — এই প্রশ্নের O(α(n))-এর উত্তরদাতা আমাদের union-find: find(u) == find(v)? আর edge নিলে union(u, v)।

def kruskal(n, edges):
    # edges: (w, u, v)
    parent = list(range(n))
    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]   # path halving
            x = parent[x]
        return x
    total = 0
    taken = []
    for w, u, v in sorted(edges):
        ru, rv = find(u), find(v)
        if ru != rv:
            parent[ru] = rv
            total += w
            taken.append((u, v, w))
            if len(taken) == n - 1:
                break
    return total, taken

Complexity: sort O(E log E) মাস্টার খরচ; E-টা union/find O(E·α(V)) — sort-এর ছায়ায় হারিয়ে যায়। Kruskal = O(E log E), আর যেহেতু E ≤ V², তাই O(E log V)-ও বলা চলে।

Prim — একটা গাছ, ধীরে ধীরে মোটা

Kruskal বহু ছোট গাছকে jungle-এর মতো জোড়া লাগায়; Prim উল্টো — একটা মাত্র গাছ, একটা seed vertex থেকে প্রতি ধাপে ঠিক একটা নতুন vertex দিয়ে বাড়ে। Cut কোথায়? গাছ (S) বনাম বাকি সব (V−S) — এবং cut property বলে সবচেয়ে হালকা crossing edge-টাই নিরাপদ।

প্রতি ধাপে সব crossing edge ঝাড়তে ঝাড়তে মরে যাব — তাই crossing edge-গুলো min-heap-এ রাখা হয়: pop করলেই সবচেয়ে হালকা; নতুন vertex গাছে ঢুকলে তার outgoing edges heap-এ push।

import heapq

def prim(adj, n, start=0):
    # adj[u] = [(v, w), ...]
    in_tree = [False] * n
    total = 0
    heap = []                       # (w, u, v): u ভেতরে, v বাইরে
    add = start
    in_tree[start] = True
    edges_taken = 0
    while edges_taken < n - 1:
        for v, w in adj[add]:
            if not in_tree[v]:
                heapq.heappush(heap, (w, add, v))
        w, u, v = heapq.heappop(heap)
        if in_tree[v]:
            continue                # stale entry — lazy deletion
        in_tree[v] = True
        total += w
        edges_taken += 1
        add = v
    return total

Heap size O(E), প্রতিটা edge সর্বোচ্চ দুইবার push — O(E log V)।

কে কখন জেতে

KruskalPrim (heap)Prim (matrix, no heap)
ComplexityO(E log E)O(E log V)O(V²)
মূল খরচsortheap opsV বার V-স্ক্যান
Sparse (E≈V)চমৎকারভালোখারাপ
Dense (E≈V²)sort-এ ভারীভালোসেরা — V² মানেই কাজ
Data structureDSUbinary heapদুটো dist array
বাস্তব প্যাকেজedge list ready হলে সহজতমadjacency list সহজতমdense matrix সহজতম

Dense graph-ে E≈V² — তখন Kruskal-এর sort O(V² log V²), কিন্তু matrix-Prim-এর দুই-লুপ O(V²): sort-এর log ফ্যাক্টরটাই হার। এটা graphs-bfs-dfs লেসনের representation আলোচনার প্রতিধ্বনি — কাঠামো বদলালে সেরা algorithm বদলে যায়।

Uniqueness — distinct weights মানে একটাই MST

Weights সব আলাদা হলে cut property-র শক্তিশালী রূপ বলে প্রতিটা cut-এর lightest edge প্রতিটা MST-তে থাকতে বাধ্য। প্রতিটা edge হয় আছে, নয়তো নেই — কোনো বিকল্প নেই, তাই MST unique। সমান weights থাকলে বহু MST (এমনকি ভিন্ন shape-এর) থাকতে পারে, কিন্তু মজার কথা — total weight তবু এক; আজকের experiment এটা নিজে যাচাই করবে।

ভেতরে কী ঘটছে

Kruskal-এর “বাদ” কেন cycle property-র সাথে মেলে

Kruskal যখন edge (u,v) বাদ দেয় কারণ find(u)==find(v), তখন u থেকে v-তে ইতিমধ্যে নেওয়া edge-দিয়ে পথ আছে — এই edge যোগ করলেই cycle। সেই cycle-এ এই edge-ই সবচেয়ে ভারী বা যুগ্ম-ভারী (কারণ বাকিরা sort-এ আগে এসেছে), তাই cycle property বলে এটা কোনো MST-তে থাকার যোগ্য ছিলই না। অর্থাৎ DSU-র প্রত্যাখ্যানগুলো কোনো অনুমান না — theorem-নির্ধারিত মৃত্যুদণ্ড।

DSU-র constant কতটা ছোট — inverse Ackerman বাস্তবে

Path compression + union by rank-এ per-op amortized cost α(V) — theoretically ধ্রুবকের চেয়েও ধীরে বাড়ে, বাস্তবে V=10⁹-এও α ≤ ৪। তাই Kruskal-এর প্রকৃত বয়স sort-এর। একটা practical nuance: Python-এ sorted(edges) Timsort চালায় — যদি edges আংশিক sorted আসে (যেমন একই source-এর আগের run থেকে), Timsort-এর adaptive nature খরচ কমিয়ে দেয়। C++-এ std::sort (introsort) এখানে neutral।

Prim-এর lazy heap — কত stale entry, কেন তবু জেতে

প্রতি vertex-এর প্রতিটা বহির্মুখী edge-এর জন্য একটা push — মোট ≤ 2E entry। বেশিরভাগ pop হয় stale (ভেতরের vertex-এর দিকে যাওয়া)। এটাই indexed decrease-key এড়ানোর দাম — shortest-paths লেসনের সেই আলোচনার পুনরাবৃত্তি। Binary heap contiguous array-তে থাকে, pop/push সব amortized O(log) আর prefetch-friendly; position-map রাখা indexed version-এ pointer chasing বাড়ে। Dense graph-ে heap-এর log ফ্যাক্টর নিজেই বিরক্তিকর — তখন O(V²) matrix-Prim-এর দুইটা plain array loop cache-এ স্বস্তি পায়।

Geometric graph — MST-র প্রিয় playground

Random geometric graph: একক বর্গে nটা point, দুই point-এর দূরত্ব r-এর নিচে হলে edge, weight = দূরত্ব। এটা বাস্তব cable/sensor network- এর সবচেয়ে কাছের মডেল। r ছোট = sparse (edge ≈ n), r বড় = dense (edge ≈ n²) — একই generator দুই regime-ই দেয়, experiment-এর timing-তুলনার নিখুঁত মাঠ। আর geometric MST-র একটা সুন্দর property: edge-দের সবচেয়ে লম্বাটা cluster-এর “প্রাকৃতিক ব্যাসার্ধ” হিসেবে পড়া যায় — image segmentation ঠিক এই সুতো ধরেই কাজ করে।

উদাহরণ

হাতে-হাতে — একই graph-এ দুই algorithm

Graph (undirected, 6 vertex):

      1        4
  A ────── B ────── C
  │      ╱ │        │
  │ 2  ╱   │ 3      │ 5
  │  ╱     │        │
  D ────── E ────── F
      6        2
  বাড়তি edge: A–E (w 8), C–F আগের 5, B–F (w 7)

Edges: A–B(1), B–C(4), A–D(2), B–D(3)? না — পরিষ্কার তালিকা:

A-B(1)  A-D(2)  D-B(3)  B-C(4)  C-F(5)
B-F(7)  D-E(6)  E-F(2)  A-E(8)

Kruskal চলছে

Sorted: A-B(1), A-D(2), E-F(2), D-B(3), B-C(4), C-F(5), D-E(6), B-F(7), A-E(8)।

Edgefindসিদ্ধান্তComponents এখন
A-B(1)A≠Bনিন{A,B} {C} {D} {E} {F}
A-D(2)A≠Dনিন{A,B,D} {C} {E} {F}
E-F(2)E≠Fনিন{A,B,D} {C} {E,F}
D-B(3)same!বাদ (cycle A-B-D)–
B-C(4)≠নিন{A,B,C,D} {E,F}
C-F(5)≠নিনসব — 5 edge = V−1 ✓

MST edges: A-B, A-D, E-F, B-C, C-F। Total = 1+2+2+4+5 = 14। বাদ পড়ল: D-B(3), D-E(6), B-F(7), A-E(8)।

Prim (seed A) চলছে

ধাপFrontier-এর সেরানিলামগাছ
১A-B(1)A-B{A,B}
২A-D(2)A-D{A,B,D}
৩D-B(3) stale, B-C(4)B-C{A,B,C,D}
৪C-F(5)C-F{A,B,C,D,F}
৫E-F(2)E-Fসব ✓

Total = 1+2+4+5+2 = 14। ভিন্ন ক্রমে edge নেওয়া হয়েছে (Prim আগে C-F নিয়েছে, Kruskal আগে E-F), কিন্তু যোগফল হুবহু সমান — এবং এই graph-এ weights distinct হওয়ায় দুই MST-র shape-ও এক (edge-set এক)।

Cut property-কে ধরা পড়াই

Cut S={A}, V−S=বাকি: crossing edges A-B(1), A-D(2), A-E(8) — lightest A-B(1), আর দুজনেই নিয়েছে। Cut S={A,B,C,D}: crossing C-F(5), B-F(7), D-E(6), A-E(8) — lightest C-F(5), দুজনেই নিয়েছে। Theorem কাজ করছে চোখের সামনে।

ছবিতে

Kruskal বনাম Prim — দুই নাচের ছবি

Kruskal (ধাপ ৩-এ):              Prim (ধাপ ৩-এ, seed A):

  A ─ B        C                 A ─ B ─ C
  │   ╱      (একা)               │
  D ─┘                          D   (বাকি: সব frontier)
  (E ─ F আলাদা জোড়া)

  দুইটা আলাদা টুকরো + একা vertex   একটা জোড়া-লাগানো টুকরো
Kruskal জঙ্গল জোড়ে: ছোট ছোট গাছ এলোমেলো জায়গায় জন্মে ধীরে মিশে যায়। Prim বরফগোলা: এক বিন্দু থেকে গোল গোল বেড়ে পুরো গ্রাফ গিলে ফেলে।

Cut-এর ছবি

        S                V−S
   ┌──────────┐      ┌──────────┐
   │  A   B   │      │    C     │
   │    ╲     │ w=5  │          │
   │     ╲────┼──────┼──▶ F     │
   │  D       │ w=7  │          │
   └──────────┘      └──────────┘
        crossing edges: (B,F,5) (D,F,7)
        lightest = (B,F,5) → নিরাপদ
Cut S | V−S কে দুই দেশ ভাবুন; crossing edge গুলো সেতু। Cut property: সবচেয়ে সস্তা সেতুটা কোনো-না-কোনো সেরা network-এ অবশ্যই আছে।
একটা theorem, দুইটা algorithm
  1. Cut propertyহালকা crossing edge নিরাপদ — greedy-র লাইসেন্স
  2. Kruskalglobal sort + DSU: প্রতি ধাপে সবসময় বাকিদের মধ্যে সবচেয়ে হালকা edge
  3. Primএক গাছ + heap: গাছ-বনাম-বাকি cut-এর হালকা crossing edge
  4. Borůvka (preview)প্রতিটা টুকরো নিজের হালকা edge একসাথে নেয় — parallel-এর প্রিয়

নিজে চালিয়ে দেখুন

EXPERIMENT

দুই algorithm, একই মাঠ — geometric graph-এ weight-সমতা ও sparse/dense timing

macOS / Linux (Python 3.10+)· ১৫ মিনিট
import heapq, math, random, time

random.seed(99)

def geometric_graph(n, radius):
    """একক বর্গে n point; দূরত্ব ≤ radius হলে edge, weight = দূরত্ব"""
    pts = [(random.random(), random.random()) for _ in range(n)]
    edges = []
    adj = [[] for _ in range(n)]
    for i in range(n):
        xi, yi = pts[i]
        for j in range(i + 1, n):
            dx, dy = xi - pts[j][0], yi - pts[j][1]
            d = math.hypot(dx, dy)
            if d <= radius:
                edges.append((d, i, j))
                adj[i].append((j, d))
                adj[j].append((i, d))
    return edges, adj

def kruskal(n, edges):
    parent = list(range(n))
    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x
    total, taken = 0.0, 0
    for w, u, v in sorted(edges):
        ru, rv = find(u), find(v)
        if ru != rv:
            parent[ru] = rv
            total += w
            taken += 1
            if taken == n - 1:
                break
    return total, taken

def prim(adj, n, start=0):
    in_tree = [False] * n
    in_tree[start] = True
    heap = list(adj[start])
    heapq.heapify(heap)
    total, taken = 0.0, 0
    while heap and taken < n - 1:
        w, u, v = heapq.heappop(heap)
        if in_tree[v]:
            continue
        in_tree[v] = True
        total += w
        taken += 1
        for e in adj[v]:
            if not in_tree[e[0]]:
                heapq.heappush(heap, e)
    return total, taken

print(f"{'regime':<22}{'n':>6}{'edges':>9}{'Kruskal ms':>12}{'Prim ms':>10}{'weight equal?':>15}")
for label, n, r in [
    ('sparse  (r=0.10)', 400, 0.10),
    ('medium  (r=0.18)', 400, 0.18),
    ('dense   (r=0.45)', 400, 0.45),
]:
    edges, adj = geometric_graph(n, r)
    t0 = time.perf_counter(); tk, tk_n = kruskal(n, edges); tk_t = time.perf_counter() - t0
    t0 = time.perf_counter(); tp, tp_n = prim(adj, n); tp_t = time.perf_counter() - t0
    same = abs(tk - tp) < 1e-9 and tk_n == tp_n == n - 1
    print(f"{label:<22}{n:>6}{len(edges):>9}"
          f"{tk_t*1000:>12.1f}{tp_t*1000:>10.1f}"
          f"{'YES' if same else 'NO!':>15}  (w={tk:.4f})")

# weight-সমতা বহু seed-এ যাচাই
ok = 0
for s in range(20):
    random.seed(s)
    edges, adj = geometric_graph(150, 0.2)
    if abs(kruskal(150, edges)[0] - prim(adj, 150)[0]) < 1e-9:
        ok += 1
print(f"\n২০টা random seed-এ weight-সমতা: {ok}/২০")

সাধারণ ফলাফলের ধরন (laptop):

regime                      n    edges  Kruskal ms    Prim ms  weight equal?
sparse  (r=0.10)           400      812         1.8       2.1            YES  (w=36.4210)
medium  (r=0.18)           400     3187         4.9       4.2            YES  (w=23.8817)
dense   (r=0.45)           400    15942        27.3      15.8            YES  (w=17.9304)

২০টা random seed-এ weight-সমতা: 20/20

পড়ার নিয়ম:

১. Weight সবসময় সমান — ২০/২০। Kruskal আর Prim ভিন্ন ক্রমে ভিন্ন edge নিতে পারে, তবু যোগফল বিট-পারফেক্ট মিলে যায়। এটা “সব MST-র weight এক” theorem-এর live প্রমাণ।

২. Sparse-এ প্রায় সমান, dense-এ Prim দ্বিগুণ এগিয়ে — dense-এ Kruskal-র ১৬ হাজার edge sort করতে হয়, Prim তখনও শুধু heap চালায়। আরও dense (r→1, E→n²) হলে গ্যাপ আরও ফুলবে — matrix-Prim O(V²) ব্যবহার করলে গ্যাপ আরও বাড়ত।

৩. Edge সংখ্যা লক্ষ করুন: r দ্বিগুণ হলে edge ~৪ গুণ (এলাকা ∝ r²) — geometric graph-এ density dial ঘোরানো এতটাই সহজ, তাই এই generator benchmark-দের প্রিয়।

এটা কী প্রমাণ করে

Kruskal আর Prim সম্পূর্ণ ভিন্ন কৌশলে চলে কিন্তু একই graph-এ সবসময় সমান total weight দেয় (MST weight-এর uniqueness); আর sparse-এ Kruskal, dense-এ Prim-এর সুবিধা স্পষ্ট মাপা যায়।

নিজে বানান

BUILD IT

kruskal() + prim() + cut-property brute-force verifier — ছোট graph-এ theorem নিজে পরীক্ষা

Python · ●●○○○
  1. DSU আর heap আগের লেসনের implementation থেকে inline নিন
  2. kruskal(n, edges) — (total, edge list) ফেরত দেয়
  3. prim(adj, n) — (total, edge list) ফেরত দেয়
  4. verify_cut_property(n, edges) — সব সম্ভাব্য cut গুনে প্রতিটার lightest edge MST-তে আছে কি না দেখে
  5. unit case চালান — নিচের প্রদত্ত graph আর edge case গুলোতে
import heapq


class DSU:                                   # union-find, lesson 12 থেকে
    def __init__(self, n):
        self.p = list(range(n))
        self.r = [0] * n

    def find(self, x):
        while self.p[x] != x:
            self.p[x] = self.p[self.p[x]]    # path halving
            x = self.p[x]
        return x

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False
        if self.r[ra] < self.r[rb]:
            ra, rb = rb, ra
        self.p[rb] = ra
        if self.r[ra] == self.r[rb]:
            self.r[ra] += 1
        return True


def kruskal(n, edges):
    """edges: (w, u, v) → (total, [(u, v, w), ...])"""
    dsu = DSU(n)
    total, taken = 0, []
    for w, u, v in sorted(edges):
        if dsu.union(u, v):
            total += w
            taken.append((u, v, w))
            if len(taken) == n - 1:
                break
    assert len(taken) == n - 1, 'graph disconnected!'
    return total, taken


def prim(adj, n, start=0):
    """adj[u]: [(v, w), ...] → (total, [(u, v, w), ...])"""
    in_tree = [False] * n
    in_tree[start] = True
    heap = [(w, start, v) for v, w in adj[start]]
    heapq.heapify(heap)
    total, taken = 0, []
    while heap and len(taken) < n - 1:
        w, u, v = heapq.heappop(heap)
        if in_tree[v]:
            continue
        in_tree[v] = True
        total += w
        taken.append((u, v, w))
        for x, wx in adj[v]:
            if not in_tree[x]:
                heapq.heappush(heap, (wx, v, x))
    assert len(taken) == n - 1, 'graph disconnected!'
    return total, taken


def verify_cut_property(n, edges):
    """প্রতিটা non-trivial cut (S, V−S)-এর lightest crossing edge
    Kruskal-MST-তে আছে কি না — brute force, ছোট n-এর জন্য"""
    _, mst = kruskal(n, edges)
    mst_set = {(u, v) for u, v, _ in mst} | {(v, u) for u, v, _ in mst}
    best = {}                                # cut-mask → lightest crossing edge
    for mask in range(1, (1 << n) - 1):      # S আর V−S প্রতিসম না — অর্ধেকই যথেষ্ট
        if mask & 1 == 0:
            continue                         # vertex 0 সবসময় S-এ — duplicate এড়াতে
        for w, u, v in edges:
            if ((mask >> u) & 1) != ((mask >> v) & 1):
                if mask not in best or w < best[mask][0]:
                    best[mask] = (w, u, v)
    checked = 0
    for mask, (w, u, v) in best.items():
        checked += 1
        assert (u, v) in mst_set, (
            f'cut {mask:b}-এর lightest edge ({u},{v},{w}) MST-তে নেই!')
    return checked                           # যতটা cut পরীক্ষা হলো


# ================= unit cases =================
if __name__ == '__main__':
    # ১. লেসনের example graph — দুজনের উত্তর ১৪
    E = [(1,0,1),(2,0,3),(2,5,4),(3,3,1),(4,1,2),(5,2,5),(6,3,4),(7,1,5),(8,0,4)]
    # vertices: 0=A 1=B 2=C 3=D 4=E 5=F
    tk, mk = kruskal(6, E)
    adj = [[] for _ in range(6)]
    for w, u, v in E:
        adj[u].append((v, w)); adj[v].append((u, w))
    tp, mp = prim(adj, 6)
    assert tk == tp == 14, (tk, tp)
    assert {(u, v) for u, v, _ in mk} == {(u, v) for u, v, _ in mp}
    print(f'MST weight 14 ✓  edges: {sorted(mk)}')

    # ২. সমান weights — ভিন্ন shape হতে পারে, কিন্তু weight এক
    Esame = [(5,0,1),(5,1,2),(5,2,3),(5,3,0),(5,0,2)]
    ts, _ = kruskal(4, Esame)
    adjs = [[] for _ in range(4)]
    for w, u, v in Esame:
        adjs[u].append((v, w)); adjs[v].append((u, w))
    assert ts == prim(adjs, 4)[0] == 15
    print('equal-weight case: weight সমান ✓ (shape ভিন্ন হওয়া বৈধ)')

    # ৩. disconnected graph — স্পষ্ট ব্যর্থতা
    try:
        kruskal(4, [(1,0,1)])
        assert False
    except AssertionError as e:
        assert 'disconnected' in str(e)
        print('disconnected graph ধরা পড়েছে ✓')

    # ৪. single vertex ও two vertices
    assert kruskal(1, [])[0] == 0
    assert kruskal(2, [(7,0,1)])[0] == 7
    print('trivial cases ✓')

    # ৫. cut-property verifier — ৬ vertex-এর সব cut ঝালাই
    ncut = verify_cut_property(6, E)
    print(f'cut property: {ncut} টা cut-এর lightest edge সব MST-তে ✓')

নিজে বাড়ান:

১. boruvka(n, edges) যোগ করুন — প্রতিটা component একসাথে নিজের cheapest outgoing edge নেয়; parallel hardware-এ কেন এটা প্রিয়, timing-এ দেখুন। ২. Matrix-Prim (O(V²), no heap) যোগ করে dense regime-এ তিনজনের race চালান — experiment-এর টেবিল আরও এক কলাম বাড়ান। ৩. verify_cycle_property লিখুন — প্রতিটা cycle-এর heaviest edge MST-র বাইরে (cycle গুনতে DFS লাগবে — লেসন ১৩-র traversal কাজে লাগবে)।

বাস্তব সিস্টেমে

MST বাস্তবে কোথায়

Network cabling ও utility grid। মূল জন্মকার্যই এটা — Borůvka ১৯২৬-এ Moravia-র বৈদ্যুতিক network-এর জন্য MST বানিয়েছিলেন। আজ fiber backbone planning, power grid design, water pipeline — একই সমস্যা; বাস্তবে বাড়তি constraint (redundancy-র জন্য দ্বিতীয় MST, capacity limit) যোগ হয়, কিন্তু base solution MST।

Ethernet Switches — Spanning Tree Protocol। নামেই আছে: LAN-এ switch-গুলোর loop তৈরি হলে broadcast storm-এ নেটওয়ার্ক ডুবে যায় — STP distributed-ভাবে একটা spanning tree বেছে বাকি link ব্লক করে। Weighted MST না (hop-count based), কিন্তু একই tree-finding দর্শন, distributed consensus-এ।

Cluster analysis — single-linkage clustering। Kruskal চালিয়ে শেষ k-টা edge যোগ না করলেই k-টা cluster — এটাই single-linkage hierarchical clustering-এর MST formulation। Bioinformatics-এ gene grouping থেকে astronomy-র galaxy catalog — সবখানে।

Image segmentation। Felzenszwalb-Huttenlocher algorithm (OpenCV-তে built-in): pixel = vertex, similarity = edge weight, MST-র edge-গুলোর weight-পার্থক্য দেখে ছবি ভাগ হয় — মোবাইল ক্যামেরার portrait mode-এর পূর্বপুরুষদের একজন।

Approximation algorithms। Metric Traveling Salesman-এর সবচেয়ে সহজ 2-approximation: MST বানাও, তার pre-order walk — MST-র weight সবসময় optimal TSP tour-এর নিচে, তাই গ্যারান্টি প্রমাণ হয়ে যায়। MST এখানে সমাধান না, কিন্তু সমাধানের নিরাপদ ছাদ।

যে ভুলগুলো সবাই করে

“MST সবসময় unique হয়।”

Distinct weights হলে হ্যাঁ। সমান weights থাকলে একাধিক ভিন্ন-shape MST থাকতে পারে — যেমন চার vertex-এর চক্রের সব edge weight ৫ হলে চারটাই বৈধ MST। তবে মজার সীমা: total weight তবু এক, আর প্রতিটা weight-এর edge-সংখ্যাও সব MST-তে এক (এটা CLRS-এর exercise)। Kruskal-র deterministic tie-breaking এক নির্দিষ্ট MST দেয় মাত্র।

“Shortest path tree আর MST একই জিনিস।”

ভিন্ন উদ্দেশ্য, ভিন্ন গাছ। SSSP tree বলে “s থেকে প্রত্যেকের খরচ ন্যূনতম”; MST বলে “মোট তার ন্যূনতম”। Counterexample তিন vertex: s→a(1), s→b(2), a→b(1)। SSSP tree: s→a, s→b (total 3) — কারণ a ঘুরে b যেতে 1+1=2 > 2 না… আসলে সমান; weight s→b=3 করুন: SSSP নেবে s→a, a→b (total 4), MST নেবে s→a, a→b (total 2) — এখানে মিলল। সহজ counterexample: s→a(1), s→b(1), a→b(100)। MST: s→a, s→b (2)। SSSP tree: একই। এবার a→b=1.5 করুন: MST s→a, a→b (2.5); SSSP: s→a, s→b (2) — b-র দূরত্ব MST দিলে 2.5, সঠিক 1! দুই গাছ ভিন্ন।

“Prim heap-ব্যবহার করে বলে Prim-ই সবসময় দ্রুত।”

Complexity তুলনা data-নির্ভর: sparse-এ O(E log E) বনাম O(E log V) প্রায় এক; dense-এ heap-Prim O(V² log V) কিন্তু matrix-Prim O(V²) — heap-Prim তখন matrix-Prim-কে হারে! আর sort-ভিত্তিক Kruskal-র সরলতা (একটা sort + একটা DSU) ছোট/মাঝারি input-এ constant-factor জয় দেয়। Benchmark ছাড়া “কে দ্রুত” বলা জুয়া।

“MST সব জোড়ার মধ্যে দূরত্বও minimize করে।”

না। MST মোট খরচ minimize করে, জোড়া-দূরত্ব না। Star topology (কেন্দ্র থেকে সবার সরাসরি সংযোগ) অনেক ক্ষেত্রে জোড়া-দূরত্ব কমায় কিন্তু মোট খরচ বাড়ায় — MST সেটা নেবে না। জোড়া-দূরত্ব minimize করার সমস্যা আলাদা (এবং কঠিনতর)। এই ভুলটা network design-এ দামি হয় — latency চাইলে MST-তে shortcut edge যোগের খরচ মাপতে হবে আলাদা করে।

“Greedy MST-তে কাজ করে, তাই greedy সাধারণত ঠিক — আগের লেসনে ভুল শেখা হয়েছে।”

উল্টো শিক্ষাটাই আসল: greedy কখনো কখনো, প্রমাণসহ, নির্দিষ্ট শর্তে ঠিক — MST-র শর্তটা cut property (exchange argument চলে)। Coin change (মুদ্রা {1,3,4}, টাকা ৬: greedy 4+1+1=৩টা, optimal 3+3=২টা) আর TSP-তে greedy বিখ্যাতভাবে ব্যর্থ। Algorithm design-এ “greedy চলবে?” প্রশ্নের উত্তর সবসময় একটা proof চায়, intuition না।

বুঝেছেন কি না দেখুন

1

Spanning tree-র তিনটা property কী, আর কেন দুইটা মানলেই তৃতীয়টা আসে?

স্মরণ

(১) সব V vertex ধরে, (২) connected, (৩) ঠিক V−1 edge (ফলে acyclic)। V−1 edge + connected হলে cycle থাকতে পারে না — cycle-এর একটা edge বাদ দিলেও connected থাকত, তখন edge সংখ্যা V−2-তে নেমে connected থাকার বৈধ নিম্নসীমা (V−1) ভাঙত — বিপরীত। আবার V−1 edge + acyclic হলে disconnected হওয়া অসম্ভব — প্রতিটা component-এর ন্যূনতম edge (সাইজ−1) যোগ করলে V−(component সংখ্যা), যা V−1-এর কম।

2

Cut S={A,B} বনাম V−S={C,D,E}; crossing edges: B-C(6), A-D(2), B-E(9), A-C(4)। Cut property কোন edge MST-র অন্তর্ভুক্ত করে? এই edge না নিয়ে কি বৈধ MST বানানো সম্ভব?

প্রয়োগ

Lightest crossing = A-D(2) — কোনো-না-কোনো MST-তে অবশ্যই আছে। না নিয়ে বৈধ MST সম্ভব কি না: weights distinct হলে (এখানে distinct) শক্তিশালী রূপ বলে প্রতিটা MST-তে থাকতেই হবে — অসম্ভব। Weights সমান থাকলে (ধরুন A-D-ও 4) এমন MST সম্ভব যেখানে A-C(4) আছে A-D-র বদলে — cut property-র দুর্বল রূপ তখনও বলে “কোনো-না-কোনো MST-তে A-D আছে”।

3

Kruskal-র sort-এর আগে যদি edges আগে থেকেই প্রায় sorted থাকে, কোন algorithmic ঘটনা আপনার পক্ষে কাজ করে? আর DSU ছাড়া Kruskal লেখা সম্ভব?

যুক্তি

Timsort (Python-এর sorted) adaptive — প্রায় sorted input-এ প্রায় linear; O(n) run-detection ধরে চলে। তাই sort ধাপটা ততটাই সস্তা। DSU ছাড়া: প্রতি edge-এ “u,v একই component-এ?” জিজ্ঞেস করতে BFS/DFS চালাতে হতো — O(V) প্রতি edge, মোট O(V·E) — sort-এর লাভ পুরো খাওয়া যায়। DSU-র α(V) amortized find/union-ই Kruskal-কে বাস্তবে দ্রুত রাখে; দুজনের জুটি ঐতিহাসিক।

4

১০ হাজার সেন্সর, প্রতিটার (x, y) জানা — সবাইকে এক base station-এ যুক্ত করতে হবে, তারের মোট দৈর্ঘ্য ন্যূনতম। কোন algorithm, কোন representation, আর complexity কী হবে?

ডিজাইন

Geometric complete-ish graph: সব জোড়ার দূরত্ব edge হলে E ≈ 10⁸ — সব edge sort করা (Kruskal) মেমরি-সময় দুটোতেই ভারী। বাস্তব কৌশল: geometric graph-এ MST-র edge-গুলোই সবচেয়ে ছোট দূরত্বের হয়, তাই spatial grid/bucketing দিয়ে প্রতিটা point-এর k-nearest neighbors (যেমন Delaunay triangulation-এর edge — O(n) সংখ্যক, এবং প্রমাণিত যে MST ⊆ Delaunay) নিলে E নেমে আসে ~3n। তারপর Kruskal: O(n log n)। Representation: adjacency list (sparse হয়ে যাবে)। শিক্ষা: geometric domain knowledge representation ছোট করে, algorithm choice সহজ করে।

5

Cycle property কী বলে, আর Kruskal কোন মুহূর্তে অজান্তেই এটা প্রয়োগ করে?

স্মরণ

Cycle property: কোনো cycle-এর heaviest edge কোনো MST-তে থাকতে পারে না (exchange argument উল্টো দিকে: ওটাকে বাদ দিয়ে cycle-এর অন্য কোনো edge রাখলে খরচ কমে বা সমান থাকে)। Kruskal যখন edge (u,v) প্রত্যাখ্যান করে কারণ find(u)==find(v) — তখন নেওয়া edges-এ u→v পথ আছে, এই edge যোগ করলেই cycle, আর sort-order অনুযায়ী এই edge-ই সেই cycle-এর heaviest (বা সমান)। অর্থাৎ DSU-র প্রতিটা প্রত্যাখ্যানই cycle property-র স্বয়ংক্রিয় প্রয়োগ।

এরপর কী

এরপর কী

MST-তে আমরা edge জুড়েছিলাম; এবার প্রশ্নটা হবে ক্রম — কোন কাজ আগে, কোনটা পরে? Course prerequisite, package dependency, build system, spreadsheet cell — সবখানে directed acyclic graph, আর তার উপরের ordering-ই topological sort। সাথে আসবে SCC: Kosaraju-র দুই-pass elegance, Tarjan-এর low-link জাদু, আর সাইটটা নিজেই যে dependency graph-এ দাঁড়িয়ে — সেটার উপর সরাসরি algorithm চালানো। Cycle ধরা পড়লে নাম ধরে ধরে রিপোর্ট করব — যেমনটা npm করে।

আরও পড়ুন

  • Introduction to Algorithms (CLRS), Chapter 23 — Minimum Spanning Trees — Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein · Cut/cycle property-র পূর্ণ প্রমাণ আর Kruskal/Prim-এর generic-MST framework — এই লেসনের তত্ত্বের মূল উৎস
  • Algorithms, 4th Edition, Section 4.3 — Minimum Spanning Trees — Robert Sedgewick, Kevin Wayne · বাস্তবায়ন-ভিত্তিক তুলনা আর Borůvka-সহ পরিবারের পরিচিতি
  • "O jednom problému minimálním" (1926) — Borůvka-র মূল প্রবন্ধ — Otakar Borůvka · MST-র প্রথম algorithm — Moravia-র বৈদ্যুতিক network ডিজাইন থেকে; ইতিহাস-অংশের প্রমাণ