এই পর্বে যা শিখবেন
- Spanning tree আর MST-এর সংজ্ঞা, আর কেন সবচেয়ে সস্তা সংযোগ সবসময় একটি tree
- Cut property-র সম্পূর্ণ exchange প্রমাণ, cycle property, আর বিশিষ্ট ওজনে MST অনন্য হওয়ার প্রমাণ লেখা
- Generic greedy MST থেকে Kruskal আর Prim বের করা, দুটির সঠিকতা cut property দিয়ে প্রমাণ করা
- Kruskal (DSU), Prim (heap) আর ঘন গ্রাফের O(V²) Prim হাতে trace করা ও তাদের complexity বলা
- Borůvka, maximum ও bottleneck spanning tree, MST বনাম shortest-path tree আর বাস্তব প্রয়োগ চেনা
১ · সমস্যা — সব উপজেলা জুড়তে সবচেয়ে কম খরচ
একটি জেলার ছয়টি উপজেলাকে ফাইবার অপটিক কেবল দিয়ে জুড়তে হবে, যাতে যেকোনো উপজেলা থেকে যেকোনো উপজেলায় ইন্টারনেটের ডেটা যেতে পারে — সরাসরি না হলেও অন্য উপজেলা ঘুরে। প্রকৌশলীরা জরিপ করে নয়টি সম্ভাব্য কেবল-পথ বের করেছেন, প্রতিটির খরচ (কোটি টাকায়) জানা। সব পথে কেবল টানার দরকার নেই; প্রশ্ন হলো: কোন কোন পথ বেছে নিলে সব উপজেলা যুক্ত হয়, আর মোট খরচ সবচেয়ে কম হয়? পল্লী বিদ্যুতায়নের লাইন, গ্রামের পানির পাইপ বা একটি অফিসের কম্পিউটার নেটওয়ার্ক — সবখানেই একই প্রশ্ন।
পর্ব ৩৯-এর ভাষায় উপজেলাগুলো vertex, কেবল-পথগুলো edge, আর প্রতিটি edge-এর গায়ে একটি সংখ্যা — খরচ বা ওজন। এমন গ্রাফকে বলে weighted graph. কেবল দুই দিকেই ডেটা নেয়, তাই গ্রাফটি undirected. আমাদের উদাহরণ গ্রাফটি এই পর্বের সব ডায়াগ্রাম আর কোডে একই থাকবে:
| edge | A–B | A–C | B–C | B–D | C–D | C–E | D–E | D–F | E–F |
|---|---|---|---|---|---|---|---|---|---|
| খরচ (কোটি টাকা) | ৪ | ৩ | ৫ | ৬ | ৭ | ৮ | ২ | ৯ | ১ |
কেন উত্তর সবসময় একটি tree
ধরুন আপনার বেছে নেওয়া কেবলগুলোর মধ্যে একটি cycle আছে — যেমন A–B, B–C আর C–A তিনটিই নেওয়া। তাহলে cycle-এর যেকোনো একটি কেবল তুলে ফেললেও সবাই যুক্ত থাকে, কারণ ওই দুই উপজেলার মধ্যে cycle-এর বাকি অংশ দিয়ে ঘুরে যাওয়ার পথ আছে। আর খরচ কমে (সব খরচ ধনাত্মক হলে)। তাই সবচেয়ে সস্তা সমাধানে কোনো cycle থাকতে পারে না। Cycle-হীন, connected গ্রাফই হলো tree (পর্ব ২৬): V টি vertex-এ ঠিক V − 1 টি edge.
একটি connected undirected গ্রাফ G = (V, E)-এর spanning tree হলো E-এর এমন একটি উপসেট T, যা V-এর সব vertex-কে জোড়ে আর কোনো cycle বানায় না — অর্থাৎ ঠিক |V| − 1 টি edge-এর একটি tree. T-এর ওজন w(T) = তার edge-গুলোর ওজনের যোগফল। যে spanning tree-র ওজন সবচেয়ে কম, সেটিই minimum spanning tree (MST). গ্রাফ connected না হলে প্রতিটি component-এর জন্য একটি করে MST মিলে হয় minimum spanning forest.
সব spanning tree গুনে দেখার brute force কি চলে? না। Cayley-র সূত্র (Arthur Cayley, ১৮৮৯) বলে, n টি vertex-এর সম্পূর্ণ গ্রাফে (প্রতি জোড়ার মধ্যে edge) nn−2 টি আলাদা spanning tree. মাত্র ২০টি উপজেলায় সেটি 2018 ≈ 2.6 × 1023 — সেকেন্ডে একশো কোটি tree দেখলেও প্রায় ৮০ লাখ বছর লাগবে। আমাদের দরকার একটি চতুর নিয়ম, যা প্রতিটি ধাপে নিশ্চিতভাবে একটি “ঠিক” edge বেছে নেয়। সেই নিয়ম আসে দুটি সরল সত্য থেকে — cut property আর cycle property.
উপমা — দুই পাড়ের মাঝে সেতু। একটি নদী জেলাটিকে দুই ভাগ করেছে। সব উপজেলা জুড়তে হলে নদীর ওপর অন্তত একটি সেতু (কেবল) লাগবেই। নদী পার হওয়ার যতগুলো সম্ভাব্য পথ আছে, তার মধ্যে সবচেয়ে সস্তাটি নেওয়াই বুদ্ধিমানের কাজ — যদি অন্য কোনো দামি সেতু নেন, সেটি ভেঙে সস্তাটি বানালে সংযোগ থাকে, খরচ কমে। নদী যেভাবেই আঁকা হোক, এই যুক্তি খাটে। এটিই cut property-র মর্মকথা।
২ · Cut property আর cycle property — প্রমাণসহ
একটি cut (S, V − S) হলো vertex-গুলোকে দুটি অশূন্য ভাগে ভাগ করা — উপমার নদী। যে edge-এর এক প্রান্ত S-এ আর অন্য প্রান্ত V − S-এ, সেটি cut-কে পার হয় — crossing edge. পার হওয়া edge-গুলোর মধ্যে যার ওজন সবচেয়ে কম, সেটি ওই cut-এর light edge.
Cut property: ধরুন সব edge-এর ওজন আলাদা। তাহলে যেকোনো cut-এর light edge e প্রতিটি MST-তে আছে। (ওজনে সমতা থাকলে: e অন্তত একটি MST-তে আছে।)
প্রমাণ (exchange argument): ধরুন e = (u, v), u ∈ S, v ∈ V − S, আর একটি MST T-তে e নেই — বিরোধে পৌঁছানোর জন্য এটি ধরে নিই।
- T একটি spanning tree, তাই T-তে u থেকে v-তে ঠিক একটি পথ P আছে।
- P শুরু হয় S-এ (u), শেষ হয় V − S-এ (v). তাই P-এর কোথাও অন্তত একবার পাড় বদলাতে হয় — P-এর একটি edge f আছে, যেটিও cut পার হয়। f ≠ e, কারণ e T-তে নেই।
- T − f: f সরালে tree দুই টুকরো হয়, আর u ও v পড়ে আলাদা টুকরোয় (তাদের একমাত্র পথটি f দিয়েই যেত)।
- T′ = T − f + e: e ঠিক ওই দুই টুকরোকে আবার জোড়ে। T′-এ V − 1 টি edge, সব vertex যুক্ত — অর্থাৎ T′ একটি spanning tree.
- w(T′) = w(T) − w(f) + w(e). e হলো cut-এর সবচেয়ে হালকা crossing edge আর ওজন সব আলাদা, তাই w(e) < w(f), ফলে w(T′) < w(T). কিন্তু T ছিল minimum — বিরোধ। ∎
ওজনে সমতা থাকলে শেষ ধাপে শুধু w(e) ≤ w(f) পাই, তাই w(T′) ≤ w(T): T′-ও একটি MST, আর তাতে e আছে। নিচের ডায়াগ্রামে আমাদের গ্রাফেই প্রমাণটি চালিয়ে দেখুন। Cut: S = ওপরের চার উপজেলা {A, B, C, D}, V − S = {E, F}.
| এই ধাপ | |
| সবুজ edge-গুলো | |
| মোট খরচ |
- Cut. S = {A, B, C, D}, অন্য পাশে {E, F}. পার হওয়া edge তিনটি: C–E (৮), D–E (২), D–F (৯). সবচেয়ে হালকা, অর্থাৎ light edge, e = D–E. দাবি: এটি প্রতিটি MST-তে আছে।
- একটি tree, যাতে e নেই. T = {A–B, A–C, B–D, C–E, E–F}. পাঁচটি edge, ছয়টি উপজেলাই যুক্ত, cycle নেই — বৈধ spanning tree, খরচ ২২. নদী পার হচ্ছে দামি C–E (৮) দিয়ে।
- e যোগ. T-তে D থেকে E-এর একমাত্র পথ D–B–A–C–E. এর সাথে D–E জুড়লে একটি cycle: D–B–A–C–E–D. ছয়টি edge — আর tree নয়।
- দ্বিতীয় crossing edge. Cycle-টি S থেকে বেরিয়ে আবার S-এ ফেরে, তাই cut-কে জোড় সংখ্যক বার পার হয়: একবার e = D–E দিয়ে, আরেকবার f = C–E দিয়ে। f-ও crossing edge, তাই w(f) ≥ w(e) — এখানে ৮ > ২.
- বদল. f সরিয়ে দিন: T′ = {A–B, A–C, B–D, D–E, E–F}. আবার পাঁচটি edge, সবাই যুক্ত, খরচ ২২ + ২ − ৮ = ১৬. প্রতিটি “e-হীন” tree-কে এভাবে সস্তা করা যায় — তাই কোনো MST-তে e বাদ পড়তে পারে না।
Cycle property — উল্টো দিকের নিয়ম
Cycle property: গ্রাফের যেকোনো cycle C-তে যে edge f-এর ওজন একাই সবচেয়ে বেশি (কঠোরভাবে), f কোনো MST-তে নেই।
প্রমাণ: ধরুন একটি MST T-তে f = (x, y) আছে। T থেকে f সরালে tree দুই টুকরো হয় — এটি একটি cut, যার এক পাশে x, অন্য পাশে y. Cycle C-এর বাকি অংশ x থেকে y-তে একটি পথ, যা f ব্যবহার করে না; সেই পথ কোথাও না কোথাও এই cut পার হয় — একটি edge g ∈ C, g ≠ f. তাহলে T − f + g একটি spanning tree, আর w(g) < w(f) (f একাই সবচেয়ে ভারী), তাই সেটি T-এর চেয়ে হালকা — বিরোধ। ∎ আমাদের গ্রাফে cycle A–B–C-তে সবচেয়ে ভারী B–C (৫), cycle C–D–E-তে C–E (৮), cycle D–E–F-তে D–F (৯) — তিনটির কোনোটিই MST-তে থাকবে না। Cut property বলে “কোন edge নিতেই হবে”, cycle property বলে “কোন edge কখনো নেওয়া যাবে না”.
সব ওজন আলাদা হলে MST একটাই
দাবি: সব edge-এর ওজন আলাদা হলে MST অনন্য। প্রমাণ: ধরুন দুটি আলাদা MST আছে, T₁ আর T₂. দুটির মধ্যে যেসব edge শুধু একটিতে আছে (symmetric difference), তাদের মধ্যে সবচেয়ে হালকাটি e; ধরি e ∈ T₁, e ∉ T₂. T₂-তে e যোগ করলে একটি cycle হয়। T₁-এ cycle নেই, তাই এই cycle-এর অন্তত একটি edge f T₁-এ নেই — f ∈ T₂ − T₁, অর্থাৎ f-ও symmetric difference-এ। e সেখানে সবচেয়ে হালকা আর ওজন আলাদা, তাই w(e) < w(f). তাহলে T₂ + e − f একটি spanning tree, T₂-এর চেয়ে হালকা — বিরোধ। ∎ উল্টোটি সত্য নয়: কিছু ওজন সমান হলেও MST অনন্য হতে পারে (যেমন গ্রাফটি নিজেই একটি tree হলে)।
৩ · Generic greedy MST — প্রতিবার একটি নিরাপদ edge
Cut property থেকে সরাসরি একটি greedy কাঠামো পাওয়া যায়। একটি edge-এর সেট A রাখুন, শুরুতে খালি। প্রতি ধাপে এমন একটি edge যোগ করুন, যা যোগ করার পরেও A কোনো না কোনো MST-এর অংশ থাকে — এমন edge-কে বলে A-এর জন্য safe edge. V − 1 বার এটি করলে A নিজেই একটি MST. এই কাঠামোকে বলা হয় generic MST অ্যালগরিদম (CLRS-এর নাম)।
Pseudocode — generic MST (any vertex numbering)
GenericMST(G, w):
A = { } // invariant: A is a subset of some MST
while A has fewer than |V| - 1 edges:
choose a cut (S, V - S) that no edge of A crosses
e = a light edge crossing that cut // safe by the cut property
A = A + { e }
return Aকেন safe: Loop invariant — “A কোনো একটি MST T-এর উপসেট”. শুরুতে খালি সেট, সত্য। এখন একটি cut নিন, যা A-এর কোনো edge পার হয় না (CLRS-এর ভাষায় cut A-কে respect করে), আর তার light edge e. e ∈ T হলে কিছু করার নেই। না হলে অংশ ২-এর exchange: T-এর u–v পথে একটি crossing edge f আছে; f ∉ A, কারণ A-এর কোনো edge cut পার হয় না; T′ = T − f + e-এর ওজন ≤ w(T), তাই T′-ও MST, আর A ∪ {e} ⊆ T′. Invariant টিকে রইল। প্রতি ধাপে একটি করে edge বাড়ে, তাই V − 1 ধাপে শেষ — তখন A একটি MST-এর উপসেট আর নিজেই V − 1 টি edge-এর spanning tree, অর্থাৎ A = MST. ∎
কাঠামোটি একটি প্রশ্ন খোলা রাখে: কোন cut বেছে নেব? দুটি স্বাভাবিক উত্তরই দুটি বিখ্যাত অ্যালগরিদম:
Kruskal
সব edge সস্তা থেকে দামি ক্রমে দেখুন। যে edge দুটি আলাদা টুকরো (component) জোড়ে, তার জন্য cut = “একটি টুকরো বনাম বাকি সব”. A হলো একটি forest, যা ধীরে ধীরে জোড়া লাগে।
Prim
একটি vertex থেকে শুরু; cut সবসময় = “এখন পর্যন্ত বানানো tree বনাম বাকি সব”. A সবসময় একটিই tree, যা প্রতি ধাপে একটি করে vertex গিলে বড় হয়।
Borůvka
প্রতিটি টুকরো একসাথে নিজের সবচেয়ে সস্তা বাইরের edge নেয় — প্রতি টুকরোর জন্য আলাদা cut. প্রতি রাউন্ডে টুকরোর সংখ্যা অন্তত অর্ধেক হয়।
৪ · Kruskal — সস্তা থেকে দামি, cycle এড়িয়ে
Kruskal-এর অ্যালগরিদম, যা Joseph Kruskal ১৯৫৬ সালে প্রকাশ করেন, এক বাক্যে: সব edge খরচ অনুযায়ী সাজান; সস্তা থেকে দামি ক্রমে প্রতিটি edge নিন, যদি না সেটি আগে নেওয়া edge-গুলোর সাথে cycle বানায়। একটি edge (u, v) cycle বানায় ঠিক তখন, যখন u আর v আগেই একই component-এ আছে। এই প্রশ্নের দ্রুত উত্তর দেয় পর্ব ৩৩-এর disjoint set union (DSU): find(u) == find(v) হলে বাদ, না হলে নিন আর union(u, v).
Pseudocode — Kruskal (0-based vertices)
Kruskal(V, E):
sort E by weight, cheapest first // O(E log E)
for each vertex v: make-set(v)
A = { }; cost = 0
for each edge (w, u, v) in sorted order:
if find(u) != find(v): // different components: no cycle
A = A + { (u, v) }; cost = cost + w
union(u, v)
if |A| == |V| - 1: break // optional early stop
return A, cost| এই ধাপের edge | |
| find(u), find(v) | |
| DSU-এর দলগুলো | |
| গৃহীত edge / মোট খরচ |
- শুরু. নয়টি edge খরচ অনুযায়ী সাজানো। DSU-তে ছয়টি এক-সদস্যের দল — প্রতিটি উপজেলা একা।
- E–F (১). সবচেয়ে সস্তা edge. E আর F আলাদা দলে, তাই নিন; union-এর পর দল {E, F}. মোট খরচ ১.
- D–E (২). D একা, E আছে {E, F}-এ — আলাদা। নিন: {D, E, F}, খরচ ৩.
- A–C (৩). দুজনই একা। নিন: {A, C}, খরচ ৬. এখন তিনটি দল।
- A–B (৪). A আছে {A, C}-তে, B একা। নিন: {A, B, C}, খরচ ১০. চারটি edge, দুটি দল বাকি।
- B–C (৫). B আর C এখন একই দলে — find(B) == find(C). এটি নিলে A–B–C cycle হতো, তাই বাদ. Cycle property-ও তাই বলে: ওই cycle-এ B–C সবচেয়ে ভারী।
- B–D (৬). B আছে {A, B, C}-তে, D আছে {D, E, F}-এ — দুই দল জুড়ে গেল। পাঁচটি edge = V − 1, খরচ ১৬. MST সম্পূর্ণ; early stop থাকলে এখানেই থামা যায়।
- C–D (৭). সবাই এখন এক দলে, তাই বাকি প্রতিটি edge cycle বানাবে। বাদ।
- C–E (৮). বাদ — অংশ ২-এর ডায়াগ্রামের সেই দামি “নদী পারের” edge.
- D–F (৯). বাদ। শেষ ফল: A–B, A–C, B–D, D–E, E–F; মোট খরচ ১৬ কোটি টাকা। চারটি edge বাদ গেছে — ৯ − (৬ − ১) = ৪.
কেন Kruskal ঠিক
Kruskal যখন e = (u, v) গ্রহণ করে, তখন u আর v আলাদা দলে। S = u-এর দল নিন। (১) এই cut A-কে respect করে: A-এর প্রতিটি edge কোনো একটি দলের ভেতরে, তাই কেউ S-এর সীমা পার হয় না। (২) e এই cut-এর light edge: e-এর চেয়ে সস্তা প্রতিটি edge আগেই দেখা হয়েছে — হয় গ্রহণ করা হয়েছে (তাহলে সে A-তে, cut পার হয় না), নয়তো বাদ গেছে কারণ তার দুই প্রান্ত তখন একই দলে ছিল; দল শুধু বড় হয়, তাই এখনো দুই প্রান্ত একই দলে, আর সেও cut পার হয় না। তাই e-এর চেয়ে সস্তা কোনো crossing edge নেই, আর অংশ ৩ অনুযায়ী e safe. বাদ পড়া edge-গুলো A-এর সাথে cycle বানাত, আর A যে MST-এর অংশ, সেখানে cycle থাকতে পারে না — তাই তাদের বাদ দেওয়াও নিরাপদ। ∎
জটিলতা: সর্ট O(E log E). DSU-তে V টি make-set আর 2E টি find, V − 1 টি union — union by size বা rank আর path compression-সহ মোট O(E α(V)), যা সর্টের তুলনায় নগণ্য। তাই মোট O(E log E). যেহেতু E ≤ V², log E ≤ 2 log V, তাই একই জিনিস O(E log V) লেখাও ঠিক। জায়গা: edge-এর তালিকা Θ(E) আর DSU Θ(V). খরচগুলো যদি ছোট পূর্ণসংখ্যা হয়, counting sort বা radix sort (পর্ব ২৪) দিয়ে সর্টও প্রায় রৈখিক হয়, তখন Kruskal কার্যত O(E α(V)). আরেকটি সুবিধা: গ্রাফ connected না হলেও Kruskal থামে না — প্রতিটি component-এর MST, অর্থাৎ minimum spanning forest দেয়।
৫ · Prim — একটি উপজেলা থেকে জাল ছড়ানো
Kruskal সারা জেলায় ছড়িয়ে ছিটিয়ে টুকরো বানায়, শেষে জোড়া দেয়। Prim-এর অ্যালগরিদম উল্টো স্বভাবের: একটি উপজেলা থেকে শুরু করে একটিমাত্র tree বড় করে। প্রতি ধাপে cut = (tree-র vertex, বাকি vertex), আর সেই cut-এর সবচেয়ে সস্তা edge দিয়ে নতুন একটি vertex tree-তে ঢোকে। যেমন একটি ইন্টারনেট সেবাদাতা প্রথমে জেলা সদরে অফিস খোলে, তারপর প্রতি মাসে সেই জাল থেকে সবচেয়ে সস্তায় যে উপজেলায় পৌঁছানো যায়, সেখানে কেবল টানে।
“Tree থেকে বাইরে যাওয়া সবচেয়ে সস্তা edge” বারবার খুঁজতে লাগে একটি priority queue (পর্ব ২৩). সবচেয়ে সহজ রূপটি “lazy”: নতুন vertex ঢুকলে তার সব বাইরের edge heap-এ ঢোকান; heap থেকে বের হওয়া edge-এর অন্য প্রান্ত যদি আগেই tree-তে থাকে, সেটি পুরোনো (stale) entry — ফেলে দিন। এটিই lazy deletion.
Pseudocode — Prim with a binary heap, lazy version (0-based)
Prim(G, s):
inTree[v] = false for every v; cost = 0
PQ = { (0, s, s) } // entries (cost, vertex, from)
while PQ is not empty:
(w, v, from) = extract-min(PQ)
if inTree[v]: continue // stale entry: skip it
inTree[v] = true; cost = cost + w
if v != s: add edge (from, v) to A
for each neighbour x of v with edge cost w2:
if not inTree[x]: insert (w2, x, v) into PQ
return A, cost| heap থেকে বের হলো | |
| সিদ্ধান্ত | |
| heap (সস্তা আগে) | |
| tree / মোট খরচ |
- শুরু. A tree-তে ঢুকল (খরচ ০)। A-এর দুটি edge heap-এ: A–C (৩), A–B (৪).
- A–C (৩). Heap-এর সবচেয়ে সস্তা। C tree-তে নেই, তাই যোগ; খরচ ৩. C-এর বাইরের edge ঢুকল: C–B (৫), C–D (৭), C–E (৮). খেয়াল করুন, B-এর জন্য এখন heap-এ দুটি entry.
- A–B (৪). B যোগ, খরচ ৭. B-এর বাইরের edge শুধু B–D (৬) — A আর C আগেই tree-তে।
- C–B (৫). B আগেই tree-তে — এটি stale entry, ফেলে দিন। Kruskal-এ এই edge-টিই “cycle” বলে বাদ গিয়েছিল।
- B–D (৬). D যোগ, খরচ ১৩. এই মুহূর্তে cut ({A, B, C}, {D, E, F})-এর light edge ছিল B–D — cut property কাজ করছে। D-এর নতুন edge: D–E (২), D–F (৯).
- D–E (২). সবচেয়ে সস্তা, E যোগ, খরচ ১৫. E থেকে নতুন: E–F (১). লক্ষ করুন, Prim সস্তা edge-গুলো দেরিতে পায় — কারণ সেগুলো tree থেকে দূরে ছিল।
- E–F (১). F যোগ, খরচ ১৬ — Kruskal-এর সাথে একই, আর edge-ও একই পাঁচটি (ওজন সব আলাদা, তাই MST অনন্য)। Heap-এ বাকি তিনটি entry পরে বের হয়ে একে একে skip হবে।
কেন Prim ঠিক, আর কত সময় নেয়
সঠিকতা: প্রতি ধাপে S = tree-র vertex. A-এর সব edge S-এর ভেতরে, তাই cut (S, V − S) A-কে respect করে। Heap-এ প্রতিটি crossing edge-এর অন্তত একটি entry আছে (S-এ প্রান্ত ঢোকার সময় ঢোকানো হয়েছিল), আর stale entry ফেলে দেওয়ার পর যা বের হয় তা সবচেয়ে সস্তা crossing edge — light edge. অংশ ৩ অনুযায়ী safe. ∎
Lazy heap: প্রতিটি edge দুই দিক থেকে সর্বোচ্চ একবার করে ঢোকে, তাই heap-এ মোট O(E) টি insert আর extract, প্রতিটি O(log E) = O(log V). মোট O(E log V), জায়গা O(E) (heap) + O(V + E) (adjacency list). Decrease-key-সহ indexed heap ব্যবহার করলে heap-এ প্রতি vertex একটি entry থাকে — সময় একই O(E log V), জায়গা O(V). Fibonacci heap-এ (Fredman ও Tarjan) decrease-key amortized O(1), তাই O(E + V log V) — তত্ত্বে ভালো, বাস্তবে ধ্রুবক বড়।
ঘন গ্রাফে O(V²) — heap ছাড়াই
যখন প্রায় প্রতিটি জোড়া উপজেলার মধ্যে পথ আছে (dense graph, E ≈ V²/2), তখন heap উল্টো ক্ষতি করে: O(E log V) = O(V² log V). তার বদলে একটি সরল অ্যারে key[v] রাখুন — “v-কে এখনকার tree-র সাথে জুড়তে সবচেয়ে সস্তা খরচ”. প্রতি ধাপে পুরো অ্যারে ঘুরে সবচেয়ে ছোট key-এর vertex নিন (O(V)), তারপর তার adjacency matrix-এর সারি দেখে প্রতিবেশীদের key কমান (O(V)). V টি ধাপ, মোট Θ(V²), জায়গা Θ(V²) matrix-এর জন্য — ঘন গ্রাফে এটিই সর্বোত্তম, কারণ ইনপুট পড়তেই Θ(V²) লাগে। কোনো দুই বিন্দুর দূরত্বই যেখানে খরচ (যেমন মানচিত্রের সব জোড়া উপজেলা), সেখানে এই রূপটিই চলে।
Pseudocode — dense Prim, O(V^2), cost matrix W (0-based, INF = no edge)
PrimDense(W, s):
key[v] = INF, inTree[v] = false for every v; key[s] = 0
repeat |V| times:
u = the vertex not in the tree with the smallest key // O(V) scan
inTree[u] = true; cost = cost + key[u]
for every vertex v: // O(V)
if not inTree[v] and W[u][v] < key[v]:
key[v] = W[u][v]; parent[v] = u| রূপ | সময় | কখন ভালো |
|---|---|---|
| Kruskal (sort + DSU) | O(E log E) | Sparse graph, edge-এর তালিকা হাতে আছে, forest-ও চলবে |
| Prim, binary heap | O(E log V) | Adjacency list, sparse থেকে মাঝারি ঘনত্ব |
| Prim, সরল অ্যারে | Θ(V²) | Dense graph বা adjacency matrix, E ≈ V² |
| Prim, Fibonacci heap | O(E + V log V) | মূলত তত্ত্বে |
৬ · কোডে Kruskal আর Prim — একই গ্রাফ, একই খরচ
এবার দুটি অ্যালগরিদম একই প্রোগ্রামে, আমাদের ছয় উপজেলার গ্রাফে। Kruskal প্রতিটি edge-এর জন্য বলে গ্রহণ (accept) না বাদ (reject); Prim বলে কোন vertex কোন edge দিয়ে যোগ হলো, আর কোন heap entry stale বলে ফেলে দেওয়া হলো। শেষ লাইনে দুটির মোট খরচ মেলানো হয় — এটি নিজের কোড যাচাইয়ের একটি সস্তা কৌশলও। DSU-তে এখানে শুধু path halving রাখা হয়েছে, কোড ছোট রাখতে; পূর্ণ DSU (union by size-সহ) পর্ব ৩৩-এ।
#include <algorithm>
#include <cstdio>
#include <functional>
#include <numeric>
#include <queue>
#include <tuple>
#include <vector>
using namespace std;
using Edge = tuple<int, int, int>; // (cost, u, v)
const char* N = "ABCDEF"; // six upazilas, 0..5
int kruskal(int n, vector<Edge> es) {
vector<int> p(n); iota(p.begin(), p.end(), 0);
auto root = [&](int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; };
sort(es.begin(), es.end()); // cheapest first: O(E log E)
int total = 0;
for (auto [w, u, v] : es) {
int ru = root(u), rv = root(v);
if (ru == rv) { printf(" reject %c-%c %d (cycle)\n", N[u], N[v], w); continue; }
p[ru] = rv; total += w; printf(" accept %c-%c %d\n", N[u], N[v], w);
}
return total;
}
int prim(int n, const vector<Edge>& es, int s = 0) {
vector<vector<pair<int, int>>> adj(n);
for (auto [w, u, v] : es) { adj[u].push_back({w, v}); adj[v].push_back({w, u}); }
vector<bool> seen(n, false); int total = 0;
priority_queue<Edge, vector<Edge>, greater<Edge>> pq; // min-heap of (cost, vertex, from)
pq.push({0, s, s});
while (!pq.empty()) {
auto [w, v, from] = pq.top(); pq.pop();
if (seen[v]) { printf(" skip %c-%c %d (stale)\n", N[from], N[v], w); continue; }
seen[v] = true; total += w;
if (v != s) printf(" add %c via %c-%c %d\n", N[v], N[from], N[v], w);
for (auto [w2, x] : adj[v]) if (!seen[x]) pq.push({w2, x, v});
}
return total;
}
int main() {
vector<Edge> es = {{4,0,1}, {3,0,2}, {5,1,2}, {6,1,3}, {7,2,3}, {8,2,4}, {2,3,4}, {9,3,5}, {1,4,5}};
puts("Kruskal:"); int k = kruskal(6, es); printf("Kruskal total = %d\n", k);
puts("Prim from A:"); int q = prim(6, es); printf("Prim total = %d | same: %s\n", q, k == q ? "yes" : "no");
}আউটপুট:
Kruskal:
accept E-F 1
accept D-E 2
accept A-C 3
accept A-B 4
reject B-C 5 (cycle)
accept B-D 6
reject C-D 7 (cycle)
reject C-E 8 (cycle)
reject D-F 9 (cycle)
Kruskal total = 16
Prim from A:
add C via A-C 3
add B via A-B 4
skip C-B 5 (stale)
add D via B-D 6
add E via D-E 2
add F via E-F 1
skip C-D 7 (stale)
skip C-E 8 (stale)
skip D-F 9 (stale)
Prim total = 16 | same: yes
Kruskal-এর নয়টি লাইন অংশ ৪-এর ডায়াগ্রামের শুরুর ধাপ বাদে বাকি নয়টি ধাপের সাথে হুবহু মেলে, আর Prim-এর লাইনগুলো অংশ ৫-এর। tuple<int, int, int> হিসেবে edge রাখায় sort আর greater<Edge> দুটোই প্রথমে খরচ দিয়ে তুলনা করে — আলাদা comparator লাগে না। Prim-এর heap entry-তে তৃতীয় সংখ্যাটি “from”, অর্থাৎ কোন tree-vertex থেকে এই edge এসেছে; তাই আউটপুটে add D via B-D 6. শেষের তিনটি skip দেখায় lazy Prim-এর দাম: heap-এ stale entry থেকে যায়, তাই heap O(E) পর্যন্ত বড় হতে পারে। চাইলে seen-এ V টি vertex হয়ে গেলেই লুপ ভাঙা যায়। Kruskal-এর vector<Edge> es by value নেওয়া, কারণ সে নিজের কপি সর্ট করে। খরচ বড় হলে total রাখুন long long-এ — ১০৫ টি vertex-এর MST-তে প্রায় ১০৫ টি edge, প্রতিটি ১০৯ হলে int উপচে পড়ে।
import heapq
N = "ABCDEF" # six upazilas, 0..5
edges = [(4, 0, 1), (3, 0, 2), (5, 1, 2), (6, 1, 3), (7, 2, 3),
(8, 2, 4), (2, 3, 4), (9, 3, 5), (1, 4, 5)] # (cost, u, v)
def kruskal(n, edges):
p = list(range(n))
def root(x): # DSU find with path halving
while p[x] != x:
p[x] = p[p[x]]
x = p[x]
return x
total = 0
for w, u, v in sorted(edges): # cheapest first: O(E log E)
ru, rv = root(u), root(v)
if ru == rv:
print(f" reject {N[u]}-{N[v]} {w} (cycle)")
else:
p[ru] = rv
total += w
print(f" accept {N[u]}-{N[v]} {w}")
return total
def prim(n, edges, s=0):
adj = [[] for _ in range(n)]
for w, u, v in edges:
adj[u].append((w, v)); adj[v].append((w, u))
seen, total, pq = [False] * n, 0, [(0, s, s)] # heap of (cost, vertex, from)
while pq:
w, v, frm = heapq.heappop(pq)
if seen[v]:
print(f" skip {N[frm]}-{N[v]} {w} (stale)"); continue
seen[v], total = True, total + w
if v != s: print(f" add {N[v]} via {N[frm]}-{N[v]} {w}")
for w2, x in adj[v]:
if not seen[x]: heapq.heappush(pq, (w2, x, v))
return total
print("Kruskal:"); k = kruskal(len(N), edges); print("Kruskal total =", k)
print("Prim from A:"); q = prim(len(N), edges)
print("Prim total =", q, "| same:", "yes" if k == q else "no")আউটপুট C++-এর সাথে হুবহু এক:
Kruskal:
accept E-F 1
accept D-E 2
accept A-C 3
accept A-B 4
reject B-C 5 (cycle)
accept B-D 6
reject C-D 7 (cycle)
reject C-E 8 (cycle)
reject D-F 9 (cycle)
Kruskal total = 16
Prim from A:
add C via A-C 3
add B via A-B 4
skip C-B 5 (stale)
add D via B-D 6
add E via D-E 2
add F via E-F 1
skip C-D 7 (stale)
skip C-E 8 (stale)
skip D-F 9 (stale)
Prim total = 16 | same: yes
Python-এর tuple-ও প্রথম উপাদান দিয়ে তুলনা হয়, তাই sorted(edges) আর heapq সরাসরি খরচ ধরে সাজায়। heapq একটি min-heap, ঠিক Prim-এর যা দরকার। DSU-এর root লুপ দিয়ে লেখা (path halving), তাই বড় গ্রাফেও recursion-এর সীমায় আটকায় না। খরচ সমান দুটি edge থাকলে tuple-এর পরের উপাদান (vertex নম্বর) দিয়ে ক্রম ঠিক হয় — ফলে C++ আর Python একই ক্রমে edge দেখে, আর আউটপুট মেলে। Python-এ int উপচে পড়ে না, কিন্তু ১০৬ টি edge-এ sorted আর heapq C++-এর চেয়ে কয়েক গুণ ধীর — কনটেস্টে দ্রুত ইনপুট পড়া জরুরি।
নিজে চালিয়ে দেখুন — Kruskal visualiser
নিচের টুলটি একই গ্রাফে Kruskal চালায়। শুরুতে পুরো রান কনসোলে লেখে আর শেষ অবস্থা দেখায়। শুরু থেকে চেপে তারপর পরের edge বারবার চাপুন: সবুজ মোটা দাগ = গ্রহণ, লাল ভাঙা দাগ = বাদ, কমলা সংখ্যা = এইমাত্র দেখা edge. এলোমেলো খরচ চাপলে নয়টি edge নতুন, আলাদা খরচ পায় — প্রতিবার আগে অনুমান করুন কোন edge বাদ পড়বে।
const POS = {A: [40, 30], B: [250, 30], C: [40, 135], D: [250, 135], E: [40, 240], F: [250, 240]};
const EDGES = [["A","B",4], ["A","C",3], ["B","C",5], ["B","D",6], ["C","D",7], ["C","E",8], ["D","E",2], ["D","F",9], ["E","F",1]];
let order, par, k, state, total; // state[i]: 0 waiting, 1 accepted, 2 rejected
const $ = id => document.getElementById(id);
function root(x) { while (par[x] !== x) { par[x] = par[par[x]]; x = par[x]; } return x; }
function reset() {
order = EDGES.map((e, i) => i).sort((a, b) => EDGES[a][2] - EDGES[b][2]); // cheapest first
par = {}; for (const v in POS) par[v] = v;
k = 0; total = 0; state = EDGES.map(() => 0);
draw("start: edges sorted by cost");
}
function step() {
if (k >= order.length) return draw("done: MST cost = " + total);
const i = order[k++], [u, v, w] = EDGES[i], ru = root(u), rv = root(v);
let msg;
if (ru === rv) { state[i] = 2; msg = `reject ${u}-${v} ${w} (cycle)`; }
else { par[ru] = rv; state[i] = 1; total += w; msg = `accept ${u}-${v} ${w}`; }
console.log(" " + msg);
draw(msg + " | total = " + total);
}
function draw(msg) {
let s = "";
EDGES.forEach(([u, v, w], i) => {
const [x1, y1] = POS[u], [x2, y2] = POS[v], cur = k > 0 && order[k - 1] === i;
const col = state[i] === 1 ? "#16a34a" : state[i] === 2 ? "#dc2626" : "#94a3b8";
s += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${col}" stroke-width="${state[i] === 1 ? 5 : 2}"${state[i] === 2 ? ' stroke-dasharray="6 4"' : ""}/>`;
s += `<text x="${(x1 + x2) / 2 + 7}" y="${(y1 + y2) / 2 - 5}"${cur ? ' class="cur"' : ""}>${w}</text>`;
});
for (const v in POS) s += `<circle cx="${POS[v][0]}" cy="${POS[v][1]}" r="15"/><text x="${POS[v][0]}" y="${POS[v][1] + 5}" text-anchor="middle" class="nm">${v}</text>`;
$("g").innerHTML = s; $("msg").textContent = msg;
}
function shuffleCosts() {
const pool = Array.from({length: 20}, (_, i) => i + 1).sort(() => Math.random() - 0.5);
EDGES.forEach((e, i) => { e[2] = pool[i]; }); // distinct random costs
console.log("new costs: " + EDGES.map(([u, v, w]) => `${u}${v}=${w}`).join(" "));
reset();
}
$("bs").onclick = step; $("br").onclick = reset; $("bn").onclick = shuffleCosts;
console.log("Kruskal:"); reset(); while (k < order.length) step();
console.log("MST cost = " + total);শুরুতে কনসোলের আউটপুট:
Kruskal:
accept E-F 1
accept D-E 2
accept A-C 3
accept A-B 4
reject B-C 5 (cycle)
accept B-D 6
reject C-D 7 (cycle)
reject C-E 8 (cycle)
reject D-F 9 (cycle)
MST cost = 16
লাইনগুলো C++ আর Python প্রোগ্রামের Kruskal অংশের সাথে মেলে। order হলো edge-এর index, খরচ অনুযায়ী সাজানো — মূল তালিকা না বদলে সর্ট করার পরিচিত কৌশল (argsort). par একটি object, যার key উপজেলার নাম — DSU-তে সংখ্যার বদলে নামও চলে। পরীক্ষা: কয়েকবার এলোমেলো খরচ চেপে গুনে দেখুন প্রতিবার ঠিক কয়টি edge সবুজ হয় — সবসময় ৫, অর্থাৎ V − 1. আর লাল প্রতিটি edge-এর জন্য খুঁজে দেখুন সবুজ edge-গুলো নিয়ে কোন cycle-এ সে সবচেয়ে ভারী — cycle property-র জীবন্ত প্রমাণ।
৭ · Borůvka আর MST-এর আত্মীয়রা
১৯২৬ সালে চেক গণিতবিদ Otakar Borůvka মোরাভিয়ার (আজকের চেক প্রজাতন্ত্রের একটি অঞ্চল) বিদ্যুৎ নেটওয়ার্ক সবচেয়ে কম খরচে বানানোর প্রশ্নে প্রথম MST অ্যালগরিদম প্রকাশ করেন। চার বছর পর, ১৯৩০ সালে, তাঁর দেশের আরেক গণিতবিদ Vojtěch Jarník আরেকটি পদ্ধতি দেন — যা ১৯৫৭ সালে Bell Labs-এ Robert Prim আর ১৯৫৯ সালে Edsger Dijkstra আবার স্বাধীনভাবে আবিষ্কার করেন। তাই অনেক বই একে Jarník-Prim অ্যালগরিদম বলে। Joseph Kruskal-এর পদ্ধতি প্রকাশিত হয় ১৯৫৬ সালে।
Borůvka-র অ্যালগরিদম রাউন্ডে রাউন্ডে চলে: প্রতি রাউন্ডে প্রতিটি component একসাথে নিজের সবচেয়ে সস্তা বাইরের edge বেছে নেয়, আর সব বাছাই করা edge একসাথে যোগ হয়। প্রতিটি বাছাই cut property-র সরাসরি প্রয়োগ (cut = ওই component বনাম বাকি সব)। প্রতিটি component অন্তত আরেকটির সাথে জোড়ে, তাই প্রতি রাউন্ডে component-এর সংখ্যা অন্তত অর্ধেক হয় — সর্বোচ্চ ⌈log₂ V⌉ রাউন্ড, প্রতিটি O(E): মোট O(E log V). সতর্কতা: ওজন সমান হলে দুই component দুটি ভিন্ন “সমান সস্তা” edge বেছে cycle বানাতে পারে — তাই সমতা ভাঙতে edge-এর index দিয়ে একটি নির্দিষ্ট নিয়ম লাগে।
| রাউন্ড | |
| প্রতিটি component-এর সস্তা edge | |
| component | |
| মোট খরচ |
- শুরু. ছয়টি component, প্রতিটি একটি উপজেলা।
- রাউন্ড ১. ছয়জনই একসাথে নিজের সবচেয়ে সস্তা edge বাছে। A আর C দুজনই A–C বাছে, E আর F দুজনই E–F — তাই ছয়টি বাছাই থেকে চারটি আলাদা edge: A–C, A–B, D–E, E–F. ছয়টি component থেকে দুটি — অর্ধেকেরও কম।
- রাউন্ড ২. {A, B, C}-এর বাইরের edge B–D (৬), C–D (৭), C–E (৮) — সবচেয়ে সস্তা B–D. {D, E, F}-ও একই edge বাছে। যোগ করতেই এক component, খরচ ১৬ — আবার একই MST.
রাউন্ডের ভেতরের কাজগুলো একে অন্যের ওপর নির্ভর করে না, তাই Borůvka সহজে parallel করা যায় — GPU আর বড় ডিস্ট্রিবিউটেড সিস্টেমে MST-এর জন্য এটিই প্রায়ই ভিত্তি। ১৯৯৫ সালে Karger, Klein আর Tarjan-এর randomized অ্যালগরিদম, যা expected (প্রত্যাশিত) O(E) সময়ে MST দেয়, তার ভেতরেও Borůvka-র রাউন্ড আছে।
Maximum spanning tree
কখনো উল্টোটা দরকার — যেমন প্রতিটি সংযোগের “নির্ভরযোগ্যতা” বা ব্যান্ডউইথ সবচেয়ে বেশি রেখে সব জুড়তে। Maximum spanning tree পেতে Kruskal-এ edge দামি থেকে সস্তা ক্রমে সাজান, বা সব ওজনকে −w করে সাধারণ MST চালান। দুটোই কাজ করে, কারণ cut আর cycle property-র প্রমাণ শুধু ওজনের ক্রম ব্যবহার করে। একই কারণে MST ঋণাত্মক ওজনেও ঠিক কাজ করে — Dijkstra-র (পর্ব ৪৩) মতো কোনো সমস্যা নেই।
Minimum bottleneck spanning tree
একটি spanning tree-র bottleneck হলো তার সবচেয়ে ভারী edge. যে spanning tree-র bottleneck সবচেয়ে ছোট, সেটি minimum bottleneck spanning tree (MBST). উপমা: বর্ষায় প্রতিটি রাস্তার একটি সর্বোচ্চ পানির গভীরতা; আপনি এমন রাস্তার জাল চান, যার সবচেয়ে খারাপ রাস্তাটিও যতটা সম্ভব কম ডোবে। প্রতিটি MST একটি MBST: ধরুন MST T-এর সবচেয়ে ভারী edge e, আর অন্য একটি spanning tree T′-এর সব edge e-এর চেয়ে হালকা। T থেকে e সরালে একটি cut হয়; T′ connected, তাই T′-এর একটি edge f এই cut পার হয়, আর w(f) < w(e). T − e + f তাহলে T-এর চেয়ে হালকা — বিরোধ। উল্টোটি সত্য নয়: একটি MBST-এর মোট খরচ বেশি হতে পারে। একই যুক্তিতে, MST-তে যেকোনো দুই vertex-এর মধ্যের পথটি তাদের মধ্যে “সবচেয়ে ছোট সর্বোচ্চ edge”-এর পথ (minimax path) — Kruskal-এর ক্রম থেকে এটি বোঝা সহজ।
আরও গভীরে — second-best MST: MST-এর পরের সবচেয়ে সস্তা spanning tree (যেমন সেরা পথটি কোনো কারণে বানানো না গেলে বিকল্প)। জানা ফল: second-best MST MST থেকে ঠিক একটি edge বদলে পাওয়া যায় — একটি non-tree edge (u, v) যোগ করুন, আর MST-তে u–v পথের সবচেয়ে ভারী edge সরান; সব non-tree edge-এর মধ্যে যেটিতে বাড়তি খরচ সবচেয়ে কম, সেটিই উত্তর। প্রতিটি vertex থেকে DFS করে সব জোড়ার “পথের সর্বোচ্চ edge” বের করলে O(V²); binary lifting দিয়ে (পর্ব ৪৮) প্রতিটি প্রশ্ন O(log V)-এ, মোট O(E log V) — কনটেস্টের পরিচিত সমস্যা। Deterministic O(E) সময়ে MST সম্ভব কি না, তা আজও অজানা: Chazelle (২০০০) দেন O(E α(E, V)), আর Pettie ও Ramachandran (২০০২) এমন একটি অ্যালগরিদম দেন যা প্রমাণসহ সর্বোত্তম — কিন্তু তার সঠিক জটিলতা কেউ জানে না।
৮ · MST shortest-path tree নয় — আর বাস্তব প্রয়োগ
সবচেয়ে প্রচলিত ভুল ধারণা: “MST-তে দুই উপজেলার মধ্যের পথই তাদের সবচেয়ে ছোট পথ”. না। MST কমায় মোট কেবলের খরচ; পর্ব ৪৩-এর Dijkstra-র shortest-path tree কমায় একটি উৎস থেকে প্রতিটি vertex-এর দূরত্ব। ছোট একটি প্রতি-উদাহরণ (counterexample) দেখুন — তিনটি উপজেলা S, X, Y:
| edge | S–X | S–Y | X–Y | মোট খরচ | S থেকে Y-এর দূরত্ব |
|---|---|---|---|---|---|
| খরচ | ৩ | ৪ | ২ | — | — |
| MST: X–Y, S–X | ✓ | ✓ | ৫ | ৩ + ২ = ৫ | |
| S থেকে shortest-path tree: S–X, S–Y | ✓ | ✓ | ৭ | ৪ |
MST সস্তা (৫ বনাম ৭), কিন্তু তাতে S থেকে Y যেতে ৫, যেখানে সরাসরি পথ ৪. কোনটি দরকার তা প্রশ্নের ওপর নির্ভর করে: কেবল একবার বসানোর খরচ কমাতে হলে MST; প্রতিদিন সদর থেকে প্রতিটি উপজেলায় দ্রুত পৌঁছাতে হলে shortest-path tree. আরেকটি পার্থক্য: সব খরচে ১০ যোগ করলে MST বদলায় না (প্রতিটি spanning tree-তে ঠিক V − 1 টি edge, সবার খরচ সমান বাড়ে), কিন্তু shortest path বদলাতে পারে, কারণ বিভিন্ন পথে edge-এর সংখ্যা আলাদা।
প্রয়োগ ১ — নেটওয়ার্ক নকশা
বিদ্যুৎ, ফাইবার, পানির পাইপ, সার্কিট বোর্ডের তার — সবখানে MST একটি প্রথম খসড়া দেয়। কিন্তু বাস্তবে দুটি সীমা মনে রাখতে হয়। (১) নির্ভরযোগ্যতা: tree-তে প্রতিটি edge একেকটি দুর্বল বিন্দু — একটি কেবল কাটলেই নেটওয়ার্ক দুই ভাগ (পর্ব ৪৬-এর bridge). তাই বাস্তব নকশায় MST-এর ওপর কিছু বাড়তি সংযোগ দিয়ে ring বানানো হয়। (২) নতুন সংযোগ-বিন্দু: মাঝপথে একটি নতুন জংশন বসালে খরচ আরও কমতে পারে — এটি Steiner tree সমস্যা, যা NP-hard (পর্ব ৬৪). মজার ব্যাপার, MST সেখানে একটি ভালো approximation দেয়: Steiner tree-র সর্বোত্তম খরচের ২ গুণের বেশি নয়।
প্রয়োগ ২ — clustering
ধরুন ২০০টি গ্রাহককে তাদের কেনাকাটার মিল অনুযায়ী k টি দলে ভাগ করতে চান, যাতে আলাদা দলের যেকোনো দুজনের মধ্যে দূরত্ব যতটা সম্ভব বেশি থাকে। উত্তর: Kruskal চালান, আর component-এর সংখ্যা k হলেই থামুন। যে দলগুলো রইল, সেগুলোই উত্তর — একে বলে single-linkage clustering. এখানে দলের মধ্যের ন্যূনতম দূরত্ব (spacing) হলো পরের যে edge দুটি দল জুড়ত, তার ওজন; আর cut property-র ধাঁচের একটি exchange যুক্তি দেখায় যে অন্য কোনো k-ভাগে spacing এর চেয়ে বেশি হতে পারে না। সমতুল্যভাবে: পুরো MST বানিয়ে সবচেয়ে ভারী k − 1 টি edge কেটে দিন।
প্রয়োগ ৩ — traveling salesman-এর approximation
একজন বিক্রয়কর্মীকে সব উপজেলা ঘুরে সদরে ফিরতে হবে, মোট পথ সবচেয়ে কম রেখে — traveling salesman problem (TSP), NP-hard. কিন্তু দূরত্ব যদি ত্রিভুজ অসমতা মানে (সরাসরি যাওয়া কখনো ঘুরে যাওয়ার চেয়ে দীর্ঘ নয়), তবে MST দিয়ে দ্রুত একটি ভালো উত্তর পাওয়া যায়: MST-কে DFS ক্রমে ঘুরুন, আর আগে দেখা উপজেলা এড়িয়ে সরাসরি পরেরটিতে যান। যেকোনো ট্যুর থেকে একটি edge সরালে একটি spanning tree পাওয়া যায়, তাই সর্বোত্তম ট্যুরও MST-এর চেয়ে দামি; আর আমাদের ট্যুর MST-এর ২ গুণের বেশি নয়। ফল: সর্বোত্তমের সর্বোচ্চ দ্বিগুণ — একটি ২-approximation. বিস্তারিত পর্ব ৬৬-এ।
পরীক্ষা ও ইন্টারভিউতে:
- Cut property লিখে প্রমাণ করুন। — Light edge e T-তে না থাকলে T-এর u–v পথে আরেকটি crossing edge f আছে; T − f + e হালকা বা সমান spanning tree.
- Kruskal আর Prim-এর time complexity? — Kruskal O(E log E) (সর্ট; DSU O(E α(V))); Prim binary heap-এ O(E log V), অ্যারেতে O(V²), Fibonacci heap-এ O(E + V log V).
- কখন Prim-এর O(V²) রূপ ভালো? — Dense graph-এ (E ≈ V²), যেখানে O(E log V) = O(V² log V).
- সব ওজন আলাদা হলে MST কয়টি? — ঠিক একটি; দুটি থাকলে symmetric difference-এর সবচেয়ে হালকা edge দিয়ে হালকা tree বানানো যায়।
- MST কি সবসময় shortest-path tree? — না; S–X ৩, S–Y ৪, X–Y ২ গ্রাফে MST-তে S থেকে Y ৫, সবচেয়ে ছোট পথ ৪.
৯ · AI-এর যুগে MST — greedy সহজ, প্রমাণ কঠিন
“Kruskal লিখে দাও” — যেকোনো AI সহকারী নিখুঁত দেখতে কোড দেবে, আর ছোট উদাহরণে উত্তরও প্রায়ই ঠিক আসবে। বিপদ লুকিয়ে থাকে তিন জায়গায়: Prim আর Dijkstra-র মধ্যে এক লাইনের পার্থক্য গুলিয়ে ফেলা, DSU-এর বদলে কোনো ভুল “cycle পরীক্ষা”, আর নতুন সমস্যায় অপ্রমাণিত greedy — “সবচেয়ে সস্তা আগে নাও” শুনতে MST-এর মতো হলেও সব সমস্যায় খাটে না।
MST-এর কোডে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| Prim-এ key = dist[u] + w (Dijkstra-র মতো) — ফলে shortest-path tree আসে | S–X ৩, S–Y ৪, X–Y ২ গ্রাফে চালান; MST-এর খরচ ৫ আসা উচিত, ৭ এলে ভুল |
| Kruskal-এ cycle পরীক্ষা “দুই প্রান্তই আগে দেখা হয়েছে কি না” দিয়ে | A–B ১, C–D ২, B–C ৩: B আর C দুজনই দেখা, অথচ আলাদা টুকরো — ভুল কোড B–C বাদ দেবে |
| “Kruskal O(E α(V))” বা “Prim O(V log V)” — সর্ট বা edge-এর খরচ বাদ দিয়ে | প্রতিটি অংশের খরচ আলাদা করে লিখতে বলুন: সর্ট, DSU, heap-এ insert-এর সংখ্যা |
| Disconnected গ্রাফে চুপচাপ ভুল উত্তর | গৃহীত edge-এর সংখ্যা V − 1 কি না পরীক্ষা করুন; Prim-এ সব vertex tree-তে কি না |
| Directed গ্রাফে Kruskal/Prim চালানো | Directed সংস্করণ (minimum arborescence) আলাদা সমস্যা — Chu–Liu/Edmonds অ্যালগরিদম লাগে |
| মোট খরচ int-এ — বড় ইনপুটে উপচে পড়া | ১০৫ টি edge, প্রতিটি ১০৯ দিয়ে পরীক্ষা করুন; long long লাগবে |
AI-কে কোড লিখে দিতে না বলে, আপনার কোড, trace আর প্রমাণ পরীক্ষা করতে বলুন:
MST-এর কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১প্রথম MST অ্যালগরিদম এসেছিল ১৯২৬ সালে মোরাভিয়ার বিদ্যুৎ লাইনের প্রশ্ন থেকে — কম্পিউটার আসার আগে Borůvka কেন এমন অ্যালগরিদম লিখলেন, আর আজ সেটি কেন আবার জনপ্রিয়?
প্রথম বিশ্বযুদ্ধের পর নতুন চেকোস্লোভাকিয়ায় দ্রুত বিদ্যুতায়ন চলছিল। গ্রামের পর গ্রামে লাইন টানতে হবে, আর তামার তার ও খুঁটি ছিল দামি। মোরাভিয়ার একটি বিদ্যুৎ কোম্পানির পক্ষ থেকে ব্রনো শহরের তরুণ গণিতবিদ Otakar Borůvka-র কাছে প্রশ্নটি আসে: অনেকগুলো শহর-গ্রামকে সবচেয়ে কম মোট দৈর্ঘ্যের লাইনে কীভাবে জোড়া যায়? ১৯২৬ সালে তিনি চেক ভাষায় দুটি প্রবন্ধে সমাধান প্রকাশ করেন — আজ আমরা যাকে MST বলি, তার প্রথম পরিচিত অ্যালগরিদম।
তখন “অ্যালগরিদম” মানে ছিল হাতে কাগজ-কলমে করার একটি পদ্ধতি, কম্পিউটারের প্রোগ্রাম নয়। Borůvka-র পদ্ধতির সৌন্দর্য হলো, প্রতিটি রাউন্ডে প্রতিটি শহর বা শহর-গুচ্ছ নিজে নিজে সিদ্ধান্ত নিতে পারে — “আমার সবচেয়ে কাছের প্রতিবেশী গুচ্ছ কোনটি?” একাধিক প্রকৌশলী একসাথে মানচিত্রের আলাদা অংশে কাজ করতে পারতেন। প্রতি রাউন্ডে গুচ্ছের সংখ্যা অন্তত অর্ধেক হয়, তাই হাজার গ্রামেও মাত্র দশটির মতো রাউন্ড।
অনেক বছর পশ্চিমা দুনিয়া এই কাজ প্রায় জানতই না। ১৯৩০ সালে Vojtěch Jarník Borůvka-কে লেখা চিঠির আকারে আরেকটি পদ্ধতি দেন, যা পরে Prim-এর নামে পরিচিত হয়। ১৯৫০-৬০-এর দশকে কয়েকজন গবেষক Borůvka-র পদ্ধতি স্বাধীনভাবে আবার বের করেন — যেমন Georges Sollin, তাই কিছু বইয়ে এর নাম “Sollin-এর অ্যালগরিদম”. Robert Graham আর Pavol Hell ১৯৮৫ সালে MST-এর ইতিহাস নিয়ে একটি প্রবন্ধ লিখে এই পুরোনো চেক কাজগুলো ব্যাপকভাবে সামনে আনেন।
আজ Borůvka আবার জনপ্রিয় ঠিক সেই “নিজে নিজে সিদ্ধান্ত” বৈশিষ্ট্যের জন্য। কোটি কোটি edge-এর গ্রাফে, যেখানে ডেটা অনেক মেশিনে ভাগ করা বা হাজার GPU-কোর একসাথে কাজ করে, Kruskal-এর “একটি বড় সর্ট, তারপর একে একে” ধরনটি কঠিন; কিন্তু Borůvka-র প্রতিটি রাউন্ড স্বাভাবিকভাবে parallel. Karger–Klein–Tarjan-এর প্রত্যাশিত (expected) রৈখিক সময়ের randomized অ্যালগরিদমও Borůvka-র রাউন্ড দিয়ে গ্রাফ ছোট করে।
মূল উপলব্ধি: একটি বাস্তব প্রকৌশল-প্রশ্ন থেকে জন্ম নেওয়া প্রায় শতবর্ষী পদ্ধতি parallel কম্পিউটিং-এর যুগে নতুন প্রাণ পেয়েছে — ভালো অ্যালগরিদমের মূল ধারণা প্রযুক্তি বদলালেও টিকে থাকে।
প্র ০২একই অ্যালগরিদম Jarník (১৯৩০), Prim (১৯৫৭) আর Dijkstra (১৯৫৯) তিনজন আলাদাভাবে বের করেছেন — আর Prim আর Dijkstra-র shortest path অ্যালগরিদম দেখতে প্রায় যমজ। পার্থক্যটি ঠিক কোথায়?
Vojtěch Jarník ১৯৩০ সালে Borůvka-র কাজের জবাবে একটি সরল পদ্ধতি লেখেন: একটি বিন্দু থেকে শুরু করে বারবার বাইরের সবচেয়ে কাছের বিন্দুটি জুড়ে দেওয়া। প্রবন্ধটি চেক ভাষায়, তাই বাইরের দুনিয়ায় প্রায় অপরিচিত থাকে। ২৭ বছর পর আমেরিকার Bell Labs-এ Robert Prim একই পদ্ধতি বের করেন। Bell Labs তখন টেলিফোন কোম্পানি AT&T-এর গবেষণাগার, আর টেলিফোন লাইনের নেটওয়ার্ক নকশায় সবচেয়ে কম দৈর্ঘ্যের সংযোগ-নেটওয়ার্কের প্রশ্ন সরাসরি কাজে লাগত। ১৯৫৭ সালে Bell System Technical Journal-এ তাঁর প্রবন্ধ প্রকাশিত হয়। ১৯৫৯ সালে Edsger Dijkstra তাঁর বিখ্যাত তিন পৃষ্ঠার প্রবন্ধে দুটি সমস্যার সমাধান দেন — প্রথমটি MST (Jarník-Prim পদ্ধতিতে), দ্বিতীয়টি আজকের Dijkstra-র shortest path.
দুটি অ্যালগরিদম যমজ মনে হওয়ার কারণ আছে: দুটোই একটি উৎস থেকে একটি tree বড় করে, দুটোই প্রতি ধাপে priority queue থেকে “সবচেয়ে ছোট” বাইরের vertex বের করে, দুটোরই জটিলতা binary heap-এ O(E log V). পার্থক্য মাত্র এক লাইনে — key কী:
- Prim: key[v] = min(key[v], w(u, v)) — শুধু শেষ edge-টির ওজন। প্রশ্ন: “v-কে tree-র সাথে জুড়তে সবচেয়ে সস্তা কেবল কোনটি?”
- Dijkstra: dist[v] = min(dist[v], dist[u] + w(u, v)) — উৎস থেকে পুরো পথের দৈর্ঘ্য। প্রশ্ন: “উৎস থেকে v কত দূরে?”
এই এক লাইনের পার্থক্যই অংশ ৮-এর প্রতি-উদাহরণের মূলে। আরও একটি পার্থক্য: Prim ঋণাত্মক ওজনে নির্ভুল, কারণ cut property-তে শুধু ওজনের ক্রম লাগে; Dijkstra ঋণাত্মক edge-এ ভুল করতে পারে (পর্ব ৪৩–৪৪)। তাই কোড-রিভিউতে বা AI-এর কোডে প্রথম দেখার জিনিস হলো relaxation-এর লাইনটি।
মূল উপলব্ধি: একই কাঠামো, ভিন্ন প্রশ্ন। কোড দেখে নয়, key-এর অর্থ দেখে বুঝুন অ্যালগরিদমটি কী অপ্টিমাইজ করছে — মোট সংযোগ-খরচ, নাকি উৎস থেকে দূরত্ব।
প্র ০৩MST-এর জন্য O(E log V) অ্যালগরিদম ১৯৫০-এর দশকেই ছিল। তাহলে পরের পঞ্চাশ বছর গবেষকরা কী খুঁজছিলেন — আর deterministic রৈখিক সময়ের প্রশ্নটি কেন আজও খোলা?
সর্টিং-এ আমরা জানি তুলনা-ভিত্তিক যেকোনো অ্যালগরিদমকে Ω(n log n) দিতেই হবে (পর্ব ২৪)। MST-তে এমন কোনো নিম্নসীমা নেই — উত্তর বের করতে সব edge একবার দেখা লাগে, তাই Ω(E), কিন্তু log V গুণক অবশ্যম্ভাবী কি না কেউ প্রমাণ করতে পারেননি। তাই প্রশ্ন দাঁড়াল: MST কি সর্টের চেয়ে সহজ?
১৯৭০-৮০-এর দশকে একের পর এক উন্নতি আসে। Andrew Yao ১৯৭৫ সালে O(E log log V) দেন। ১৯৮৪ সালে Michael Fredman আর Robert Tarjan Fibonacci heap আবিষ্কার করেন, যাতে Prim O(E + V log V) হয় — sparse নয় এমন গ্রাফে রৈখিক। তারপর Gabow, Galil, Spencer আর Tarjan এটিকে আরও কাছে নিয়ে যান, log-এর জায়গায় প্রায় ধ্রুবকের মতো ধীরে বাড়া ফাংশন।
১৯৯৫ সালে বড় লাফ: David Karger, Philip Klein আর Robert Tarjan একটি randomized অ্যালগরিদম দেন, যার প্রত্যাশিত (expected) সময় O(E) — কোনো ইনপুটের ওপর গড় নয়, অ্যালগরিদমের নিজের এলোমেলো পছন্দের ওপর গড়। মূল কৌশল: অর্ধেক edge এলোমেলোভাবে নিয়ে সেগুলোর MST বানান, তারপর cycle property দিয়ে মূল গ্রাফের অনেক “ভারী” edge একসাথে বাদ দিন — যে edge কোনো cycle-এ সবচেয়ে ভারী, সে কখনো লাগবে না। এর সাথে Borůvka-র রাউন্ড আর রৈখিক সময়ে “এই tree কি MST?” যাচাইয়ের একটি কৌশল (Komlós, Dixon–Rauch–Tarjan, King-এর কাজ) মিলে প্রত্যাশিত রৈখিক সময়।
Deterministic দিকে Bernard Chazelle ২০০০ সালে O(E α(E, V)) দেন — সেই inverse Ackermann, যা পর্ব ৩৩-এ DSU-তে দেখেছেন। আর ২০০২ সালে Seth Pettie আর Vijaya Ramachandran একটি অদ্ভুত ফল প্রকাশ করেন: একটি অ্যালগরিদম, যা প্রমাণসহ সর্বোত্তম (কোনো তুলনা-ভিত্তিক অ্যালগরিদম এর চেয়ে asymptotically ভালো হতে পারে না), কিন্তু এর সময় ঠিক কত — O(E) নাকি O(E α) — তা কেউ জানে না। অর্থাৎ আমাদের হাতে সেরা অ্যালগরিদম আছে, শুধু তার দাম জানি না।
মূল উপলব্ধি: দেখতে “সমাধান হয়ে যাওয়া” একটি সমস্যার ভেতরেও গভীর খোলা প্রশ্ন থাকতে পারে। বাস্তবে Kruskal আর Prim যথেষ্ট, কিন্তু তত্ত্বের সীমা খোঁজা থেকেই randomization আর amortized বিশ্লেষণের মতো সার্বজনীন হাতিয়ারগুলো এসেছে।
প্র ০৪বাস্তবের বিদ্যুৎ বা ফাইবার নেটওয়ার্ক কেন হুবহু MST নয় — Steiner tree আর নির্ভরযোগ্যতার প্রশ্ন MST-কে কোথায় সীমিত করে?
MST একটি নির্দিষ্ট প্রশ্নের নিখুঁত উত্তর: “দেওয়া vertex আর দেওয়া সম্ভাব্য edge থেকে, সবাইকে জোড়ার সবচেয়ে সস্তা উপায়”. বাস্তব নকশায় প্রশ্নটি প্রায় কখনো এত সরল থাকে না। তিনটি পার্থক্য সবচেয়ে গুরুত্বপূর্ণ।
প্রথমত, নতুন জংশন। তিনটি গ্রাম যদি একটি সমবাহু ত্রিভুজের কোণে থাকে, MST দুটি বাহু নেয়। কিন্তু ত্রিভুজের মাঝখানে একটি নতুন খুঁটি বসিয়ে তিন দিকে লাইন টানলে মোট দৈর্ঘ্য প্রায় ১৩% কম হয়। এই “বাড়তি বিন্দু বসানোর অনুমতি” দিলেই সমস্যাটি হয়ে যায় Steiner tree — ১৯৭২ সালে Richard Karp-এর বিখ্যাত ২১টি NP-complete সমস্যার তালিকায় এর একটি রূপ আছে। অর্থাৎ বড় ইনপুটে নিখুঁত উত্তর পাওয়ার কোনো দ্রুত পদ্ধতি জানা নেই। তবে MST এখানে বাতিল নয় — মূল বিন্দুগুলোর মধ্যের দূরত্ব দিয়ে MST বানালে তা সর্বোত্তম Steiner tree-র ২ গুণের বেশি নয়, আর বাস্তবের অনেক টুল এখান থেকেই শুরু করে।
দ্বিতীয়ত, নির্ভরযোগ্যতা। Tree-র প্রতিটি edge একটি bridge: বন্যায় একটি খুঁটি পড়লে বা রাস্তা খোঁড়ার সময় একটি ফাইবার কাটলে নেটওয়ার্ক দুই ভাগ। ব্যাংকের ATM বা হাসপাতালের সংযোগের মতো জরুরি সেবায় এটি গ্রহণযোগ্য নয়। তাই ফাইবার নেটওয়ার্ক সাধারণত ring বা mesh আকারে বানানো হয় — যাতে একটি সংযোগ কাটলে ডেটা উল্টো দিক দিয়ে ঘুরে যায়। “প্রতিটি জোড়ার মধ্যে অন্তত দুটি আলাদা পথ, সবচেয়ে কম খরচে” — এটি 2-edge-connected subgraph সমস্যা, যা আবার NP-hard.
তৃতীয়ত, ধারণক্ষমতা আর দূরত্ব। সদরের কাছের লাইন দিয়ে পুরো জেলার বিদ্যুৎ যায়, তাই সেখানে মোটা তার লাগে; আর অনেক ধাপ ঘুরে যাওয়া লাইনে ভোল্টেজ কমে যায়। ফলে খরচ শুধু দৈর্ঘ্যের ওপর নির্ভর করে না, প্রবাহের ওপরও — তখন সমস্যাটি network flow (পর্ব ৪৭) আর অপ্টিমাইজেশনের জগতে চলে যায়।
মূল উপলব্ধি: MST বাস্তব নকশার “প্রথম খসড়া” আর তুলনার মাপকাঠি — একটি নিম্নসীমা ও ভালো approximation. বাস্তব প্রকৌশলী জানেন কোন অনুমানগুলো (নতুন জংশন নেই, ব্যর্থতা নেই, ধারণক্ষমতা অসীম) তাঁর সমস্যায় খাটে না।
প্র ০৫Kruskal-এর ১৯৫৬ সালের প্রবন্ধের শিরোনামেই “traveling salesman problem” ছিল। MST কীভাবে TSP আর ডেটা বিজ্ঞানের clustering দুটোর সাথেই যুক্ত?
Joseph Kruskal-এর প্রবন্ধের শিরোনাম ছিল “On the shortest spanning subtree of a graph and the traveling salesman problem”. তখন অনেকে আশা করতেন, MST-এর মতো সরল greedy দিয়ে হয়তো TSP-ও সমাধান হবে। হয়নি — আজ আমরা জানি TSP NP-hard, আর P ≠ NP হলে এর কোনো দ্রুত নিখুঁত সমাধান নেই (পর্ব ৬৪)। কিন্তু MST TSP-এর সবচেয়ে কাজের সঙ্গী হয়ে রইল।
কারণটি সরল: একটি ট্যুর থেকে যেকোনো একটি edge মুছলে একটি spanning path থাকে, যা একটি spanning tree. তাই MST-এর খরচ ≤ সর্বোত্তম ট্যুরের খরচ — একটি নিম্নসীমা, যা দ্রুত হিসাব করা যায়। দূরত্ব ত্রিভুজ অসমতা মানলে MST-কে দুইবার ঘুরে (প্রতিটি edge দুবার) আর পুনরাবৃত্ত শহর এড়িয়ে ২-approximation পাওয়া যায়। ১৯৭৬ সালে Nicos Christofides এর সাথে একটি minimum-weight perfect matching জুড়ে ১.৫-approximation দেন, যা প্রায় অর্ধশতক ধরে সেরা ছিল। আর branch and bound (পর্ব ৫৭) দিয়ে নিখুঁত TSP সমাধানেও MST-ভিত্তিক নিম্নসীমা (যেমন Held–Karp-এর 1-tree) বহু বছর ব্যবহৃত হয়েছে।
ডেটা বিজ্ঞানে MST আসে অন্য পথে। ১৯৬৯ সালে J. C. Gower আর G. J. S. Ross দেখান যে পরিসংখ্যানবিদদের বহুল ব্যবহৃত single-linkage clustering আসলে MST-ই: দুটি গুচ্ছের দূরত্ব = তাদের সবচেয়ে কাছের দুই বিন্দুর দূরত্ব, আর বারবার সবচেয়ে কাছের দুই গুচ্ছ জোড়া মানে ঠিক Kruskal. ফলে পুরো clustering-এর গাছ (dendrogram) একটি MST থেকেই পাওয়া যায়। এর একটি পরিচিত দুর্বলতাও আছে — “chaining”: দুটি আলাদা দলের মাঝে কয়েকটি বিন্দুর সরু সেতু থাকলে MST তাদের এক দল বানিয়ে ফেলে। জনপ্রিয় HDBSCAN-এর মতো আধুনিক পদ্ধতিও ভেতরে একটি পরিবর্তিত দূরত্বে MST বানিয়ে এই সমস্যা কমায়।
মূল উপলব্ধি: MST শুধু “সস্তা নেটওয়ার্ক” নয় — এটি কঠিন সমস্যার নিম্নসীমা, approximation-এর কাঁচামাল আর ডেটার গঠন বোঝার একটি লেন্স। একটি সরল, প্রমাণিত greedy কত দূর যায়, MST তার সেরা উদাহরণ।
অনুশীলন
খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।
-
হাতে Kruskal trace করুন: পাঁচটি গ্রাম P, Q, R, S, T; edge: P–Q ২, P–R ৬, Q–R ৩, Q–S ৮, R–S ৪, R–T ৭, S–T ৯. সাজানো ক্রমে প্রতিটি edge-এর জন্য গ্রহণ না বাদ লিখুন, DSU-এর দলসহ। মোট খরচ কত?
সাজানো: P–Q ২, Q–R ৩, R–S ৪, P–R ৬, R–T ৭, Q–S ৮, S–T ৯. P–Q গ্রহণ → {P, Q}. Q–R গ্রহণ → {P, Q, R}. R–S গ্রহণ → {P, Q, R, S}. P–R: দুজনই একই দলে → বাদ (cycle P–Q–R). R–T গ্রহণ → সব এক দলে, চারটি edge = V − 1. Early stop না থাকলে Q–S আর S–T দুটোই বাদ।
MST: P–Q, Q–R, R–S, R–T; মোট খরচ ২ + ৩ + ৪ + ৭ = ১৬. লক্ষ করুন T-কে জুড়তে S–T (৯) নয়, R–T (৭) — cycle R–S–T-তে S–T সবচেয়ে ভারী, cycle property অনুযায়ী সেটি কখনো আসবে না।
-
হাতে Prim trace করুন: একই গ্রাফে T থেকে শুরু করে lazy heap-ভিত্তিক Prim চালান। প্রতিটি ধাপে heap-এর অবস্থা আর কোন vertex কোন edge দিয়ে যোগ হলো লিখুন। মোট খরচ কি অনুশীলন ১-এর সাথে মেলে?
T ঢুকল; heap: R–T ৭, S–T ৯. বের হলো R–T ৭ → R যোগ (খরচ ৭); ঢুকল Q–R ৩, R–S ৪, P–R ৬. heap: ৩, ৪, ৬, ৯. বের হলো Q–R ৩ → Q যোগ (১০); ঢুকল P–Q ২, Q–S ৮. বের হলো P–Q ২ → P যোগ (১২); P-এর নতুন কোনো বাইরের edge নেই (Q, R আগেই tree-তে)। বের হলো R–S ৪ → S যোগ (১৬). বাকি P–R ৬, Q–S ৮, S–T ৯ পরে stale বলে skip.
যোগের ক্রম R, Q, P, S; edge একই চারটি, খরচ ১৬ — মেলে, কারণ সব ওজন আলাদা আর MST অনন্য। খেয়াল করুন, সবচেয়ে সস্তা P–Q (২) Prim পেয়েছে তৃতীয় ধাপে — Prim edge-গুলো ওজনের ক্রমে নয়, tree-র কাছাকাছির ক্রমে নেয়।
-
ভেবে বলুন: (ক) প্রতিটি edge-এর খরচে ১০ কোটি টাকা যোগ করলে MST কি বদলাবে? আর দুই উপজেলার মধ্যে সবচেয়ে ছোট পথ? (খ) সব খরচ ধনাত্মক; প্রতিটি খরচকে বর্গ করলে MST বদলাবে? (গ) কোনো edge যদি bridge হয় (সরালে গ্রাফ বিচ্ছিন্ন হয়), সে কি সবসময় MST-তে থাকে?
(ক) MST বদলায় না: প্রতিটি spanning tree-তে ঠিক V − 1 টি edge, তাই সবার খরচ ঠিক ১০(V − 1) বাড়ে — ক্রম একই থাকে। Kruskal-এর দিক থেকেও: সর্টের ক্রম একই, তাই একই সিদ্ধান্ত। কিন্তু shortest path বদলাতে পারে, কারণ বিভিন্ন পথে edge-এর সংখ্যা আলাদা। উদাহরণ: S–X ১, X–Y ১, S–Y ৩. আগে S থেকে Y-এর সবচেয়ে ছোট পথ S–X–Y (২), সরাসরি পথ ৩; ১০ যোগ করার পর S–X–Y = ২২, সরাসরি S–Y = ১৩ — এবার সরাসরি পথই ছোট।
(খ) না। ধনাত্মক সংখ্যায় বর্গ করা ক্রম বদলায় না (a < b হলে a² < b²), আর Kruskal শুধু ক্রম দেখে — তাই একই edge গ্রহণ। যেকোনো কঠোরভাবে বাড়ন্ত ফাংশনে একই কথা। (গ) হ্যাঁ। Bridge e সরালে গ্রাফ দুই ভাগ হয়, আর সেই cut-এর একমাত্র crossing edge e; যেকোনো spanning tree-কে এই cut পার হতেই হবে, তাই প্রতিটি spanning tree-তে, সুতরাং প্রতিটি MST-তে, e আছে — যত দামিই হোক।
-
কোড লিখুন: k-clustering: edge-এর তালিকা আর একটি সংখ্যা k দেওয়া আছে। Kruskal চালিয়ে component-এর সংখ্যা k হলেই থামুন, আর spacing ফেরত দিন — পরের যে edge দুটি আলাদা দল জুড়ত, তার খরচ। আমাদের ছয় উপজেলার গ্রাফে k = 2 দিয়ে হাতে উত্তর মেলান।
Kruskal-এর লুপই, শুধু থামার শর্ত বদলেছে। দল k-তে নামার পর যে প্রথম edge আলাদা দল জুড়ত, সেটিই spacing:
// edges as (cost, u, v); returns the spacing of the best k-clustering int spacing(int n, vector<tuple<int, int, int>> es, int k) { vector<int> p(n); iota(p.begin(), p.end(), 0); auto root = [&](int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }; sort(es.begin(), es.end()); int comps = n; for (auto [w, u, v] : es) { int ru = root(u), rv = root(v); if (ru == rv) continue; // same cluster already if (comps == k) return w; // first edge that would merge two clusters p[ru] = rv; --comps; } return -1; // no edge left that merges two of the clusters }হাতে, k = 2: E–F, D–E, A–C, A–B গ্রহণের পর দল দুটি — {A, B, C} আর {D, E, F}. পরের B–C একই দলে, এড়িয়ে যাই; তারপর B–D (৬) দুই দল জুড়ত → spacing = ৬. সময় O(E log E), জায়গা O(V + E). সমতুল্যভাবে, MST-এর সবচেয়ে ভারী k − 1 = ১ টি edge (B–D) কাটলেও একই দুই দল।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমKruskal-এর অ্যালগরিদমের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- সব edge ওজন অনুযায়ী ছোট থেকে বড় ক্রমে সাজানো
- প্রতিটি vertex-এর জন্য DSU-তে একটি আলাদা দল বানানো (make-set)
- পরের edge (u, v)-এর জন্য find(u) আর find(v) বের করা
- দুই root আলাদা হলে edge-টি MST-তে নেওয়া আর union(u, v) করা
- V − 1 টি edge নেওয়া হলে থামা
১০টি vertex-এর একটি connected, weighted, undirected গ্রাফের যেকোনো minimum spanning tree-তে কয়টি edge থাকে?
- 10
- 9
- 45
- ওজনের ওপর নির্ভর করে
একটি গ্রাফের সব edge-এর ওজন আলাদা। একটি cut (S, V − S) পার হওয়া edge তিনটি, যাদের ওজন 7, 9 আর 3. কোন বক্তব্যটি নিশ্চিতভাবে সত্য?
- ওজন 9-এর edge প্রতিটি MST-তে আছে
- MST-তে এই তিনটির ঠিক একটি থাকবে
- ওজন 3-এর edge প্রতিটি MST-তে আছে
- কোনো edge সম্পর্কেই নিশ্চিত করে বলা যায় না
Sort আর DSU (union by rank + path compression) দিয়ে বাস্তবায়িত Kruskal-এর মোট time complexity কত?
- O(E log E)
- O(E α(V))
- O(V²)
- O(V · E)
একটি dense graph (E ≈ V²/2), adjacency matrix-এ রাখা। Prim-এর কোন বাস্তবায়ন asymptotically সবচেয়ে দ্রুত?
- Binary heap, O(E log V)
- Lazy deletion-সহ binary heap
- প্রথমে সব edge সর্ট করে নেওয়া
- সরল অ্যারেতে key রেখে প্রতি ধাপে স্ক্যান, O(V²)
তিনটি vertex S, X, Y; edge S–X ওজন 3, S–Y ওজন 4, X–Y ওজন 2. এই গ্রাফের MST-তে S থেকে Y-এর পথের দৈর্ঘ্য কত?
- 4
- 5
- 6
- 9
একটি গ্রাফের সব edge-এর ওজন আলাদা। কোনো একটি cycle-এর সবচেয়ে ভারী edge e সম্পর্কে কোনটি সত্য?
- e প্রতিটি MST-তে আছে
- e কিছু MST-তে থাকতে পারে, কিছুতে নয়
- e কোনো MST-তে নেই
- e একটি bridge, তাই সবসময় MST-তে থাকে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- MST = সব vertex জোড়া, cycle-হীন, সবচেয়ে কম মোট ওজনের edge-সেট; সবসময় V − 1 টি edge.
- Cut property: যেকোনো cut-এর light edge কোনো MST-তে আছে (exchange: T − f + e); cycle property: cycle-এর একক ভারীতম edge কোনো MST-তে নেই।
- Kruskal = সর্ট + DSU, O(E log E); Prim = একটি tree বড় করা, heap-এ O(E log V), dense-এ O(V²); দুটোই cut property-র প্রয়োগ।
- সব ওজন আলাদা → MST অনন্য; Borůvka (১৯২৬) প্রথম, parallel-বান্ধব; প্রতিটি MST একটি minimum bottleneck spanning tree.
- MST shortest-path tree নয়; প্রয়োগ: নেটওয়ার্ক নকশা, single-linkage clustering, metric TSP-এর ২-approximation.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৬ · SCC, bridge আর articulation pointপরবর্তী পর্বনেটওয়ার্কের দুর্বল বিন্দু আর directed গ্রাফের শক্ত অংশ খোঁজা।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ৩৩ · Disjoint set union (union-find)আগের ভিত্তিKruskal-এর ভেতরের DSU — union by size, path compression আর O(α(n)).
- পর্ব ২৩ · Heap, heapsort আর priority queueআগের ভিত্তিPrim-এর heap, decrease-key আর lazy deletion.
- C++ প্রোগ্রামিং · STL algorithmপাশের ট্র্যাক
sort,priority_queueআর tuple-এর তুলনা — Kruskal ও Prim লেখার তৈরি হাতিয়ার।