MST — Kruskal আর Prim: greedy-এর প্রমাণিত জয়
Minimum Spanning Trees: Kruskal and Prim
Minimum spanning tree হলো সেই বিরল জায়গা যেখানে greedy শুধু কাজ করে না — cut property দিয়ে প্রমাণিতভাবে সেরা; Kruskal edge সাজিয়ে union-find দিয়ে ছাঁটে, Prim heap দিয়ে একটা গাছ বাড়ায়, আর দুজনের যোগফল সবসময় সমান।
আগে এটা বুঝি
ধরুন আপনাকে একটা নতুন আবাসিক এলাকায় 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, takenComplexity: 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 totalHeap size O(E), প্রতিটা edge সর্বোচ্চ দুইবার push — O(E log V)।
কে কখন জেতে
| Kruskal | Prim (heap) | Prim (matrix, no heap) | |
|---|---|---|---|
| Complexity | O(E log E) | O(E log V) | O(V²) |
| মূল খরচ | sort | heap ops | V বার V-স্ক্যান |
| Sparse (E≈V) | চমৎকার | ভালো | খারাপ |
| Dense (E≈V²) | sort-এ ভারী | ভালো | সেরা — V² মানেই কাজ |
| Data structure | DSU | binary 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)।
| Edge | find | সিদ্ধান্ত | 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 একটা জোড়া-লাগানো টুকরো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 propertyহালকা crossing edge নিরাপদ — greedy-র লাইসেন্স
- Kruskalglobal sort + DSU: প্রতি ধাপে সবসময় বাকিদের মধ্যে সবচেয়ে হালকা edge
- Primএক গাছ + heap: গাছ-বনাম-বাকি cut-এর হালকা crossing edge
- Borůvka (preview)প্রতিটা টুকরো নিজের হালকা edge একসাথে নেয় — parallel-এর প্রিয়
নিজে চালিয়ে দেখুন
দুই algorithm, একই মাঠ — geometric graph-এ weight-সমতা ও sparse/dense timing
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-এর সুবিধা স্পষ্ট মাপা যায়।
নিজে বানান
kruskal() + prim() + cut-property brute-force verifier — ছোট graph-এ theorem নিজে পরীক্ষা
- DSU আর heap আগের লেসনের implementation থেকে inline নিন
- kruskal(n, edges) — (total, edge list) ফেরত দেয়
- prim(adj, n) — (total, edge list) ফেরত দেয়
- verify_cut_property(n, edges) — সব সম্ভাব্য cut গুনে প্রতিটার lightest edge MST-তে আছে কি না দেখে
- 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 না।
বুঝেছেন কি না দেখুন
1Spanning 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-এর কম।
2Cut 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 বানানো সম্ভব?
প্রয়োগ
{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 আছে”।
3Kruskal-র 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 সহজ করে।
5Cycle 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 ডিজাইন থেকে; ইতিহাস-অংশের প্রমাণ