সরাসরি পর্বে যান
পর্ব ৬৭ · ৮৩-এর মধ্যে · মডিউল ১১

Spanning tree — DFS/BFS, minimum spanning tree আর Kirchhoff-এর গণনা

Spanning Trees — DFS and BFS Trees, Minimum Spanning Trees (Cut and Cycle Properties, Kruskal, Prim), Kirchhoff's Matrix-Tree Theorem and the MST 2-Approximation for Metric TSP

Taught in Bangla, this lesson proves a graph has a spanning tree exactly when it is connected and compares DFS and BFS trees. It proves the cut and cycle properties of minimum spanning trees by an exchange argument, shows Kruskal and Prim are correct through the cut property, and gives uniqueness when weights differ. Kirchhoff's matrix-tree theorem counts spanning trees with a Laplacian determinant, and MST doubling gives a 2-approximation for metric TSP. In-page Python runs each algorithm.

এই পর্বে যা শিখবেন

  • Spanning tree-এর সংজ্ঞা, অস্তিত্বের প্রমাণ, আর edge-বদলের lemma
  • DFS tree আর BFS tree-এর দুটি মৌলিক ধর্মের প্রমাণ, আর bipartite পরীক্ষা
  • Minimum spanning tree-এর cut property, cycle property আর অনন্যতার পূর্ণ প্রমাণ
  • Kruskal আর Prim হাতে চালানো, আর প্রতিটি ধাপের যুক্তি cut property দিয়ে দেওয়া
  • Kirchhoff-এর matrix-tree theorem দিয়ে spanning tree গোনা, Cayley-র সূত্র আর MST দিয়ে TSP-এর 2-approximation

১ · Spanning tree কী, আর কখন থাকে

বাজারঘর ঢাকা আর আশপাশের সাতটি হাবকে ফাইবার কেবলে জুড়তে চায়, যাতে যেকোনো হাব থেকে যেকোনো হাবে অর্ডারের তথ্য যেতে পারে। সম্ভাব্য প্রতিটি কেবল-পথ জানা আছে। সব পথে কেবল টানা অপচয়: যদি দুই হাবের মধ্যে আগে থেকেই একটি ঘুরপথ থাকে, নতুন কেবল কোনো নতুন সংযোগ আনে না। ঠিক যতটুকু দরকার, ততটুকু কেবল: এটাই spanning tree.

এই পর্বে graph মানে সসীম undirected graph G = (V, E), |V| = n, |E| = m; দরকার হলে multigraph-ও চলবে। Tree মানে connected আর cycle-হীন graph. পর্ব ৬৫ “Tree — ধর্ম, rooted tree আর m-ary tree-এর হিসাব” tree-এর সমতুল্য সংজ্ঞাগুলো প্রমাণ করে; এখানে আমরা সেখান থেকে তিনটি তথ্য ব্যবহার করব: n vertex-এর tree-তে ঠিক n − 1টি edge; tree-তে দুই vertex-এর মধ্যে পথ ঠিক একটি; আর n vertex-এর connected graph-এ n − 1টি edge থাকলে সেটি tree.

সংজ্ঞা ১.১ (spanning tree আর forest)

G-এর একটি subgraph T-কে spanning tree বলে, যদি T একটি tree আর T-তে G-এর সব vertex আছে। সাধারণত T-কে তার edge-সেট দিয়েই বোঝানো হয়। Cycle-হীন graph-কে forest বলে (যার প্রতিটি component একটি tree); G-এর প্রতিটি component-এর একটি করে spanning tree মিলে spanning forest.

উপপাদ্য ১.২ (অস্তিত্ব)

G-এর একটি spanning tree আছে যদি এবং কেবল যদি G connected.

(⇒) T connected আর G-এর প্রতিটি vertex T-তে আছে, তাই যেকোনো দুই vertex-এর মধ্যে T-এর ভেতরেই পথ আছে, আর সেটি G-এরও পথ। (⇐) ধরুন G connected.

  1. G-এর সব vertex রেখে কিছু edge বাছলে যত connected subgraph পাওয়া যায়, তাদের সেট খালি নয় (G নিজেই একটি)। পর্ব ১১-এর well-ordering অনুযায়ী এদের মধ্যে সবচেয়ে কম edge-এর একটি H আছে। — well-ordering
  2. ধরুন H-এ একটি cycle C আছে আর e = uv তার একটি edge. H − e থেকেও u থেকে v-তে যাওয়া যায়: C-এর বাকি অংশ ঘুরে। তাই H-এর যে walk e ব্যবহার করত, সেটি ঘুরপথে চলে, আর H − e connected থাকে। — cycle-এর ঘুরপথ
  3. H − e-তে H-এর চেয়ে কম edge, অথচ সেটি connected আর সব vertex ধারণ করে: H-এর বাছাইয়ের বিরোধ। তাই H-এ কোনো cycle নেই। — বিরোধ
  4. H connected, cycle-হীন, আর সব vertex ধারণ করে: H একটি spanning tree. ∎

প্রমাণটি একটি পদ্ধতিও দেয়: cycle পেলে তার একটি edge মুছুন, আর cycle না থাকা পর্যন্ত চালিয়ে যান। Spanning tree-তে n − 1টি edge, তাই মোট ঠিক m − n + 1টি edge মুছতে হয়, যেকোনো ক্রমে। এই সংখ্যার নাম circuit rank বা cyclomatic number. পর্ব ৫৯-এ দেখেছেন connected graph-এ m ≥ n − 1; spanning tree সেই সীমার নিখুঁত উদাহরণ।

নিচের ছবিতে বাজারঘরের সাত হাব আর ১১টি সম্ভাব্য কেবল-পথ, প্রতিটির পাশে কিলোমিটারে কেবলের দৈর্ঘ্য (সংখ্যাগুলো কাল্পনিক, আসল দূরত্ব নয়)। এমন graph-কে weighted graph বলে, আর সংখ্যাগুলো edge weight. এখানে m − n + 1 = 11 − 7 + 1 = 5, তাই যেকোনো spanning tree পাঁচটি পথ বাদ দেয়। scratch-এ সব উপসেট গুনে দেখা গেছে, এই graph-এ মোট ১৪১টি spanning tree, যাদের ওজন 31 থেকে 53. সবুজ মোটা রেখাগুলো সবচেয়ে হালকা tree, যা §৩–৪-এ আমরা খুঁজে বের করব।

বাজারঘরের সাত হাবের কেবল-নকশা

D ঢাকা, G গাজীপুর, T টঙ্গী, S সাভার, M মানিকগঞ্জ, K কেরানীগঞ্জ, N নারায়ণগঞ্জ। সংখ্যা = কেবলের দৈর্ঘ্য (কিমি, কাল্পনিক)। সবুজ মোটা রেখা MST, মোট 31; ধূসর রেখা বাদ পড়া পাঁচটি পথ।

Weighted graph of seven hubs; the minimum spanning tree DK, DT, KN, TG, SK, SM (total 31) is drawn thick and green 2345 6789 101112 DGTS MKN

একটি ছোট কিন্তু বারবার লাগা তথ্য এখনই প্রমাণ করে রাখি। এটিই পরের সব প্রমাণের যন্ত্র।

Lemma ১.৩ (edge-বদল)

T একটি spanning tree, e = uv ∉ T, আর P হলো T-এর ভেতরে u থেকে v-এর একমাত্র পথ। তাহলে (ক) T + e-তে ঠিক একটি cycle আছে, P + e, যার নাম e-এর fundamental cycle; (খ) P-এর যেকোনো edge f-এর জন্য T′ = T + e − f আবার একটি spanning tree.

  1. (ক) T + e-এর যেকোনো cycle-এ e থাকতেই হবে (T নিজে cycle-হীন)। e বাদ দিলে cycle-এর বাকি অংশ T-এ u থেকে v-এর একটি পথ, আর সেই পথ একটিই, P. তাই cycle-ও একটিই। — tree-তে পথ অনন্য
  2. (খ) T′-এ edge-সংখ্যা (n − 1) + 1 − 1 = n − 1. — গণনা
  3. T′ connected: T-এর যে পথ f ব্যবহার করত, তাতে f-এর জায়গায় cycle P + e-এর বাকি অংশ বসান; সেই অংশে f নেই, e আছে, তাই সেটি T′-এর ভেতরে। — ঘুরপথ
  4. n vertex, n − 1টি edge আর connected: পর্ব ৬৫-এর সমতুল্যতা অনুযায়ী T′ একটি tree. ∎

২ · DFS tree আর BFS tree — খোঁজার পথই একটি tree

Spanning tree খোঁজার সবচেয়ে স্বাভাবিক উপায় হলো graph ঘুরে দেখা। BFS (breadth-first search) একটি root s থেকে স্তরে স্তরে এগোয়: আগে s-এর সব প্রতিবেশী, তারপর তাদের নতুন প্রতিবেশী। DFS (depth-first search) যতদূর পারে গভীরে যায়, আটকে গেলে পেছনে ফেরে। দুই ক্ষেত্রেই প্রতিটি নতুন vertex v যে edge দিয়ে প্রথম আবিষ্কৃত হয়, সেটি v থেকে তার “parent”-এর দিকে একটি edge. এই edge-গুলোকে tree edge বলে, আর এরা মিলে BFS tree বা DFS tree. Root ছাড়া প্রতিটি পৌঁছানো vertex ঠিক একটি parent পায়, আর parent সবসময় আগে আবিষ্কৃত, তাই এই edge-গুলোতে cycle হয় না: G connected হলে n − 1টি edge-এর একটি spanning tree পাওয়া যায়। Algorithm-এর কোড DSA কোর্সের পর্ব ৪০ আর পর্ব ৪১-এ; এখানে আমরা দুই tree-এর গঠনের প্রমাণ দেখব।

উপপাদ্য ২.১ (BFS tree)

Connected graph-এ root s থেকে BFS চালালে: (ক) প্রতিটি vertex v-এর স্তর L(v) = d(s, v), তাই BFS tree-তে s থেকে v-এর পথটি G-এর একটি সবচেয়ে ছোট পথ; (খ) G-এর প্রতিটি edge uv-এর জন্য |L(u) − L(v)| ≤ 1.

  1. (ক) পর্ব ৫৯-এর উপপাদ্য ৮.১ ঠিক এই কথা প্রমাণ করেছে: BFS যে স্তরে v-কে প্রথম পায়, সেটিই দূরত্ব d(s, v). Tree-তে s থেকে v-এর পথে স্তর প্রতি ধাপে ১ করে বাড়ে, তাই পথের দৈর্ঘ্য L(v) = d(s, v). — পর্ব ৫৯
  2. (খ) uv একটি edge হলে s থেকে u-এর সবচেয়ে ছোট পথের শেষে uv জুড়লে v-তে একটি walk পাওয়া যায়, তাই d(s, v) ≤ d(s, u) + 1. একইভাবে d(s, u) ≤ d(s, v) + 1. — দূরত্বের triangle inequality
  3. দুটি মিলিয়ে আর (ক) ব্যবহার করে |L(u) − L(v)| ≤ 1. ∎

বাজারঘরের graph-এ D থেকে BFS: K, T, N, G, S স্তর 1-এ, আর M স্তর 2-এ (scratch-এ মেলানো)। (খ) থেকে একটি পুরোনো প্রশ্নের সুন্দর উত্তর আসে। কোনো edge একই স্তরের দুই vertex u, v জুড়লে, tree-তে তাদের সবচেয়ে কাছের সাধারণ পূর্বপুরুষ w থেকে u আর v-এর দূরত্ব সমান, ধরুন k; তাহলে w থেকে u, তারপর uv, তারপর v থেকে w মিলে দৈর্ঘ্য 2k + 1-এর একটি বিজোড় cycle. আর এমন edge না থাকলে স্তরের জোড়-বিজোড় দিয়ে দুই রঙে রাঙানো যায়, কারণ প্রতিটি edge পাশাপাশি স্তর জোড়ে। তাই BFS একবার চালিয়েই জানা যায় graph bipartite কি না।

উপপাদ্য ২.২ (DFS tree)

Undirected connected graph-এ DFS tree T-এর বাইরের প্রতিটি edge uv একটি পূর্বপুরুষ আর তার বংশধরকে জোড়ে (T-কে root থেকে ঝোলানো rooted tree ধরে)। অর্থাৎ undirected graph-এর DFS-এ কোনো cross edge নেই, শুধু back edge.

  1. DFS-এর visit(u) চলাকালে যে vertex প্রথমবার আবিষ্কৃত হয়, সে u-এর বংশধর: তাকে আবিষ্কার করে এমন একটি visit, যা visit(u)-এর ভেতর থেকে ডাকা, আর তার parent-শৃঙ্খল ডাকার শৃঙ্খল ধরে u-তে পৌঁছায়। — recursion-এর গঠন
  2. যেকোনো edge uv নিন; ধরুন u আগে আবিষ্কৃত। visit(u) শুরু হওয়ার সময় v তখনো অদেখা। — ধরে নেওয়া (WLOG)
  3. visit(u) শেষ হওয়ার আগে u-এর প্রতিবেশী-তালিকায় v-এর পালা আসে। তখন v অদেখা থাকলে সেখানেই আবিষ্কৃত হয় (u তার parent); আর আগেই দেখা হয়ে থাকলে তা visit(u) চলাকালেই হয়েছে। দুই ক্ষেত্রেই ধাপ ১ অনুযায়ী v হলো u-এর বংশধর। ∎ — ধাপ ১

এই ধর্মই bridge খোঁজার ভিত্তি: tree edge (p, c) bridge ঠিক তখন, যখন c-এর subtree থেকে কোনো back edge p বা তার ওপরে পৌঁছায় না। নিচের ব্লক এলোমেলো graph-এ দুই উপপাদ্যই পরীক্ষা করে। প্রথমবার চালালে ব্রাউজারে Python (Pyodide) নামতে কয়েক সেকেন্ড লাগে।

from collections import deque
import random
def dfs_tree(adj, root):
    parent = {root: None}
    def visit(u):
        for v in adj[u]:
            if v not in parent:
                parent[v] = u; visit(v)
    visit(root); return parent
def bfs_tree(adj, root):
    parent, level, q = {root: None}, {root: 0}, deque([root])
    while q:
        u = q.popleft()
        for v in adj[u]:
            if v not in parent:
                parent[v], level[v] = u, level[u] + 1; q.append(v)
    return parent, level
def ancestors(P, v):
    return {v} | ancestors(P, P[v]) if v is not None else set()
random.seed(59); graphs = nontree = 0
for trial in range(300):
    n = random.randint(2, 9)
    E = [(u, v) for u in range(n) for v in range(u + 1, n) if random.random() < 0.4]
    adj = {u: [] for u in range(n)}
    for u, v in E: adj[u].append(v); adj[v].append(u)
    P = dfs_tree(adj, 0)
    if len(P) < n: continue                        # keep connected graphs only
    B, lev = bfs_tree(adj, 0); graphs += 1
    for u, v in E:
        if P[u] != v and P[v] != u:                # DFS non-tree edge
            nontree += 1
            assert u in ancestors(P, v) or v in ancestors(P, u)
        assert abs(lev[u] - lev[v]) <= 1           # BFS: same or next level
print(graphs, "connected graphs,", nontree, "DFS non-tree edges: all ancestor-descendant;",
      "every edge joins BFS levels that differ by at most 1")

আসল আউটপুট (ব্রাউজারের Python):

166 connected graphs, 443 DFS non-tree edges: all ancestor-descendant; every edge joins BFS levels that differ by at most 1

৩০০টি এলোমেলো graph-এর মধ্যে ১৬৬টি connected ছিল। তাদের DFS tree-এর বাইরের ৪৪৩টি edge-এর প্রতিটির এক প্রান্ত অন্য প্রান্তের পূর্বপুরুষ (উপপাদ্য ২.২), আর প্রতিটি edge BFS-এর একই বা পাশের স্তর জোড়ে (উপপাদ্য ২.১(খ))। লক্ষ করুন, একটি tree edge-কে চেনা হয়েছে “P[u] == v বা P[v] == u” দিয়ে; এটি simple graph-এ ঠিক আছে, multigraph-এ নয়।

৩ · Minimum spanning tree — cut property আর cycle property

১৪১টি spanning tree-এর মধ্যে বাজারঘর চায় সবচেয়ে কম মোট কেবলের tree-টি। Spanning tree T-এর ওজন w(T) = T-এর edge-গুলোর ওজনের যোগফল। যে spanning tree-এর ওজন সবচেয়ে কম, সেটি minimum spanning tree (MST). Connected graph-এ spanning tree সসীম সংখ্যক, তাই MST সবসময় আছে; প্রশ্ন হলো দ্রুত খুঁজে পাওয়া আর প্রমাণ করা যে খুঁজে পাওয়াটিই সর্বনিম্ন। সব tree গোনা চলে না: Kn-এ nn−2টি tree (§৬)।

দুটি পরিভাষা লাগবে। Cut হলো vertex-গুলোর দুই অশূন্য ভাগে বিভাজন (S, V − S). যে edge-এর এক প্রান্ত S-এ আর অন্যটি V − S-এ, সেটি cut-টি পার হয়: crossing edge. পার হওয়া edge-গুলোর মধ্যে যার ওজন সবচেয়ে কম (সমান থাকলে তাদের যেকোনোটি), সেটি cut-এর light edge. বাজারঘরের graph-এ S = {S} নিলে পার হওয়া edge S–K 7, D–S 9, S–M 10, G–S 11, তাই light edge S–K.

উপপাদ্য ৩.১ (cut property)

ধরুন A ⊆ E এমন edge-সেট যা কোনো একটি MST-এর অংশ, (S, V − S) এমন একটি cut যা A-এর কোনো edge পার হয় না, আর e এই cut-এর একটি light edge. তাহলে A ∪ {e}-ও কোনো একটি MST-এর অংশ। আরও: e যদি পার হওয়া edge-গুলোর মধ্যে একাই সবচেয়ে হালকা হয়, তবে e প্রতিটি MST-তে আছে।

  1. T একটি MST যাতে A ⊆ T. e ∈ T হলে কাজ শেষ। ধরুন e = uv ∉ T, যেখানে u ∈ S, v ∈ V − S. — অনুমান
  2. T-এ u থেকে v-এর পথ P নিন। P শুরু হয় S-এ, শেষ হয় V − S-এ, তাই P-এর অন্তত একটি edge f cut-টি পার হয়। A কোনো edge দিয়ে cut পার হয় না, তাই f ∉ A. — পথকে কোথাও না কোথাও পার হতে হয়
  3. Lemma ১.৩(খ) অনুযায়ী T′ = T + e − f একটি spanning tree, আর A ∪ {e} ⊆ T′. — edge-বদল
  4. e light edge আর f-ও পার হয়, তাই w(e) ≤ w(f), ফলে w(T′) = w(T) + w(e) − w(f) ≤ w(T). T সর্বনিম্ন, তাই T′-ও MST. — সমান ওজনও এখানে চলে, তাই tie কোনো সমস্যা নয়
  5. e একাই সবচেয়ে হালকা হলে w(e) < w(f), তাই e ∉ T ধরলে w(T′) < w(T), যা অসম্ভব। অতএব e প্রতিটি MST-তে। ∎ — বিরোধ

ধাপ ৪-এর “≤” চিহ্নটি গুরুত্বপূর্ণ। অনেক বইয়ে প্রমাণ শুধু আলাদা ওজনের জন্য দেওয়া থাকে; সমান ওজন থাকলে “প্রতিটি MST-তে আছে” কথাটি ভুল হতে পারে (ত্রিভুজের তিন edge-এর ওজন 1 হলে কোনো edge-ই সব MST-তে নেই), কিন্তু “কোনো একটি MST-তে আছে” সবসময় সত্য। Algorithm-এর জন্য দুর্বল রূপটিই যথেষ্ট। প্রমাণের কৌশলটির নাম exchange argument: সর্বোত্তম উত্তর থেকে একটি অংশ বদলে আমাদের পছন্দ ঢোকানো, ক্ষতি ছাড়াই।

উপপাদ্য ৩.২ (cycle property)

কোনো cycle C-তে edge e-এর ওজন C-এর বাকি প্রতিটি edge-এর চেয়ে কঠোরভাবে বেশি হলে e কোনো MST-তে নেই। (C-তে e সবচেয়ে ভারী, কিন্তু সমান ভারী আরও থাকলে, অন্তত একটি MST e এড়িয়ে যায়।)

  1. বিপরীতে ধরুন e = uv একটি MST T-তে আছে। T − e দুটি component-এ ভাঙে; u-এরটির vertex-সেট S, তাই v ∈ V − S. — tree থেকে একটি edge সরালে দুই টুকরো
  2. C − e হলো u থেকে v-এর একটি পথ, তাই তার কোনো একটি edge f cut (S, V − S) পার হয়, আর f ≠ e. — উপপাদ্য ৩.১-এর ধাপ ২-এর মতো
  3. T − e + f connected (দুই টুকরো f জোড়ে) আর তাতে n − 1টি edge, তাই spanning tree; আর w(f) < w(e), তাই এর ওজন w(T)-এর চেয়ে কম। বিরোধ। ∎ — edge-বদল

দুই ধর্ম একে অন্যের প্রতিচ্ছবি: cut property বলে কোন edge নিশ্চিন্তে নেওয়া যায়, cycle property বলে কোন edge নিশ্চিন্তে বাদ দেওয়া যায়। প্রথমটি থেকে আসে Kruskal আর Prim, দ্বিতীয়টি থেকে “reverse-delete” পদ্ধতি: সবচেয়ে ভারী থেকে শুরু করে প্রতিটি edge মুছুন, যদি মোছার পরও graph connected থাকে।

উপপাদ্য ৩.৩ (অনন্যতা)

Connected graph-এর সব edge-এর ওজন আলাদা হলে MST ঠিক একটি।

  1. বিপরীতে ধরুন T1 ≠ T2 দুটি MST. দুটিতে edge-সংখ্যা সমান, তাই symmetric difference T1 ⊕ T2 অশূন্য। তার সবচেয়ে হালকা edge e নিন (ওজন আলাদা, তাই অনন্য); ধরুন e ∈ T1 − T2. — WLOG
  2. T2 + e-তে e-এর fundamental cycle C আছে (Lemma ১.৩)। C-এর সব edge T1-এ থাকতে পারে না, কারণ T1 cycle-হীন; তাই C-তে একটি f ∈ T2 − T1 আছে, আর f ≠ e. — tree-তে cycle নেই
  3. f-ও symmetric difference-এ, আর e সেখানে সবচেয়ে হালকা, ওজন আলাদা: w(e) < w(f). — e-এর বাছাই
  4. T2 + e − f একটি spanning tree (Lemma ১.৩(খ)), যার ওজন w(T2)-এর চেয়ে কম। T2 MST হওয়ার বিরোধ। ∎

বাজারঘরের graph-এ সব ওজন আলাদা, তাই MST একটিই; scratch-এ ১৪১টি tree গুনে মেলানো: ওজন 31-এর tree একটিই, তার পরের ওজন 33 (এমন tree তিনটি).

৪ · Kruskal আর Prim — দুই পথ, একই উত্তর

Kruskal-এর algorithm (Joseph Kruskal, ১৯৫৬): সব edge ওজনের ছোট থেকে বড় ক্রমে সাজান। খালি forest A থেকে শুরু করে প্রতিটি edge দেখুন: যদি তা A-এর দুটি আলাদা component জোড়ে, নিন; একই component-এর দুই vertex জুড়লে (অর্থাৎ cycle বানালে) বাদ দিন। “একই component কি না” প্রশ্নের দ্রুত উত্তর দেয় union-find, যার দলগুলো আসলে পর্ব ৫৫-এর equivalence class: “A-তে পথ আছে” সম্পর্কের class.

1১১টি edge, ওজনে সাজানো2D–K 23D–T 34K–N 45T–G 56D–N 67S–K 78D–G 8, D–S 99S–M 10
Forest A আর যুক্তি
সিদ্ধান্ত
যুক্তি
দল
মোট
  1. শুরু। ওজনের ক্রম: D–K 2, D–T 3, K–N 4, T–G 5, D–N 6, S–K 7, D–G 8, D–S 9, S–M 10, G–S 11, M–K 12. A খালি, সাতটি vertex সাতটি আলাদা দল। n − 1 = 6টি edge নেওয়া হলে কাজ শেষ।
  2. D–K 2 নেওয়া। D আর K আলাদা দলে। যুক্তি: cut S = {D} (D-এর দল) A-এর কোনো edge পার হয় না, আর তার পার হওয়া edge D–K 2, D–T 3, D–N 6, D–G 8, D–S 9-এর মধ্যে D–K সবচেয়ে হালকা। Cut property অনুযায়ী নেওয়া নিরাপদ।
  3. D–T 3 নেওয়া। D-এর দল এখন {D, K}. এই cut পার হয় D–T 3, K–N 4, D–N 6, S–K 7, D–G 8, D–S 9, M–K 12; সবচেয়ে হালকা D–T.
  4. K–N 4 নেওয়া। K-এর দল {D, K, T}; পার হওয়া edge-গুলোর মধ্যে K–N 4 সবচেয়ে হালকা (পরেরটি T–G 5).
  5. T–G 5 নেওয়া। T-এর দল {D, K, N, T}; পার হয় T–G 5, S–K 7, D–G 8, D–S 9, M–K 12. সবচেয়ে হালকা T–G. মোট এখন 2 + 3 + 4 + 5 = 14.
  6. D–N 6 বাদ। D আর N একই দলে, কারণ D–K–N পথ আগেই আছে। নিলে cycle D–K–N হতো, যার ওজন 2, 4, 6; এই cycle-এ 6 একাই সবচেয়ে ভারী, তাই cycle property অনুযায়ী কোনো MST-তে D–N নেই।
  7. S–K 7 নেওয়া। S এখনো একা। Cut {S} পার হয় S–K 7, D–S 9, S–M 10, G–S 11; সবচেয়ে হালকা S–K. লক্ষ করুন, 7-এর আগে আসা কোনো edge এই cut পার হয় না, ঠিক Kruskal-এর প্রমাণের মূল কথা।
  8. D–G 8 আর D–S 9 বাদ। D–G নিলে cycle D–T–G (3, 5, 8); D–S নিলে cycle D–K–S (2, 7, 9). দুটিতেই নতুন edge একাই সবচেয়ে ভারী।
  9. S–M 10 নেওয়া, শেষ। Cut {M} পার হয় শুধু S–M 10 আর M–K 12; হালকা S–M. ছয়টি edge হয়ে গেছে, মোট 2 + 3 + 4 + 5 + 7 + 10 = 31. বাকি G–S 11 আর M–K 12 দেখলেও বাদ পড়ত। মোট বাদ ৫টি, ঠিক m − n + 1.
উপপাদ্য ৪.১ (Kruskal-এর সঠিকতা)

Connected weighted graph-এ Kruskal-এর algorithm একটি MST দেয় (সমান ওজন থাকলেও)।

  1. Invariant: প্রতিটি ধাপের পরে A কোনো একটি MST-এর অংশ। শুরুতে A = ∅, যা সব MST-এর অংশ। — loop invariant (পর্ব ১৩)
  2. ধরুন algorithm e = uv নেয়, যেখানে u-এর দল Cu আর v-এর দল আলাদা। Cut (Cu, V − Cu) নিন; Cu একটি component বলে A-এর কোনো edge এটি পার হয় না। — cut বাছাই
  3. e এই cut-এর light edge: e-এর চেয়ে হালকা প্রতিটি edge g আগে দেখা হয়েছে। নেওয়া হলে g ∈ A, যা পার হয় না। বাদ পড়লে তখনই g-এর দুই প্রান্ত একই দলে ছিল; দল শুধু বড় হয়, তাই এখনো একই দলে, আর g পার হয় না। — দল কখনো ভাঙে না
  4. Cut property অনুযায়ী A ∪ {e} কোনো একটি MST-এর অংশ: invariant বজায় থাকে। — উপপাদ্য ৩.১
  5. শেষে A-তে cycle নেই (প্রতিটি নেওয়া edge দুটি দল জোড়ে)। A-তে দুটির বেশি দল থাকলে, G connected বলে তাদের মধ্যে একটি edge আছে, আর তা দেখার সময় দুই প্রান্ত আলাদা দলে থাকায় নেওয়া হতো। তাই A একটি spanning tree, আর একটি MST-এর অংশ হওয়ায় নিজেই MST. ∎

Prim-এর algorithm (Robert Prim, ১৯৫৭; Vojtěch Jarník ১৯৩০ সালে আগেই দেন বলে উল্লেখ করা হয়) অন্যভাবে এগোয়: একটি vertex থেকে একটি tree বাড়ান, প্রতিবার tree থেকে বাইরে যাওয়া সবচেয়ে হালকা edge জুড়ে। এর সঠিকতা আরও সরাসরি: tree-এর vertex-সেট S হলে cut (S, V − S) A-এর কোনো edge পার হয় না (A-এর সব edge S-এর ভেতরে), আর Prim ঠিক এই cut-এর light edge নেয়। তাই একই invariant, একই যুক্তি। নিচের stepper সাভার (S) থেকে শুরু করে।

1tree = {S}2+ K3+ D4+ T5+ N6+ G, তারপর M
Cut (tree, বাকি) পার হওয়া edge
tree-তে
পার হয়
বাছাই
মোট
  1. শুরু S থেকে। Tree-তে শুধু সাভার। Cut ({S}, বাকি) পার হয় S–K 7, D–S 9, S–M 10, G–S 11; সবচেয়ে হালকা S–K 7. Prim-এর প্রথম edge গোটা graph-এর সবচেয়ে হালকা নয়, শুধু এই cut-এর সবচেয়ে হালকা; Kruskal এটি নিয়েছিল পঞ্চম edge হিসেবে।
  2. K যুক্ত। এখন tree {S, K}. পার হওয়া edge-এর তালিকায় নতুন এসেছে D–K 2, K–N 4, M–K 12; সবচেয়ে হালকা D–K 2. মোট 9.
  3. D যুক্ত। Tree {S, K, D}. D–S 9 এখন ভেতরের edge, তালিকা থেকে বাদ; নতুন D–T 3, D–N 6, D–G 8. বাছাই D–T 3, মোট 12.
  4. T যুক্ত। নতুন T–G 5. তালিকার সবচেয়ে হালকা K–N 4, মোট 16.
  5. N যুক্ত। D–N 6 এখন ভেতরের edge (দুই প্রান্তই tree-তে), তাই আর বিবেচনায় নেই। সবচেয়ে হালকা T–G 5, মোট 21.
  6. G, তারপর M. G যুক্ত হলে D–G 8 আর G–S 11 ভেতরে চলে যায়; বাকি পার হওয়া edge S–M 10 আর M–K 12, বাছাই S–M 10. মোট 31: Kruskal-এর একই ছয়টি edge, শুধু আলাদা ক্রমে। ওজন আলাদা বলে উপপাদ্য ৩.৩ অনুযায়ী এটি হওয়ারই কথা।

নিচের ব্লক দুই algorithm বাজারঘরের graph-এ চালায়, সব spanning tree-এর brute-force সর্বনিম্নের সঙ্গে মেলায়, তারপর এমন এলোমেলো graph-এ পরীক্ষা করে যেখানে সমান ওজন ইচ্ছা করে রাখা (ওজন 1 থেকে 5). সমান ওজনে edge-সেট আলাদা হতে পারে, তাই তুলনা মোট ওজনের।

import random
from itertools import combinations
def find(p, x):
    while p[x] != x: x = p[x]                     # walk up to the root
    return x
def kruskal(V, W):
    p = {v: v for v in V}; T = []
    for e in sorted(W, key=W.get):
        a, b = find(p, e[0]), find(p, e[1])
        if a != b: p[a] = b; T.append(e)
    return T
def prim(V, W, root):
    inside, T = {root}, []
    while len(inside) < len(V):
        e = min((e for e in W if (e[0] in inside) != (e[1] in inside)), key=W.get)
        T.append(e); inside |= set(e)
    return T
def brute_min(V, W):                               # lightest of ALL spanning trees
    return min(sum(W[e] for e in T) for T in combinations(W, len(V) - 1)
               if len(kruskal(V, dict.fromkeys(T, 0))) == len(V) - 1)
hub = {("D", "K"): 2, ("D", "T"): 3, ("K", "N"): 4, ("T", "G"): 5, ("D", "N"): 6, ("S", "K"): 7,
       ("D", "G"): 8, ("D", "S"): 9, ("S", "M"): 10, ("G", "S"): 11, ("M", "K"): 12}
V = sorted({x for e in hub for x in e})
for name, T in (("Kruskal", kruskal(V, hub)), ("Prim from S", prim(V, hub, "S"))):
    print(f"{name:11}", " ".join(a + b for a, b in T), "total", sum(hub[e] for e in T))
print("brute force minimum:", brute_min(V, hub))
random.seed(67); checked = 0
for trial in range(150):
    n = random.randint(2, 6); V = list(range(n))
    W = {e: random.randint(1, 5) for e in combinations(V, 2) if random.random() < 0.7}
    if len(kruskal(V, W)) < n - 1: continue        # skip disconnected graphs
    k, p = kruskal(V, W), prim(V, W, random.choice(V))
    assert sum(W[e] for e in k) == sum(W[e] for e in p) == brute_min(V, W)
    checked += 1
print(checked, "random connected graphs (ties allowed): Kruskal = Prim = brute force")

আসল আউটপুট (ব্রাউজারের Python):

Kruskal     DK DT KN TG SK SM total 31
Prim from S SK DK DT KN TG SM total 31
brute force minimum: 31
131 random connected graphs (ties allowed): Kruskal = Prim = brute force

প্রথম দুই লাইন দুই stepper-এর ক্রম হুবহু দেয়: একই ছয়টি edge, আলাদা ক্রম। brute_min সব C(11, 6) = 462টি ছয়-edge উপসেট দেখে, যার মধ্যে ১৪১টি spanning tree, আর সর্বনিম্ন 31. এলোমেলো ১৩১টি connected graph-এ তিনটি সংখ্যাই মিলেছে, সমান ওজন থাকা সত্ত্বেও। এই কোড ধীর (Prim প্রতিবার সব edge দেখে); heap আর union-find-এর দ্রুত রূপ DSA কোর্সের পর্ব ৪৫ আর পর্ব ৩৩-এ।

৫ · MST-এর কাজ — কেবল টানা আর দল ভাগ

নেটওয়ার্ক নকশা। সবচেয়ে সরাসরি প্রয়োগ §১-এর গল্প: হাব, গুদাম বা গ্রাম জুড়তে মোট সবচেয়ে কম কেবল, পাইপ বা রাস্তা। একটি সতর্কতা: MST শুধু “সবাই যুক্ত” নিশ্চিত করে। Tree-তে প্রতিটি edge একটি bridge, তাই একটি কেবল কাটলেই নেটওয়ার্ক ভাগ হয়ে যায়। বাস্তবে তাই অনেক সময় MST-এর সঙ্গে কয়েকটি বাড়তি পথ রাখা হয়, যা পর্ব ৫৯-এর edge connectivity-র প্রশ্ন। মাঝপথে নতুন সংযোগ-বিন্দু বসানোর Steiner tree সমস্যা আরও কঠিন (প্র ০৫)।

Bottleneck. MST-এর একটি কম-জানা ধর্ম: যেকোনো দুই vertex-এর মধ্যে MST-এর পথে সবচেয়ে ভারী edge যতটা সম্ভব হালকা। অর্থাৎ G-এর যেকোনো u–v পথের “সবচেয়ে ভারী edge”-কে ছোট করতে চাইলে MST-এর পথ সেরা। প্রমাণ cycle property-র মতো: MST-এর পথের সবচেয়ে ভারী edge e সরালে দুই টুকরো হয়, আর যেকোনো u–v পথ কোথাও এই cut পার হয়, এমন একটি edge g দিয়ে যার ওজন ≥ w(e); নইলে T − e + g একটি হালকা spanning tree হতো। তবে সাবধান: এটি মোট দৈর্ঘ্যে সবচেয়ে ছোট পথ নয় (§৮-এর ভুল প্রমাণ)।

দল ভাগ বা clustering. বাজারঘর হাবগুলোকে k = 3টি অঞ্চলে ভাগ করতে চায়, যাতে আলাদা অঞ্চলের হাবগুলো যতটা সম্ভব দূরে থাকে। দুই অঞ্চলের সবচেয়ে কাছের দুই হাবের দূরত্বকে ভাগটির “spacing” বলি; চাই সবচেয়ে বড় spacing. উত্তর: Kruskal চালান, কিন্তু kটি দল থাকতেই থামুন। সমতুল্যভাবে MST থেকে সবচেয়ে ভারী k − 1টি edge মুছুন। এর নাম single-linkage clustering. আমাদের graph-এ S–M 10 আর S–K 7 মুছলে তিন অঞ্চল {M}, {S}, {D, G, K, N, T}, আর spacing 7 (অঞ্চলগুলোর মধ্যের edge 7, 9, 10, 11, 12; scratch-এ মেলানো)।

  1. ধরুন Kruskal-এর ভাগ 𝒞-এর spacing d*: এটি Kruskal পরের যে edge নিত তার ওজন, অর্থাৎ মুছে ফেলা MST edge-গুলোর সবচেয়ে হালকাটি। অন্য যেকোনো k-ভাগ 𝒞′ নিন। — সংজ্ঞা
  2. 𝒞′ ≠ 𝒞 হলে এমন দুই vertex p, q আছে যারা 𝒞-এ এক দলে কিন্তু 𝒞′-এ আলাদা দলে (𝒞-এর প্রতিটি দল 𝒞′-এর কোনো একটি দলের ভেতরে থাকলে, দুই ভাগেই kটি দল বলে ভাগ দুটি এক হতো)। — pigeonhole-ধাঁচের যুক্তি
  3. 𝒞-এ p আর q-এর দলটি Kruskal-এর forest-এর একটি tree, যার প্রতিটি edge-এর ওজন ≤ d*. এই tree-তে p থেকে q-এর পথ কোথাও 𝒞′-এর দুই দলের সীমা পার হয়, তাই 𝒞′-এর spacing ≤ d*. ∎ — পথকে সীমা পার হতে হয়

৬ · Kirchhoff-এর matrix-tree theorem — spanning tree গোনা

এবার প্রশ্ন: কয়টি spanning tree আছে? Spanning tree বেশি মানে নেটওয়ার্কে বিকল্প সংযোগ বেশি। ছোট উদাহরণ হাতে করা যায়।

  • Cycle Cn: n vertex আর n edge, তাই spanning tree ঠিক একটি edge বাদ দেয়; যেকোনো একটি edge বাদ দিলে একটি path থাকে, যা tree. উত্তর n.
  • K2,3: এক দিকে a, b; অন্য দিকে x, y, z. Spanning tree-তে 5 − 1 = 4টি edge, আর প্রতিটি edge x, y, z-এর ঠিক একটিকে ছোঁয়। প্রত্যেকের degree অন্তত 1 (নইলে একা থাকে), আর তিনজনের degree-এর যোগফল 4, তাই ঠিক একজনের degree 2 (সে a আর b দুজনকেই জোড়ে), বাকি দুজন a বা b-এর একটিতে ঝোলে। দুজনের degree 2 হলে cycle a–x–b–y–a হতো, তাই অন্য কোনো বিকল্প নেই। উত্তর 3 · 2 · 2 = 12.

বড় graph-এ এভাবে গোনা কঠিন। Gustav Kirchhoff বিদ্যুৎ-বর্তনী নিয়ে কাজের সময় (১৮৪৭ সালের প্রবন্ধ বলে উল্লেখ করা হয়) একটি determinant-এর সূত্র পান। দরকার একটি matrix. Graph-এর Laplacian matrix L = D − A, যেখানে D হলো degree-গুলোর diagonal matrix আর A হলো adjacency matrix (পর্ব ৫৮)। অর্থাৎ Lii = deg(i), আর i ≠ j হলে Lij = −(i আর j-এর মধ্যে edge-সংখ্যা)। প্রতিটি সারির যোগফল 0.

উপপাদ্য ৬.১ (Kirchhoff-এর matrix-tree theorem, প্রমাণের রূপরেখা)

Loop-হীন যেকোনো multigraph G-এর জন্য, L থেকে যেকোনো একটি vertex i-এর সারি আর কলাম বাদ দিয়ে যে (n − 1) × (n − 1) matrix Li পাওয়া যায়, তার determinant = G-এর spanning tree-এর সংখ্যা τ(G). G disconnected হলে determinant 0.

রূপরেখা। প্রতিটি edge-কে একটি দিক দিন, আর n × m “incidence” matrix B বানান: edge e = (u → v)-এর কলামে u-এর সারিতে +1, v-এর সারিতে −1. তাহলে BBT = L (দিক যেভাবেই দেওয়া হোক)। B থেকে i-এর সারি বাদ দিয়ে Bi পেলে Li = BiBiT. Cauchy–Binet সূত্র বলে, det(BiBiT) = সব (n − 1)-edge উপসেট S-এর ওপর det(Bi[S])2-এর যোগফল। শেষ ধাপ: S একটি spanning tree হলে det(Bi[S]) = ±1 (tree-এর পাতা ধরে ধরে বিস্তার করে দেখানো যায়), আর না হলে 0 (cycle থাকলে কলামগুলো linearly dependent). তাই যোগফল ঠিক tree-এর সংখ্যা। পূর্ণ প্রমাণের জন্য বীজগাণিতিক graph theory-র যেকোনো পাঠ্যবই দেখুন; নিচে আমরা সূত্রটি হাতে আর কোডে যাচাই করি।

উদাহরণ হিসেবে একটি বর্গ 1–2–3–4–1 আর তার একটি কর্ণ 1–3 নিন (৫টি edge). Determinant বের করার জন্য Gaussian elimination: একটি সারির গুণিতক নিচের সারি থেকে বিয়োগ করে diagonal-এর নিচের সব সংখ্যা শূন্য করা, যাতে determinant বদলায় না, তারপর diagonal-এর গুণফল। ভগ্নাংশ এড়ানো যায় না, তাই ঠিক ভগ্নাংশ (Fraction) রাখি।

1G: 1–2, 2–3, 3–4, 4–1, 1–32L = D − A3vertex 4-এর সারি ও কলাম বাদ4কলাম ১-এর নিচে শূন্য5কলাম ২-এর নিচে শূন্য6det = diagonal-এর গুণফল7সরাসরি গুনে যাচাই
Matrix আর হিসাব
matrix
সারি-ক্রিয়া
ফল
  1. Graph আর degree. Edge 1–2, 2–3, 3–4, 4–1 আর কর্ণ 1–3. Degree: vertex 1 আর 3-এর 3 (কর্ণের জন্য), 2 আর 4-এর 2.
  2. Laplacian. Diagonal-এ degree, আর প্রতিটি edge ij-এর জন্য (i, j) আর (j, i) ঘরে −1. যেমন সারি 2: 2-এর প্রতিবেশী 1 আর 3, তাই (−1, 2, −1, 0). প্রতিটি সারির যোগফল 0, তাই L নিজের determinant সবসময় 0; সেজন্যই একটি সারি-কলাম বাদ দিতে হয়।
  3. Vertex 4 বাদ। চতুর্থ সারি আর চতুর্থ কলাম মুছে L4 = [3 −1 −1; −1 2 −1; −1 −1 3]. উপপাদ্য বলে যেকোনো vertex বাদ দিলেও একই উত্তর আসবে।
  4. কলাম ১। Pivot 3. সারি 2-এ সারি 1-এর 1/3 গুণ যোগ: (−1 + 1, 2 − 1/3, −1 − 1/3) = (0, 5/3, −4/3). সারি 3-এ একই: (0, −1 − 1/3, 3 − 1/3) = (0, −4/3, 8/3). এই সারি-ক্রিয়া determinant বদলায় না।
  5. কলাম ২। Pivot 5/3. সারি 3-এ সারি 2-এর 4/5 গুণ যোগ: −4/3 + (4/5)(5/3) = 0, আর 8/3 + (4/5)(−4/3) = 8/3 − 16/15 = 8/5. এখন matrix উপরের ত্রিভুজাকার।
  6. Determinant. ত্রিভুজাকার matrix-এর determinant diagonal-এর গুণফল: 3 · (5/3) · (8/5) = 8. সব ভগ্নাংশ কেটে গিয়ে একটি পূর্ণসংখ্যা, যেমন হওয়ার কথা।
  7. সরাসরি যাচাই। ৫টি edge থেকে ৩টি বাছার C(5, 3) = 10 উপায়; এর মধ্যে ত্রিভুজ {1–2, 2–3, 1–3} আর {1–3, 3–4, 4–1} cycle, বাকি ৮টি tree. আরেক পথ deletion–contraction: τ(G) = τ(G − e) + τ(G / e), যেখানে e = কর্ণ। G − e = C4, যাতে 4টি; G / e-তে 1 আর 3 একটি বিন্দু, যার সঙ্গে 2 আর 4 দুটি করে সমান্তরাল edge-এ যুক্ত, তাই 2 · 2 = 4. মোট 8.
অনুসিদ্ধান্ত ৬.২ (Cayley-র সূত্র)

Kn-এ (n ≥ 2) spanning tree-এর সংখ্যা nn−2. সমতুল্যভাবে, 1, …, n লেবেলের vertex-এ আলাদা tree nn−2টি।

  1. Kn-এ প্রতিটি degree n − 1, তাই একটি vertex বাদ দিলে L-এর (n − 1) × (n − 1) অংশে diagonal-এ n − 1, বাকি সব ঘরে −1. — Laplacian-এর সংজ্ঞা
  2. সব সারি প্রথম সারিতে যোগ করুন। প্রতিটি কলামের যোগফল (n − 1) + (n − 2)(−1) = 1, তাই প্রথম সারি হয় সব 1. Determinant বদলায় না। — সারি-ক্রিয়া
  3. এই নতুন প্রথম সারি বাকি প্রতিটি সারিতে যোগ করুন: diagonal-এ n − 1 + 1 = n, অন্য ঘরে −1 + 1 = 0, প্রথম কলামে −1 + 1 = 0. — সারি-ক্রিয়া
  4. Matrix এখন উপরের ত্রিভুজাকার, diagonal 1, n, n, …, n (n − 2 বার n). Determinant nn−2. ∎ — উপপাদ্য ৬.১

K4-এ 42 = 16. Arthur Cayley ১৮৮৯ সালে সূত্রটি প্রকাশ করেন বলে নামটি তাঁর, তবে এর আগে Borchardt ১৮৬০ সালে এটি পেয়েছিলেন বলে উল্লেখ করা হয়। Prüfer code দিয়ে একটি bijective প্রমাণ পর্ব ৬৫ “Tree — ধর্ম, rooted tree আর m-ary tree-এর হিসাব”-এ। একই পদ্ধতিতে Km,n-এ mn−1nm−1টি tree পাওয়া যায়; K2,3-এ 22 · 3 = 12, ওপরের হাতের গণনার সঙ্গে মিলল।

import random; random.seed(7)
from fractions import Fraction
from itertools import combinations
def det(M):                                        # exact Gaussian elimination
    M = [[Fraction(x) for x in row] for row in M]; n, d = len(M), Fraction(1)
    for c in range(n):
        piv = next((r for r in range(c, n) if M[r][c] != 0), None)
        if piv is None: return Fraction(0)
        if piv != c: M[c], M[piv] = M[piv], M[c]; d = -d
        d *= M[c][c]
        for r in range(c + 1, n):
            k = M[r][c] / M[c][c]
            M[r] = [a - k * b for a, b in zip(M[r], M[c])]
    return d
def kirchhoff(n, E):                               # Laplacian minus last row/col
    L = [[0] * n for _ in range(n)]
    for u, v in E:
        L[u][u] += 1; L[v][v] += 1; L[u][v] -= 1; L[v][u] -= 1
    return int(det([row[:-1] for row in L[:-1]]))
def is_tree(n, T):                                 # n - 1 edges, no cycle
    p = list(range(n))
    def f(x): return x if p[x] == x else f(p[x])
    for u, v in T:
        a, b = f(u), f(v)
        if a == b: return False
        p[a] = b
    return True
def K(n): return list(combinations(range(n), 2))   # complete graph K_n
print("K4:", kirchhoff(4, K(4)), " C4 + chord:", kirchhoff(4, [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]))
print("K_n, n = 2..7:", [kirchhoff(n, K(n)) for n in range(2, 8)],
      "n^(n-2):", [n ** (n - 2) for n in range(2, 8)])
for trial in range(200):
    n = random.randint(2, 6); E = [e for e in K(n) if random.random() < 0.6]
    assert kirchhoff(n, E) == sum(is_tree(n, T) for T in combinations(E, n - 1))
print("200 random graphs: determinant = brute-force count (0 if disconnected)")

আসল আউটপুট (ব্রাউজারের Python):

K4: 16  C4 + chord: 8
K_n, n = 2..7: [1, 3, 16, 125, 1296, 16807] n^(n-2): [1, 3, 16, 125, 1296, 16807]
200 random graphs: determinant = brute-force count (0 if disconnected)

K4-এ 16 আর কর্ণসহ বর্গে 8, stepper-এর মতোই (এখানে vertex 0–3). n = 2 থেকে 7 পর্যন্ত determinant আর nn−2 হুবহু এক। শেষে ২০০টি এলোমেলো graph-এ (disconnected সহ) determinant আর সব (n − 1)-edge উপসেট গুনে পাওয়া সংখ্যা মিলেছে। Fraction ব্যবহারের কারণ: float-এ elimination করলে 7.999999… ধরনের উত্তর আসতে পারে, আর তখন int() ভুল করে 7 বানাতে পারে।

৭ · MST দ্বিগুণ করে metric TSP — 2-approximation-এর পূর্ণ প্রমাণ

পর্ব ৬১-এ traveling salesperson problem (TSP) এসেছিল: সব শহর একবার ঘুরে শুরুতে ফেরার সবচেয়ে ছোট tour. সেখানে উপপাদ্য ৮.২-এর রূপরেখা ছিল; এখানে পূর্ণ প্রমাণ। Metric TSP মানে দূরত্ব D(a, b) ≥ 0, প্রতিসম, আর triangle inequality D(a, c) ≤ D(a, b) + D(b, c) মানে; প্রতিটি দুই শহরের মধ্যে সরাসরি পথ আছে (complete graph).

উপপাদ্য ৭.১ (MST-doubling)

Metric TSP-এ T একটি MST, OPT সবচেয়ে ছোট tour-এর দৈর্ঘ্য। T-কে যেকোনো root থেকে DFS করে vertex-গুলো প্রথম দেখার ক্রমে (preorder) সাজিয়ে শেষে root-এ ফিরলে যে tour H পাওয়া যায়, তার দৈর্ঘ্য ≤ 2 · w(T) ≤ 2 · OPT.

  1. নিম্নসীমা: সবচেয়ে ছোট tour থেকে যেকোনো একটি edge সরালে একটি Hamilton path থাকে, যা একটি spanning tree; তার ওজন ≤ OPT (সরানো edge ≥ 0). MST সব spanning tree-এর চেয়ে হালকা, তাই w(T) ≤ OPT. — MST-এর সংজ্ঞা
  2. দ্বিগুণ: T-এর প্রতিটি edge-এর দুটি কপি নিন। এই multigraph connected, আর প্রতিটি degree জোড় (T-এর degree-এর দ্বিগুণ), তাই পর্ব ৬০-এর উপপাদ্য অনুযায়ী এতে একটি Euler circuit W আছে, যা প্রতিটি কপি একবার ব্যবহার করে। দৈর্ঘ্য ঠিক 2 · w(T). DFS-এর “নামা আর ফেরা” ঠিক এমন একটি circuit, আর তাতে vertex-গুলো প্রথমবার আসে preorder ক্রমে। — Euler-এর শর্ত
  3. Shortcut lemma: যেকোনো walk x0, x1, …, xk-এর জন্য D(x0, xk) ≤ ∑ D(xi−1, xi). k-এর ওপর induction: k = 1 সরাসরি; k − 1 পর্যন্ত সত্য হলে D(x0, xk) ≤ D(x0, xk−1) + D(xk−1, xk), আর প্রথম পদে hypothesis. — triangle inequality, পর্ব ১০
  4. Shortcutting: W থেকে প্রতিটি vertex-এর প্রথম আবির্ভাব রেখে বাকি মুছে H পাই। H-এর প্রতিটি ধাপ u → u′ W-এর একটি অংশকে প্রতিস্থাপন করে, যা u থেকে u′-এ যায়; lemma অনুযায়ী ধাপটি সেই অংশের চেয়ে লম্বা নয়। অংশগুলো W-এর আলাদা টুকরো, তাই len(H) ≤ len(W) = 2 · w(T). — ধাপ ৩
  5. মিলিয়ে len(H) ≤ 2 · w(T) ≤ 2 · OPT. ∎

Metric শর্ত কেবল ধাপ ৩–৪-এ লেগেছে, কিন্তু সেটি অপরিহার্য: triangle inequality ছাড়া কোনো polynomial algorithm কোনো ধ্রুব গুণকের নিশ্চয়তা দিতে পারে না, যদি না P = NP (পর্ব ৬১-এ উল্লেখিত)। এমন নিশ্চয়তাকে approximation ratio 2 বলে। নিচের ব্লক ৭টি শহরের এলোমেলো সমতল বিন্দুতে (Euclidean দূরত্ব metric) সব 6! = 720টি ক্রম দেখে OPT বের করে, আর অনুপাত মাপে।

import random
from itertools import permutations
from math import dist
def tour_len(P, t): return sum(dist(P[a], P[b]) for a, b in zip(t, t[1:] + t[:1]))
def mst_prim(P):                                   # complete graph, Euclidean weights
    best = {v: (dist(P[0], P[v]), 0) for v in range(1, len(P))}
    children = {v: [] for v in range(len(P))}; total = 0
    while best:
        v = min(best, key=lambda x: best[x][0]); w, u = best.pop(v)
        children[u].append(v); total += w
        for x in best:
            if dist(P[v], P[x]) < best[x][0]: best[x] = (dist(P[v], P[x]), v)
    return children, total
def preorder(children, u=0):                       # doubled tree walk, shortcut
    out = [u]
    for c in children[u]: out += preorder(children, c)
    return out
random.seed(61); worst = 0
for trial in range(100):
    P = [(random.randint(0, 20), random.randint(0, 20)) for _ in range(7)]
    ch, wT = mst_prim(P); tour = preorder(ch)
    opt = min(tour_len(P, [0, *p]) for p in permutations(range(1, 7)))
    assert wT <= opt + 1e-9 and tour_len(P, tour) <= 2 * wT + 1e-9
    worst = max(worst, tour_len(P, tour) / opt)
print("100 random 7-city instances: w(T) <= OPT and tour <= 2 w(T)")
print("worst ratio tour/OPT =", round(worst, 3))

আসল আউটপুট (ব্রাউজারের Python):

100 random 7-city instances: w(T) <= OPT and tour <= 2 w(T)
worst ratio tour/OPT = 1.32

১০০টি উদাহরণের প্রতিটিতে প্রমাণের দুই অসমতা সত্য, আর সবচেয়ে খারাপ অনুপাত 1.32, অর্থাৎ বাস্তবে tour সাধারণত সেরার অনেক কাছে। preorder ফাংশন ঠিক প্রমাণের H. 2 গুণকটি এলোমেলো উদাহরণে দেখা না গেলেও এর কাছাকাছি অনুপাতের বিশেষ উদাহরণ বানানো যায় বলে জানা যায়; ভালো নিশ্চয়তার জন্য Christofides-এর পদ্ধতি দেখুন (প্র ০৪) আর DSA কোর্সের পর্ব ৬৬.

৮ · পরীক্ষার অঙ্ক আর একটি ভুল প্রমাণ

অঙ্ক ১: Kruskal হাতে

Vertex A, B, C, D, E; edge-ওজন AB 1, AC 2, BC 3, CD 4, BD 5, DE 6, CE 7. MST আর তার ওজন বের করুন, আর বাদ পড়া প্রতিটি edge-এর কারণ বলুন।

  1. AB 1 নিন; AC 2 নিন (C আলাদা দলে); BC 3 বাদ (cycle A–B–C, 3 সবচেয়ে ভারী)।
  2. CD 4 নিন; BD 5 বাদ (cycle B–A–C–D–B, যাতে 1, 2, 4, 5); DE 6 নিন। চারটি edge, n − 1 = 4, শেষ।
  3. MST = {AB, AC, CD, DE}, ওজন 1 + 2 + 4 + 6 = 13; CE 7 দেখার দরকারই হয়নি। সব ওজন আলাদা, তাই এটিই একমাত্র MST (scratch-এ ২১টি spanning tree গুনে মেলানো)।

অঙ্ক ২: Spanning tree গোনা

(ক) C5, (খ) K5, (গ) wheel W4 (একটি কেন্দ্র h, চারপাশে বর্গ 0–1–2–3–0, আর h প্রত্যেকের সঙ্গে যুক্ত)।

  1. (ক) 5: যেকোনো একটি edge বাদ। (খ) 53 = 125 (Cayley).
  2. (গ) h-এর সারি-কলাম বাদ দিলে বর্গের চারটি vertex থাকে, প্রত্যেকের degree 3: Lh = [3 −1 0 −1; −1 3 −1 0; 0 −1 3 −1; −1 0 −1 3]. Gaussian elimination-এ determinant 45.
  3. তিনটিই scratch-এ সব উপসেট গুনে আর determinant দিয়ে মেলানো।

প্রমাণের ভুল ধরুন: “MST-এর পথই সবচেয়ে ছোট পথ”

একজন লিখলেন: “T একটি MST, আর u, v দুটি vertex. ধরুন G-তে u থেকে v-এর পথ Q, T-এর পথ P-এর চেয়ে ছোট। তাহলে T থেকে P সরিয়ে Q বসালে মোট ওজন কমে, যা T-এর সর্বনিম্নতার বিরোধী। তাই MST-এ যেকোনো দুই vertex-এর পথই তাদের সবচেয়ে ছোট পথ।”

ভুল কোথায়: “P সরিয়ে Q বসালে” যা থাকে তা সাধারণত spanning tree নয়: P-এর edge সরালে অন্য vertex-এরা বিচ্ছিন্ন হতে পারে, আর Q যোগ করলে cycle তৈরি হতে পারে। Edge-বদলের lemma শুধু একটি edge বদলের অনুমতি দেয়, তাও fundamental cycle-এর ভেতরে। আর তুলনাও ভুল: MST মোট ওজন কমায়, কোনো একটি জোড়ার দূরত্ব নয়। পাল্টা-উদাহরণ: ত্রিভুজ x–y–z, ওজন xy 2, yz 2, xz 3. MST = {xy, yz}, ওজন 4 (বাকি দুটি tree-র ওজন 5). MST-এ x থেকে z-এর পথ 2 + 2 = 4, অথচ সরাসরি edge 3. সঠিক কথা §৫-এর bottleneck ধর্ম, আর সবচেয়ে ছোট পথের জন্য আলাদা tree লাগে (Dijkstra-র shortest-path tree, পর্ব ৬১)।

আরও গভীরে: Greedy পদ্ধতি MST-এ কেন কাজ করে, তার গভীর উত্তর matroid: একটি সেটের কিছু “স্বাধীন” উপসেটের কাঠামো, যেখানে graph-এর cycle-হীন edge-সেটগুলো একটি উদাহরণ। Rado আর Edmonds-এর কাজ বলে উল্লেখ করা ফল অনুযায়ী, প্রতিটি ওজনের জন্য greedy সর্বোত্তম স্বাধীন সেট দেয় ঠিক তখন, যখন কাঠামোটি matroid. Kirchhoff-এর সূত্রেরও একটি সুন্দর সম্প্রসারণ আছে: প্রতিটি edge-কে 1 ohm রোধ ধরলে edge uv-এর দুই প্রান্তের মধ্যের কার্যকর রোধ = uv ধারণ করে এমন spanning tree-এর ভগ্নাংশ।

পরীক্ষা ও ইন্টারভিউতে: বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন প্রায়ই আসে:

  • n vertex, m edge-এর connected graph থেকে spanning tree পেতে কয়টি edge বাদ? m − n + 1.
  • Cut property বলুন ও প্রমাণ করুন। Cut-এর light edge কোনো MST-তে আছে; T + e-এর cycle থেকে cut পার হওয়া আরেকটি edge f সরানো।
  • সব ওজন আলাদা হলে MST কয়টি? একটি; symmetric difference-এর সবচেয়ে হালকা edge দিয়ে প্রমাণ।
  • Kn, Cn আর K2,3-এ কয়টি spanning tree? nn−2, n আর 12.
  • MST দিয়ে TSP-এর গুণক কত, আর কোন শর্তে? 2, শুধু triangle inequality মানলে।

৯ · AI-এর যুগে — spanning tree-এর প্রমাণ আর হিসাব যাচাই

MST-এর প্রশ্নে AI সহকারী algorithm-এর ধাপ সাধারণত ঠিক বলে, কিন্তু প্রমাণে সূক্ষ্ম ফাঁক রাখে আর হাতের গণনায় ভুল করে। কয়েকটি নিয়মিত ভুল আর ধরার উপায়:

AI-এর ভুলকীভাবে ধরবেন
“Cut-এর light edge প্রতিটি MST-তে থাকে”, সমান ওজনের কথা বাদসব ওজন 1 এমন ত্রিভুজ: তিনটি MST, কোনো edge সব MST-তে নেই
Cut property-র প্রমাণে “P-এর যেকোনো edge” সরানো, cut পার হওয়া edge নয়প্রমাণের প্রতিটি ধাপে জিজ্ঞেস করুন: w(e) ≤ w(f) কেন সত্য?
MST-কে shortest-path tree ভাবাত্রিভুজ 2, 2, 3: MST-এর পথ 4, সরাসরি 3
Kirchhoff-এ পুরো L-এর determinant নেওয়া, বা সারি বাদ দিয়ে কলাম রেখে দেওয়াপুরো L-এর determinant সবসময় 0; ছোট graph-এ brute force-এ মেলান
Cayley-র সূত্র nn−1 বা n! বলা, বা MST-doubling-এর গুণক সব TSP-তে লাগানোK4-এ ১৬টি tree নিজে গুনুন; non-metric উদাহরণে অনুপাত পরীক্ষা করুন

AI-এর কাছে spanning tree বা MST-এর প্রমাণ বা হিসাব চাইলে এমন কাঠামো দিন, যাতে প্রতিটি ধাপ আলাদা করে যাচাই করা যায়:

নিচের spanning tree বা MST-এর দাবিটি ধাপে ধাপে প্রমাণ করো বা খণ্ডন করো: দাবি: [যেমন "প্রতিটি MST-তে graph-এর সবচেয়ে হালকা edge থাকে"] ১. শুরুতেই লেখো graph-এর ধরন (simple বা multigraph, connected কি না) আর ওজন সব আলাদা কি না। ২. Cut property, cycle property বা edge-বদল ব্যবহার করলে কোন cut বা কোন cycle নিচ্ছ, আর কেন সরানো edge-টি cut পার হয়, তা স্পষ্ট লেখো। ৩. প্রতিটি অসমতার (≤ বা <) পাশে তার কারণ লেখো, আর সমান ওজনে দাবিটি টেকে কি না আলাদা করে বলো। ৪. দাবি মিথ্যা হলে সবচেয়ে ছোট পাল্টা-উদাহরণ edge আর ওজনের তালিকা আকারে দাও। ৫. ৬টি vertex পর্যন্ত graph-এ দাবিটি সব spanning tree গুনে পরীক্ষা করার একটি Python কোড দাও (শুধু itertools আর fractions). ৬. কোনো নাম, সাল বা সূত্র নিয়ে নিশ্চিত না হলে তা স্পষ্ট বলো, বানিয়ে উৎস দিও না।

AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১MST-এর প্রথম algorithm কেন এসেছিল মোরাভিয়ার বিদ্যুৎ লাইন থেকে, আর Kruskal আর Prim-এর নামের পেছনে কাদের কাজ চাপা পড়ে আছে?

চেক গণিতবিদ Otakar Borůvka ১৯২৬ সালে MST সমস্যার প্রথম পরিচিত সমাধান প্রকাশ করেন। প্রসঙ্গ ছিল বাস্তব: মোরাভিয়া অঞ্চলে বিদ্যুৎ সরবরাহের নেটওয়ার্ক কীভাবে সবচেয়ে কম খরচে নকশা করা যায়, এমন একটি প্রশ্ন তাঁর কাছে এসেছিল বলে ইতিহাসে বর্ণনা পাওয়া যায়। তাঁর পদ্ধতি (আজ Borůvka-এর algorithm) রাউন্ডে রাউন্ডে চলে: প্রতিটি দল নিজের সবচেয়ে হালকা বাইরের edge নেয়, আর সব দল একসঙ্গে জোড়া লাগে। প্রতিটি রাউন্ডে দলের সংখ্যা অন্তত অর্ধেক হয়, তাই বড়জোর log2 nটি রাউন্ড। প্রতিটি পছন্দ আসলে cut property: দলটি নিজেই একটি cut.

১৯৩০ সালে আরেক চেক গণিতবিদ Vojtěch Jarník একটি ভিন্ন পদ্ধতি দেন: একটি vertex থেকে tree বাড়ানো। এটিই পরে Robert Prim ১৯৫৭ সালে Bell System Technical Journal-এ নতুন করে প্রকাশ করেন, আর Edsger Dijkstra ১৯৫৯ সালের তাঁর বিখ্যাত প্রবন্ধে আবারও বর্ণনা করেন। তাই অনেকে একে Jarník–Prim বা Prim–Jarník algorithm বলেন। Joseph Kruskal ১৯৫৬ সালে Proceedings of the American Mathematical Society-তে ছোট একটি প্রবন্ধে edge সাজিয়ে নেওয়ার পদ্ধতি দেন, আর সেখানে Borůvka-র কাজের উল্লেখ ছিল বলে জানা যায়।

এই ইতিহাস বিস্তারিত তুলে আনেন Ronald Graham আর Pavol Hell, ১৯৮৫ সালের একটি প্রবন্ধে। তাঁদের বর্ণনা থেকে বোঝা যায়, চেক ভাষায় প্রকাশিত কাজ দীর্ঘদিন পশ্চিমে প্রায় অজানা ছিল। আরেকটি শিক্ষা: Borůvka-র পদ্ধতি সমান্তরাল কম্পিউটারে চমৎকার, কারণ প্রতিটি দল একই সময়ে নিজের edge বাছতে পারে। দ্রুততম আধুনিক MST algorithm-গুলোও (প্র ০৩) Borůvka-র রাউন্ডকে একটি অংশ হিসেবে ব্যবহার করে।

তিন পদ্ধতির পেছনে একই গণিত: §৩-এর cut property. Kruskal সবচেয়ে হালকা edge দিয়ে দুটি দল জোড়ে, Prim একটি দলকে বাড়ায়, Borůvka সব দলকে একসঙ্গে বাড়ায়। সমান ওজন থাকলে Borůvka-তে একটু সতর্কতা লাগে (নইলে cycle হতে পারে), যা একটি নির্দিষ্ট tie-breaking নিয়মে মেটে।

মূল উপলব্ধি: MST-এর প্রথম সমাধান একটি বাস্তব বিদ্যুৎ-নেটওয়ার্কের প্রশ্ন থেকে আসে, আর তিনটি বিখ্যাত algorithm একই cut property-র তিন রকম প্রয়োগ; নামের ইতিহাস প্রায়ই আবিষ্কারের ইতিহাসের সঙ্গে মেলে না।

প্র ০২বিদ্যুৎ-বর্তনীর সমীকরণ সমাধান করতে গিয়ে Kirchhoff কীভাবে spanning tree গোনায় পৌঁছান?

Gustav Kirchhoff তরুণ বয়সেই বর্তনীর দুটি নিয়ম দেন, যা আজ স্কুলের পদার্থবিজ্ঞানে পড়ানো হয়: কোনো বিন্দুতে আসা আর যাওয়া প্রবাহের যোগফল সমান, আর কোনো বন্ধ পথে বিভব-পতনের যোগফল শূন্য। এরপর প্রশ্ন ওঠে: অনেকগুলো তার আর রোধের একটি জটিল নেটওয়ার্কে কয়টি স্বাধীন সমীকরণ লেখা যায়, আর প্রতিটি তারের প্রবাহ কীভাবে সরাসরি বের করা যায়?

১৮৪৭ সালের একটি প্রবন্ধে (বলে উল্লেখ করা হয়) Kirchhoff দেখান, বন্ধ পথের স্বাধীন সমীকরণের সংখ্যা m − n + 1, অর্থাৎ §১-এর circuit rank. প্রতিটি spanning tree বাছলে বাইরের প্রতিটি edge ঠিক একটি fundamental cycle দেয়, আর এই cycle-গুলোই একটি স্বাধীন সমীকরণ-ব্যবস্থা। প্রবাহের সমাধান লিখতে গিয়ে একটি determinant আসে, আর তিনি লক্ষ করেন যে সেটি spanning tree-গুলোর ওপর একটি যোগফল হিসেবে লেখা যায়। এখান থেকেই matrix-tree theorem-এর জন্ম, যদিও আধুনিক Laplacian আর Cauchy–Binet-এর ভাষায় প্রমাণ পরে গুছিয়ে লেখা হয়।

এই সংযোগ এখনো জীবন্ত। Laplacian matrix আজ graph theory-র একটি কেন্দ্রীয় বস্তু। তার eigenvalue-গুলো থেকেও spanning tree গোনা যায়: শূন্য বাদে সব eigenvalue-এর গুণফলকে n দিয়ে ভাগ করলে τ(G). দ্বিতীয় সবচেয়ে ছোট eigenvalue (algebraic connectivity, Fiedler-এর নামে) বলে graph কত শক্তভাবে connected. Machine learning-এর spectral clustering এই eigenvector দিয়েই ডেটা ভাগ করে।

বাজারঘরের দৃষ্টিতে: যদি প্রতিটি কেবল সমান সম্ভাবনায় বিকল হয়, তবে নেটওয়ার্ক connected থাকার সম্ভাবনার হিসাবে spanning tree-গুলো মৌলিক ভূমিকা রাখে, কারণ নেটওয়ার্ক connected থাকে ঠিক যখন টিকে থাকা কেবলগুলো অন্তত একটি spanning tree ধারণ করে। অবশ্য এই সম্ভাবনা সঠিকভাবে গোনা সাধারণভাবে কঠিন সমস্যা বলে জানা যায়।

মূল উপলব্ধি: Matrix-tree theorem জন্মেছিল বিদ্যুৎ-বর্তনীর সমীকরণ থেকে; circuit rank, fundamental cycle আর Laplacian আজও পদার্থবিজ্ঞান, নেটওয়ার্ক নির্ভরযোগ্যতা আর ডেটা বিশ্লেষণে একসঙ্গে কাজ করে।

প্র ০৩MST কত দ্রুত বের করা যায়? Linear সময়ের প্রশ্নটি এখনো খোলা কেন?

Kruskal-এ সাজানোর জন্য O(m log m) সময় লাগে; Prim binary heap-এ O(m log n), আর Fibonacci heap-এ (Fredman আর Tarjan, ১৯৮৭ বলে উল্লেখ করা হয়) O(m + n log n). ঘন graph-এ এটি প্রায় রৈখিক, কিন্তু পাতলা graph-এ log n-এর গুণকটি থেকে যায়। প্রশ্ন জাগে: শুধু ওজনের তুলনা দিয়ে কি O(m) সময়ে, অর্থাৎ ইনপুট পড়ার সময়ের মধ্যেই, MST পাওয়া সম্ভব?

আংশিক উত্তর এসেছে কয়েক ধাপে। David Karger, Philip Klein আর Robert Tarjan ১৯৯৫ সালে একটি randomized algorithm দেন, যার প্রত্যাশিত সময় O(m). তার চাবি cycle property: edge-এর একটি এলোমেলো অর্ধেক নিয়ে তার MST বের করা হয়, আর সেই forest-এর তুলনায় “ভারী” edge-গুলো (যারা কোনো cycle-এর সবচেয়ে ভারী) নিশ্চিন্তে বাদ দেওয়া যায়। সম্ভাবনার যুক্তিতে দেখানো যায় যে খুব বেশি edge বাকি থাকে না। সঙ্গে Borůvka-র রাউন্ড আর একটি linear-time “MST যাচাই” পদ্ধতি লাগে।

নির্ধারিত (deterministic) দিকে Bernard Chazelle ২০০০ সালে O(m α(m, n)) সময়ের algorithm দেন, যেখানে α হলো inverse Ackermann ফাংশন, যা বাস্তবের যেকোনো ইনপুটে 5-এর কম। ২০০২ সালে Seth Pettie আর Vijaya Ramachandran আরও অদ্ভুত একটি ফল দেন: একটি algorithm, যা প্রমাণিতভাবে সর্বোত্তম (কোনো comparison-ভিত্তিক algorithm এর চেয়ে asymptotically দ্রুত নয়), অথচ তার সময় ঠিক কত, তা কেউ জানে না। তাই deterministic linear-time MST আছে কি না, প্রশ্নটি এখনো খোলা বলে ধরা হয়।

এই গল্পের শিক্ষা হলো, cut আর cycle property শুধু সঠিকতার প্রমাণ নয়, দ্রুত algorithm-এর নকশারও উপাদান। আর “সর্বোত্তম কিন্তু সময় অজানা” ধরনের ফল দেখায় যে algorithm-এর জগতে সহজ প্রশ্নেরও কত গভীর উত্তর থাকতে পারে।

মূল উপলব্ধি: Randomized পদ্ধতিতে MST প্রত্যাশিত linear সময়ে পাওয়া যায়, deterministic-এ প্রায় linear; একটি প্রমাণিত-সর্বোত্তম algorithm আছে যার সময় অজানা, আর linear-time deterministic MST একটি খোলা প্রশ্ন।

প্র ০৪MST-doubling-এর গুণক 2 থেকে 1.5-এ নামাল কোন ধারণা, আর তারপর প্রায় চার দশক কেন আটকে ছিল?

§৭-এর পদ্ধতিতে অপচয় কোথায়? আমরা MST-এর প্রতিটি edge দুবার নিয়েছি শুধু সব degree জোড় করতে, যাতে Euler circuit পাওয়া যায়। কিন্তু MST-এ যে vertex-গুলোর degree ইতিমধ্যে জোড়, তাদের জন্য কিছু যোগ করার দরকারই নেই। সমস্যা শুধু বিজোড় degree-র vertex-গুলো, আর handshake theorem অনুযায়ী তাদের সংখ্যা জোড়।

Christofides-এর algorithm (Nicos Christofides, ১৯৭৬; Anatoliy Serdyukov স্বাধীনভাবে ১৯৭৮ সালে বলে উল্লেখ করা হয়) ঠিক এখানে কাজ করে: বিজোড় vertex-গুলোর মধ্যে সবচেয়ে কম ওজনের perfect matching খুঁজে MST-এর সঙ্গে যোগ করুন (পর্ব ৬৪-এর matching). এখন সব degree জোড়, Euler circuit নিন, তারপর আগের মতো shortcut. বিশ্লেষণ: সেরা tour-কে শুধু বিজোড় vertex-গুলোর ওপর shortcut করলে একটি ছোট cycle পাওয়া যায়, যা দুটি perfect matching-এ ভাগ হয়; তাদের ছোটটি ≤ OPT/2. তাই মোট ≤ OPT + OPT/2 = 1.5 · OPT.

এরপর বহু বছর কেউ metric TSP-এ 1.5-এর চেয়ে ভালো গুণক প্রমাণ করতে পারেননি। ২০২০ সালে Anna Karlin, Nathan Klein আর Shayan Oveis Gharan একটি randomized algorithm দেন, যার গুণক 1.5-এর চেয়ে সামান্য কম; উন্নতিটি অতি ক্ষুদ্র (প্রায় 10−36 মাপের বলে বর্ণিত), কিন্তু দীর্ঘদিনের বাধা ভাঙার জন্য তাৎপর্যপূর্ণ। এতে MST-এর জায়গায় একটি বিশেষ সম্ভাবনা-বণ্টন থেকে এলোমেলো spanning tree বাছা হয়, যার গণিত matrix-tree theorem-এর আত্মীয়।

অন্য দিকে, একটি নির্দিষ্ট ধ্রুবকের (প্রায় 1.008, Karpinski, Lampis আর Schmied-এর ২০১৫ সালের ফল বলে উল্লেখ করা হয়) চেয়ে ভালো গুণক পাওয়া NP-hard. তাই 1.008 আর প্রায় 1.5-এর মাঝখানে সত্যিকারের সীমা কোথায়, তা এখনো অজানা।

মূল উপলব্ধি: MST-এর দ্বিগুণ করা অপচয় কমাতে বিজোড় vertex-এ matching যোগ করে Christofides 1.5 পান; এই সীমা প্রায় চার দশক টিকেছিল, আর আজও সম্ভাব্য সেরা গুণক নিয়ে বড় ফাঁক রয়ে গেছে।

প্র ০৫কেবল টানার সময় মাঝপথে নতুন সংযোগ-বিন্দু বসালে খরচ কমে কেন, আর তখন সমস্যাটি এত কঠিন হয় কেন?

তিনটি হাব একটি সমবাহু ত্রিভুজের তিন কোণে, প্রতিটি বাহু 1 কিলোমিটার। MST দুটি বাহু নেয়, মোট 2. কিন্তু ত্রিভুজের কেন্দ্রে একটি জংশন বসিয়ে তিন হাবকে তার সঙ্গে জুড়লে মোট দৈর্ঘ্য 3 · (1/√3) = √3 ≈ 1.732, অর্থাৎ প্রায় ১৩% কম। এই বাড়তি বিন্দুকে Steiner point বলে, আর সব নির্দিষ্ট বিন্দু জোড়ার সবচেয়ে ছোট এমন নেটওয়ার্ক হলো Steiner tree. সমতলে সর্বোত্তম Steiner tree-তে প্রতিটি বাড়তি বিন্দুতে তিনটি রেখা ১২০° কোণে মেলে, ঠিক এই উদাহরণের মতো।

সমস্যা হলো, বাড়তি বিন্দু কোথায় কয়টি বসবে, তার অসংখ্য বিকল্প। Graph-এর রূপে (কিছু “terminal” vertex জুড়তে হবে, বাকি vertex ইচ্ছামতো ব্যবহার করা যায়) Steiner tree সমস্যা Richard Karp-এর ১৯৭২ সালের NP-complete তালিকায় আছে বলে জানা যায়, আর সমতলের Euclidean রূপও NP-hard বলে প্রমাণিত (Garey, Graham আর Johnson-এর ১৯৭৭ সালের কাজ বলে উল্লেখ করা হয়)। অথচ MST polynomial সময়ে আসে। একটি ছোট নিয়ম বদলে সহজ সমস্যা কঠিন হয়ে যাওয়ার এটি একটি সুন্দর উদাহরণ।

তবু MST এখানে কাজে লাগে। Metric graph রূপে terminal-গুলোর মধ্যে সবচেয়ে ছোট দূরত্ব দিয়ে MST নিলে, Steiner tree-এর সর্বোত্তমের দ্বিগুণের মধ্যে থাকে; যুক্তি প্রায় §৭-এর মতো (Steiner tree দ্বিগুণ করে Euler circuit, তারপর shortcut). সমতলে অনুপাত আরও ভালো: Edgar Gilbert আর Henry Pollak ১৯৬৮ সালে অনুমান করেন যে Steiner tree আর MST-এর দৈর্ঘ্যের অনুপাত সবসময় অন্তত √3/2 ≈ 0.866, যা ওপরের ত্রিভুজে ঠিক পূর্ণ হয়। ১৯৯০ সালে Ding-Zhu Du আর Frank Hwang একটি প্রমাণ ঘোষণা করেন, কিন্তু পরে সেই প্রমাণে ফাঁক আছে বলে কয়েকজন গবেষক যুক্তি দেন; তাই অনুমানটির অবস্থা নিয়ে সতর্ক থাকা ভালো।

মূল উপলব্ধি: বাড়তি জংশন বসানোর স্বাধীনতা খরচ কমায় কিন্তু সমস্যাকে NP-hard করে; MST তখনো একটি নিশ্চিত-মানের সহজ সমাধান দেয়, যা সর্বোত্তমের দ্বিগুণের মধ্যে থাকে।

অনুশীলন

আগে খাতায় করুন, তারপর উত্তর খুলে মেলান।

  1. প্রমাণ: Connected graph-এর সব edge-ওজন আলাদা। প্রমাণ করুন, প্রতিটি vertex v-এর সঙ্গে লাগানো edge-গুলোর মধ্যে সবচেয়ে হালকাটি MST-তে আছে। এ থেকে দেখান যে MST-তে অন্তত n/2টি এমন edge থাকে (n ≥ 2).
    1. Cut ({v}, V − {v}) নিন। এই cut পার হওয়া edge ঠিক v-এর সঙ্গে লাগানো edge-গুলো (n ≥ 2 আর graph connected, তাই অন্তত একটি আছে)। — cut-এর সংজ্ঞা
    2. ওজন আলাদা, তাই সবচেয়ে হালকা ev একাই সবচেয়ে হালকা crossing edge; উপপাদ্য ৩.১-এর দ্বিতীয় অংশ (A = ∅) অনুযায়ী ev প্রতিটি MST-তে, আর MST অনন্য। — cut property
    3. প্রতিটি vertex একটি ev বাছে, আর একটি edge-কে বড়জোর তার দুই প্রান্ত বাছতে পারে। তাই আলাদা ev-এর সংখ্যা ≥ n/2. ∎ — গণনা

    এটিই Borůvka-র প্রথম রাউন্ড: দলের সংখ্যা n থেকে বড়জোর n/2-এ নামে। scratch-এ এলোমেলো আলাদা-ওজনের graph-এ দাবিটি brute force-এ মেলানো।

  2. গণনা: কয়টি spanning tree আছে (ক) C6-এ, (খ) K2,4-এ, (গ) K6-এ, (ঘ) “bowtie” graph-এ, অর্থাৎ একটি সাধারণ vertex-এ জোড়া দুটি ত্রিভুজ?

    (ক) 6: যেকোনো একটি edge বাদ। (খ) 32: K2,3-এর যুক্তিতে ডান দিকের ৪টি vertex-এর degree-যোগফল 5, তাই ঠিক একজন দুই দিকেই জোড়ে (৪ উপায়), বাকি তিনজন a বা b-এ (23 = 8 উপায়), মোট 32; সূত্র mn−1nm−1 = 23 · 4 = 32. (গ) 64 = 1296 (Cayley). (ঘ) 9: দুটি ত্রিভুজ শুধু একটি vertex ভাগ করে, তাই প্রতিটি থেকে আলাদাভাবে একটি edge বাদ: 3 · 3. চারটিই scratch-এ সব উপসেট গুনে মেলানো।

  3. আউটপুট অনুমান: কোড না চালিয়ে বলুন কী ছাপা হবে। তারপর যেকোনো Python-এ চালিয়ে মেলান।
    E = [(4, 'a', 'b'), (1, 'b', 'c'), (3, 'a', 'c'), (2, 'c', 'd'), (5, 'b', 'd')]
    comp = {v: v for v in 'abcd'}
    T, total = [], 0
    for w, u, v in sorted(E):
        if comp[u] != comp[v]:
            old = comp[v]
            for x in comp:
                if comp[x] == old: comp[x] = comp[u]
            T.append(u + v); total += w
    print(T, total)
    ['bc', 'cd', 'ac'] 6

    এটি Kruskal, যেখানে comp প্রতিটি vertex-এর দলের নাম রাখে। sorted ওজনের ক্রমে সাজায়: (1, b, c) নেয়, c-এর দল b হয়; (2, c, d): d-এর দল b হয়; (3, a, c): a আর b আলাদা, তাই নেয়, আর b-দলের সবাই a-দলে যায়; (4, a, b) আর (5, b, d) একই দলে, বাদ। মোট 1 + 2 + 3 = 6. লক্ষ করুন, তালিকায় প্রথমে থাকা (4, a, b) বাদ পড়েছে: ক্রম ঠিক করে sorted, ইনপুটের ক্রম নয় (CPython 3.14-এ চালিয়ে মেলানো)।

  4. প্রমাণের ভুল ধরুন: দাবি: “কোনো graph-এর MST অনন্য হলে তার সব edge-ওজন আলাদা।” প্রমাণ: “ধরুন দুটি edge e আর f-এর ওজন সমান। T যদি e ধারণ করে, তবে e-এর জায়গায় f বসালেও ওজন একই থাকে, তাই আরেকটি MST পাওয়া যায়। অতএব অনন্যতা থাকলে কোনো দুই ওজন সমান নয়। ∎” ভুল কোথায়? একটি পাল্টা-উদাহরণ দিন।
    • পাল্টা-উদাহরণ: Path a–b–c, দুটি edge-এরই ওজন 1. Graph নিজেই একটি tree, তাই spanning tree একটিই, আর তা MST; অথচ ওজন সমান। আরেকটি: বর্গ a–b–c–d–a, ওজন 1, 1, 1, 5; MST একটিই (5 বাদ), যদিও তিনটি ওজন সমান।
    • ভুল ধাপ: “e-এর জায়গায় f বসালে” পাওয়া সেট spanning tree হবে, এর কোনো নিশ্চয়তা নেই। Lemma ১.৩ অনুযায়ী বদল বৈধ শুধু যখন f ∉ T আর e আছে f-এর fundamental cycle-এ। f যদি আগেই T-তে থাকে বা e-এর সঙ্গে কোনো cycle-এ না পড়ে, তবে বদলে tree ভাঙে।
    • সঠিক কথা: উপপাদ্য ৩.৩-এর একমুখী রূপ: ওজন আলাদা ⇒ MST অনন্য। উল্টোটি সাধারণভাবে মিথ্যা।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রম“Cut-এর light edge e কোনো একটি MST-তে আছে” প্রমাণের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. একটি MST T নিন; e ∈ T হলে কাজ শেষ, তাই ধরুন e ∉ T
  2. T-এ e-এর দুই প্রান্তের মধ্যের পথ P নিন
  3. P এক ভাগ থেকে অন্য ভাগে যায়, তাই P-এর একটি edge f cut পার হয়
  4. T + e − f একটি spanning tree, আর w(e) ≤ w(f)
  5. তাই এর ওজন ≤ w(T), অর্থাৎ এটিও MST, আর তাতে e আছে

৫টি লেবেলযুক্ত vertex-এর complete graph K5-এ কয়টি spanning tree আছে?

  • 120
  • 125
  • 210
  • 3125

১০টি vertex আর ১৫টি edge-এর একটি connected graph থেকে একটি spanning tree রাখতে ঠিক কয়টি edge বাদ দিতে হবে?

  • 5
  • 9
  • 6
  • tree-ভেদে আলাদা

একটি undirected connected graph-এর DFS tree-এর বাইরের প্রতিটি edge সম্পর্কে কোনটি সবসময় সত্য?

  • এটি একটি পূর্বপুরুষ আর তার বংশধরকে জোড়ে
  • এটি দুটি আলাদা শাখার vertex জোড়ে
  • এটি একই বা পাশের স্তরের দুই vertex জোড়ে
  • এটি সবসময় একটি bridge

একটি cycle-এর edge-ওজন 3, 5, 9, 4; 9 ওজনের edge-টি সম্পর্কে কোনটি নিশ্চিত?

  • এটি প্রতিটি MST-তে আছে
  • graph-এর বাকি অংশ না জেনে কিছু বলা যায় না
  • এটি কোনো একটি MST-তে আছে, তবে সব MST-তে নয়
  • এটি কোনো MST-তে নেই

৪টি vertex-এর বর্গ 1–2–3–4–1 আর কর্ণ 1–3 নিয়ে graph-টির Laplacian থেকে vertex 4-এর সারি-কলাম বাদ দিলে [3 −1 −1; −1 2 −1; −1 −1 3]. Graph-টিতে কয়টি spanning tree?

  • 10
  • 8
  • 4
  • 0

MST দ্বিগুণ করে shortcut নেওয়া TSP tour-এর দৈর্ঘ্য সেরা tour-এর দ্বিগুণের মধ্যে থাকার নিশ্চয়তা কোন শর্তে প্রমাণিত?

  • শহরগুলোর graph planar হলে
  • সব দূরত্ব আলাদা হলে
  • দূরত্ব triangle inequality মানলে (metric TSP)
  • শহর ১০টির কম হলে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. অস্তিত্ব আর গণনা: spanning tree আছে ⇔ graph connected; প্রতিটিতে n − 1টি edge, তাই ঠিক m − n + 1টি edge বাদ পড়ে।
  2. খোঁজার tree: BFS tree-তে root থেকে পথ সবচেয়ে ছোট আর edge পাশাপাশি স্তর জোড়ে; DFS-এর বাইরের edge পূর্বপুরুষ–বংশধর জোড়ে।
  3. Cut আর cycle: cut-এর light edge কোনো MST-তে আছে; cycle-এর একাই সবচেয়ে ভারী edge কোনো MST-তে নেই; ওজন আলাদা হলে MST অনন্য।
  4. Kruskal আর Prim: দুটিই প্রতিটি ধাপে একটি cut-এর light edge নেয়; edge-বদলের lemma সব প্রমাণের যন্ত্র।
  5. Kirchhoff আর TSP: τ(G) = Laplacian থেকে একটি সারি-কলাম বাদের determinant, Kn-এ nn−2; MST দ্বিগুণ করে metric TSP-এ ≤ 2 · OPT.

পরের পর্বে: পর্ব ৬৮ “Prefix code আর Huffman coding”: binary tree-এর পাতায় codeword বসিয়ে prefix code, Kraft-এর অসমতা, আর Huffman-এর greedy পদ্ধতি কেন সবচেয়ে ছোট গড় দৈর্ঘ্য দেয় তার প্রমাণ আসবে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান