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

Tree-এর কৌশল — LCA, binary lifting আর diameter

Tree Techniques — Rooting a Tree, Lowest Common Ancestor, Binary Lifting, Euler Tour + RMQ, Subtree Queries & Tree Diameter

Taught in Bangla, this lesson covers tree techniques: rooting a tree from an edge list with BFS parents and depths, naive LCA, the binary lifting table and LCA by equalising depths then jumping together, weighted distances and path minimums, Euler tour plus sparse table and offline LCA, subtree queries with entry and exit times, and tree diameter by two BFS runs, with C++ and Python code.

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

  • Edge-তালিকা থেকে rooted tree বানানো, আর বংশতালিকা, অফিসের ক্রমবিন্যাস, ফোল্ডার আর নেটওয়ার্কে LCA প্রশ্ন চেনা
  • Binary lifting-এর টেবিল হাতে বানানো, আর তা দিয়ে LCA, k-তম পূর্বপুরুষ ও দূরত্ব O(log n)-এ বের করা — শুদ্ধতার প্রমাণসহ
  • পথের ওপর সর্বোচ্চ বা সর্বনিম্ন edge-এর প্রশ্নে টেবিলে বাড়তি তথ্য জমিয়ে রাখা
  • Euler tour + sparse table-এ O(1) LCA, Tarjan-এর offline LCA, আর entry/exit time দিয়ে subtree query-কে রেঞ্জ query-তে বদলানো
  • দুইবার BFS দিয়ে tree diameter বের করা আর কেন তা ঠিক, প্রমাণ করা; rerooting, centroid আর heavy-light decomposition-এর নাম ও কাজ চেনা

১ · সমস্যা — বংশতালিকায় “সবচেয়ে কাছের সাধারণ পূর্বপুরুষ”

ঈদের দাওয়াতে দুই অপরিচিত তরুণের দেখা। আলাপে জানা গেল দুজনেই একই বড় পরিবারের — কিন্তু সম্পর্কটা ঠিক কী? চাচাতো ভাই, নাকি দাদার ভাইয়ের নাতি? উত্তর বের করার উপায় একটিই: দুজনের বংশধারা ধরে ওপরে উঠতে থাকুন, যতক্ষণ না দুই ধারা প্রথমবার একই মানুষে মেলে। সেই মানুষটিই দুজনের সবচেয়ে কাছের সাধারণ পূর্বপুরুষ. তিনি দুজনের দাদা হলে তাঁরা চাচাতো/ফুপাতো ভাই; পরদাদা হলে সম্পর্ক আরও দূরের।

একটি অফিসেও একই প্রশ্ন ওঠে। একটি বড় কোম্পানির ক্রমবিন্যাসে (পর্ব ২৬) দুজন কর্মীর মধ্যে বিরোধ হলে কোন কর্মকর্তা সিদ্ধান্ত দেবেন? যিনি দুজনেরই ঊর্ধ্বতন, আর এমনদের মধ্যে সবচেয়ে নিচের পদে — কারণ তাঁর চেয়ে ওপরে যাওয়ার দরকার নেই। কম্পিউটারে ফোল্ডারের ক্ষেত্রেও: D:\office\hr\2026 থেকে D:\office\accounts-এ যেতে প্রথমে দুই ঠিকানার শেষ সাধারণ ফোল্ডার office পর্যন্ত উঠতে হয়, তারপর নামতে হয়।

Tree-র ভাষায় (পর্ব ২৬): একটি rooted tree-তে দুটি node u আর v-এর lowest common ancestor (LCA) হলো সেই node, যে দুজনেরই ancestor (একজন নিজেও হতে পারে) আর এমন node-দের মধ্যে root থেকে সবচেয়ে দূরে, অর্থাৎ সবচেয়ে গভীর। পর্ব ২৬-এ binary tree-তে প্রতি প্রশ্নে O(n)-এর একটি recursive সমাধান দেখেছিলেন। কিন্তু প্রশ্ন যদি হয় ২ লাখ node-এর tree-তে ২ লাখ প্রশ্ন? এই পর্বের মূল প্রশ্ন এটিই — একবার প্রস্তুতি নিয়ে প্রতিটি প্রশ্নের দ্রুত উত্তর। পুরো পর্ব জুড়ে নিচের tree-টি আমাদের সঙ্গী:

            1             depth 0
          /   \
         2     3          depth 1
        / \    │
       4   5   6          depth 2
      / \      │
     7   8     9          depth 3
     │         │
    10        11          depth 4
     │         │
    12        13          depth 5
এই পর্বের প্রধান উদাহরণ: ১৩টি node, ১২টি edge, root 1. যেমন LCA(7, 8) = 4, LCA(12, 5) = 2, LCA(12, 13) = 1, আর LCA(9, 13) = 9 — কারণ 9 নিজেই 13-এর পূর্বপুরুষ।

LCA জানলে আরও অনেক প্রশ্নের উত্তর বের হয়: দুই node-এর দূরত্ব, তাদের মাঝের পথে সবচেয়ে ধীর লিংক, একটি node আরেকটির পূর্বপুরুষ কি না। আর এই পর্বের শেষ অংশে দেখবেন আরেকটি ক্লাসিক প্রশ্ন — tree diameter, অর্থাৎ tree-র সবচেয়ে লম্বা পথ, যা পর্ব ২৬-এ binary tree-তে দেখেছিলেন; এখানে যেকোনো tree-তে, দুইবার BFS দিয়ে।

২ · Edge-তালিকা থেকে rooted tree — BFS দিয়ে parent আর depth

কনটেস্টে বা বাস্তব ডেটায় tree প্রায় কখনো “এর বাঁ সন্তান, ওর ডান সন্তান” আকারে আসে না। আসে এভাবে: “n টি শহর, n − 1 টি রাস্তা; প্রতিটি লাইনে দুটি শহর a b, যাদের মাঝে রাস্তা আছে।” রাস্তাগুলোর কোনো দিক নেই — এটি একটি undirected graph, যা connected আর চক্রহীন, অর্থাৎ একটি tree. এখানে কে parent আর কে সন্তান, তা ঠিক করে দেওয়া নেই। আমাদের কাজ একটি node-কে root বেছে tree-টিকে “ঝুলিয়ে” দেওয়া — তখন সেটি হয় একটি rooted tree.

প্রথমে প্রতিটি node-এর প্রতিবেশীদের একটি তালিকা বানান — adjacency list (পর্ব ৩৯-এ বিস্তারিত)। প্রতিটি edge (a, b)-র জন্য a-এর তালিকায় b আর b-এর তালিকায় a. প্রধান উদাহরণের edge-তালিকা আর তা থেকে পাওয়া কয়েকটি তালিকা:

edges: 1-2 1-3 2-4 2-5 3-6 4-7 4-8 6-9 7-10 9-11 10-12 11-13
adj[1] = 2 3      adj[2] = 1 4 5      adj[4] = 2 7 8      adj[12] = 10

তারপর root (এখানে 1) থেকে একটি BFS (পর্ব ৪০) চালান। যে node থেকে একটি নতুন node প্রথম আবিষ্কৃত হলো, সে-ই তার parent, আর নতুনটির depth = parent-এর depth + 1. আগে দেখা প্রতিবেশী (যেমন parent নিজে) বাদ — তাই দিকহীন edge-ও দুবার গোনা হয় না। নিচের ডায়াগ্রামে দেখুন queue কীভাবে স্তরে স্তরে tree খুলে দেয়:

প্রধান উদাহরণ (root 1)সবুজ = এই ধাপে আবিষ্কৃত

            
প্রতিটি ধাপের পর
এই ধাপের কাজ
queue (সামনে → পেছনে)
নতুন parent, depth
  1. শুরু. root 1-কে queue-তে রাখলাম, depth[1] = 0. root-এর parent নেই; সুবিধার জন্য par[1] = 1 ধরছি — root-এর “parent” সে নিজেই। অংশ ৪-এ দেখবেন, এই ছোট সিদ্ধান্তে কোড অনেক সরল হয়।
  2. Level 1. 1-কে বের করে তার তালিকা adj[1] = 2 3 দেখলাম। দুজনেই নতুন: par = 1, depth = 1, queue-র পেছনে ঢুকল।
  3. দিকহীন edge-এর ফাঁদ. 2-এর তালিকা 1 4 5. কিন্তু 1 আগেই দেখা — সে 2-এর parent, সন্তান নয়। এই “আগে দেখা কি না” পরীক্ষাই দিকহীন edge থেকে দিক তৈরি করে। নতুন শুধু 4 আর 5.
  4. 3. 3-এর তালিকা 1 6; নতুন শুধু 6: par[6] = 3, depth 2. queue-তে এখন পুরো level 2: 4 5 6.
  5. 4. 4-এর তালিকা 2 7 8; নতুন 7 আর 8, depth 3.
  6. Leaf আর 6. 5-এর তালিকায় শুধু 2 — সে leaf, নতুন কেউ নেই। 6 থেকে নতুন 9.
  7. Level 4. 7 থেকে 10, 8 leaf, 9 থেকে 11. সবার depth 4.
  8. শেষ. 10 থেকে 12, 11 থেকে 13; শেষে 12 আর 13 leaf. queue খালি — সব ১৩টি node-এর parent আর depth জানা। প্রতিটি node একবার ঢুকেছে আর বের হয়েছে, প্রতিটি তালিকা একবার পড়া হয়েছে: মোট O(n + (n − 1)) = O(n).

BFS-এর বদলে DFS (পর্ব ৪১) দিয়েও একই কাজ হয়: dfs(v, p)-এ প্রতিটি প্রতিবেশী u ≠ p-এর জন্য par[u] = v, depth[u] = depth[v] + 1, তারপর dfs(u, v). DFS-এর বাড়তি লাভ হলো ঢোকা-বেরোনোর সময় (অংশ ৮) একই হাঁটায় পাওয়া যায়। কিন্তু সাবধান: ২ লাখ node-এর শিকলের মতো tree-তে recursive DFS-এর recursion depth ২ লাখ — Python-এ নিশ্চিত RecursionError, C++-এ stack overflow-এর ঝুঁকি (পর্ব ০৭, ২৬). BFS নিজের queue ব্যবহার করে বলে এই ভয় নেই — এজন্যই এই পর্বের কোডে BFS.

৩ · সরল LCA — এক ধাপ এক ধাপ করে ওপরে ওঠা

parent আর depth হাতে থাকলে বংশতালিকার পদ্ধতিটি সরাসরি কোডে আসে, দুই ধাপে:

  1. সমান গভীরতায় আনা. যে বেশি গভীরে, তাকে এক এক ধাপ ওপরে তুলুন (u = par[u]), যতক্ষণ না দুজনের depth সমান হয়।
  2. একসাথে ওঠা. এখন দুজন একই level-এ। যতক্ষণ u ≠ v, দুজনকেই একসাথে এক ধাপ ওপরে তুলুন। যেখানে প্রথম মেলে, সেটিই LCA.

উদাহরণ LCA(12, 8): depth 5 আর 3. 12 দুই ধাপ উঠে 12 → 10 → 7. এখন 7 আর 8, দুজনেই depth 3, কিন্তু আলাদা। একসাথে এক ধাপ: 7 → 4, 8 → 4 — মিলে গেল, LCA = 4. প্রথম ধাপটি জরুরি কেন? সমান গভীরতায় না এনে একসাথে উঠলে 12 আর 8 কখনো একই সময়ে 4-এ পৌঁছাবে না — 8 পৌঁছাবে এক ধাপে, 12 তিন ধাপে।

কেন ঠিক. দ্বিতীয় ধাপের শুরুতে u আর v একই depth-এ। দুজনের পূর্বপুরুষদের মধ্যে একই depth-এ বড়জোর একজন করে থাকে, তাই দুজনকে সমান হারে তুললে তারা সব সময় একই depth-এ থাকে, আর প্রথম যে depth-এ দুজনের পূর্বপুরুষ একই node, সেটিই সবচেয়ে গভীর সাধারণ পূর্বপুরুষ। একবার মিললে তার ওপরে সবাই সাধারণ — কিন্তু তারা কম গভীর।

খরচ. প্রতি প্রশ্নে বড়জোর depth[u] + depth[v] ধাপ, অর্থাৎ O(h), যেখানে h হলো tree-র উচ্চতা। ভারসাম্যপূর্ণ tree-তে h ≈ log n — চমৎকার। কিন্তু বাস্তব আর কনটেস্টের tree প্রায়ই লম্বা শিকলের মতো: h = n − 1. তখন q টি প্রশ্নে O(n·q). n = q = 2·105 হলে প্রায় 4·1010 ধাপ — কয়েক মিনিট, অথচ সময়সীমা সাধারণত ১–২ সেকেন্ড। এই সরল পদ্ধতি তবু ফেলনা নয়: অংশ ৫-এর প্রোগ্রামে এটিই দ্রুত পদ্ধতির উত্তর যাচাইয়ের brute force.

৪ · Binary lifting-এর টেবিল — up[k][v] = 2k-তম পূর্বপুরুষ

উপমা — সিঁড়ি বনাম লিফট. ৩০ তলা ভবনের ২৯ তলা থেকে নিচতলায় নামতে সিঁড়িতে ২৯টি ধাপ-তলা পেরোতে হয়। এখন ধরুন একটি অদ্ভুত লিফট আছে, যার বোতাম শুধু “১ তলা নিচে”, “২ তলা নিচে”, “৪ তলা নিচে”, “৮ তলা”, “১৬ তলা”. ২৯ = ১৬ + ৮ + ৪ + ১ — মাত্র চারবার চাপলেই হয়ে যায়। যেকোনো সংখ্যাকে দুইয়ের ঘাতের যোগফলে লেখা যায় (তার binary রূপ), তাই যেকোনো দূরত্ব log₂ সংখ্যক লাফে পার হওয়া যায়। এটিই binary lifting-এর পুরো রহস্য।

প্রতিটি node v-এর জন্য আগে থেকে জমিয়ে রাখুন: up[k][v] = v থেকে 2k ধাপ ওপরের পূর্বপুরুষ. up[0][v] হলো parent, up[1][v] দাদা (২ ধাপ), up[2][v] ৪ ধাপ ওপরের পূর্বপুরুষ, ইত্যাদি। পুরো টেবিল বানানোর চাবি একটি পর্যবেক্ষণ: 2k ধাপ = প্রথমে 2k−1 ধাপ, তারপর আবার 2k−1 ধাপ। তাই

up[k][v] = up[k − 1][ up[k − 1][v] ]

পর্ব ৩৭-এর sparse table-এর সাথে মিলটা লক্ষ করুন: সেখানে দৈর্ঘ্য 2k-এর ব্লকের উত্তর বানানো হতো দৈর্ঘ্য 2k−1-এর দুটি ব্লক জুড়ে; এখানে 2k ধাপের লাফ বানানো হয় 2k−1-এর দুটি লাফ জুড়ে। একই “দ্বিগুণ করা” (doubling) কৌশল। root-এর ওপরে লাফ দিলে? আমরা par[root] = root রেখেছি, তাই অতিরিক্ত লাফ root-এ আটকে যায় — কোনো সীমানা-পরীক্ষা লাগে না।

LOG কত সারি? দরকার এমন LOG, যাতে 2LOG যেকোনো depth-এর চেয়ে বড় হয়; depth বড়জোর n − 1, তাই 2LOG > n যথেষ্ট — n = 13-এর জন্য LOG = 4 (24 = 16), n = 2·105-এর জন্য 18. প্রধান উদাহরণে টেবিলটি সারি ধরে বানানো দেখুন:

up[k][v] — v-এর 2k-তম পূর্বপুরুষসবুজ = এই ধাপে ব্যবহৃত ঘর

            
node 12-কে নজরে রাখুন
সারি
হিসাব
  1. সারি 0 = parent. অংশ ২-এর BFS যা দিয়েছে, তা-ই up[0]. root 1-এর ঘরে 1 নিজে। 12-এর parent 10.
  2. সারি 1 — দুই ধাপ. 12 থেকে দুই ধাপ ওপরে যেতে আগে এক ধাপ (up[0][12] = 10), তারপর 10 থেকে আরেক ধাপ (up[0][10] = 7). তাই up[1][12] = 7. গোটা সারিটি এভাবে, প্রতিটি ঘর O(1)-এ। লক্ষ করুন 2 আর 3-এর দাদা নেই — তাদের ঘরে 1, কারণ root-এর “parent” root নিজেই। (4, 5, 6-এর ঘরেও 1, তবে সেটি সত্যিই তাদের দাদা।)
  3. সারি 2 — চার ধাপ. চার ধাপ = দুই ধাপ + দুই ধাপ: up[1][12] = 7, তারপর up[1][7] = 2. মিলিয়ে দেখুন: 12 → 10 → 7 → 4 → 2, ঠিক চার ধাপ। 13-এর ঘরে 3, কারণ 13 → 11 → 9 → 6 → 3 পথে ঠিক চার ধাপ।
  4. সারি 3 — আট ধাপ. up[2][12] = 2, up[2][2] = 1. আট ধাপ ওপরে কেউ নেই, লাফ root-এ আটকে গেছে — পুরো সারিটিই 1. টেবিল সম্পূর্ণ: LOG × n = 4 × 13 টি ঘর, প্রতিটি O(1)-এ।
  5. k-তম পূর্বপুরুষ. 12-এর ৫-তম পূর্বপুরুষ? 5 = 4 + 1 = binary 101: bit 0 আর bit 2 চালু। প্রথমে 20 লাফ: 12 → 10; তারপর 22 লাফ: up[2][10] = 1. উত্তর 1 — মিলিয়ে দেখুন, 12 → 10 → 7 → 4 → 2 → 1. লাফগুলো যেকোনো ক্রমে দেওয়া যায়, কারণ মোট ধাপ একই থাকে।

খরচ. টেবিলে LOG × (n + 1) টি ঘর, LOG = ⌈log₂(n + 1)⌉-এর কাছাকাছি, আর প্রতিটি ঘর O(1)-এ ভরে — সময় ও জায়গা দুটোই Θ(n log n). সারি k বানাতে সারি k − 1 সম্পূর্ণ থাকা চাই, তাই বাইরের লুপ k-এর ওপর, ভেতরের লুপ v-এর ওপর। (উল্টো ক্রমে লিখলে up[k − 1]-এর কিছু ঘর তখনো খালি থাকতে পারে — খুব প্রচলিত বাগ। তবে BFS ক্রমে v সাজিয়ে প্রতিটি v-এর পুরো কলাম একসাথে ভরলেও ঠিক থাকে, কারণ তখন সব পূর্বপুরুষের কলাম আগেই ভরা।)

k-তম পূর্বপুরুষ-এর প্রশ্ন, যেমন “এই কর্মীর ৫ ধাপ ওপরের কর্মকর্তা কে?”, এখন সরাসরি: k-এর প্রতিটি চালু bit j-এর জন্য একটি 2j লাফ — পর্ব ০৭-এর binary exponentiation-এর মতোই bit ধরে ধরে কাজ। k > depth[v] হলে এমন পূর্বপুরুষ নেই। মোট O(log n):

int kth(int v, int k) {                    // k-th ancestor of v, or -1 if it does not exist
    if (k > depth[v]) return -1;
    for (int j = 0; j < LOG; ++j)
        if ((k >> j) & 1) v = up[j][v];     // bit j of k is set: jump 2^j levels
    return v;
}

৫ · Binary lifting দিয়ে LCA — সমান গভীরতা, তারপর দুজনের একসাথে লাফ

অংশ ৩-এর সরল পদ্ধতির দুটি ধাপই এখন লাফে করা যায়:

  1. সমান গভীরতা. ধরুন depth[u] ≥ depth[v] (না হলে দুটি অদলবদল)। u-কে ঠিক d = depth[u] − depth[v] ধাপ তুলুন — এটি তো k-তম পূর্বপুরুষের প্রশ্নই। এরপর u = v হলে v নিজেই LCA.
  2. দুজনের একসাথে লাফ. k = LOG − 1 থেকে 0 পর্যন্ত, বড় লাফ আগে: যদি up[k][u] ≠ up[k][v], দুজনকেই লাফ দিন। সমান হলে লাফ দেবেন না — কারণ সেই node একটি সাধারণ পূর্বপুরুষ, কিন্তু হয়তো LCA-র ওপরে। শেষে u আর v ঠিক LCA-র ঠিক নিচের দুই সন্তান, তাই উত্তর up[0][u].
Pseudocode — LCA by binary lifting (nodes 1..n, up[k][root] = root)

lca(u, v):
    if depth[u] < depth[v]: swap(u, v)
    for k = LOG-1 down to 0:                  // phase 1: lift u to v's depth
        if depth[u] - 2^k >= depth[v]: u = up[k][u]
    if u == v: return u
    for k = LOG-1 down to 0:                  // phase 2: lift both, never onto a common ancestor
        if up[k][u] != up[k][v]:
            u = up[k][u];  v = up[k][v]
    return up[0][u]

সবচেয়ে শিক্ষণীয় প্রশ্ন LCA(12, 11) — এখানে দুই ধাপেই আসল লাফ আছে:

প্রধান উদাহরণসবুজ = এই ধাপের পর u আর v

            
প্রতিটি ধাপের পর
পরীক্ষা
সিদ্ধান্ত
u, v
  1. শুরু. u = 12 (depth 5), v = 11 (depth 4). u বেশি গভীর, তাই u-কে d = 1 ধাপ তুলতে হবে। 1-এর binary রূপে শুধু bit 0 চালু।
  2. প্রথম phase শেষ. k = 3, 2, 1-এ 2k ধাপ তুললে u, v-এর চেয়েও ওপরে চলে যেত — বাদ। k = 0-তে up[0][12] = 10. এখন u = 10, v = 11 — দুজনেই depth 4, কিন্তু আলাদা node, তাই দ্বিতীয় phase.
  3. k = 3: আট ধাপ. দুজনের ৮-তম পূর্বপুরুষ — দুজনেরই 1 (লাফ root-এ আটকে গেছে)। সমান মানে এটি একটি সাধারণ পূর্বপুরুষ — কিন্তু সবচেয়ে গভীরটি কি না, জানি না। তাই লাফ দিই না।
  4. k = 2: চার ধাপ. আবার দুজনেরই 1 — সমান, লাফ নয়। লক্ষ করুন, আমরা কখনো সাধারণ পূর্বপুরুষে নামি না; সব সময় তার ঠিক নিচে থাকার চেষ্টা করি।
  5. k = 1: দুই ধাপ. up[1][10] = 4, up[1][11] = 6 — আলাদা! তার মানে 4 আর 6 এখনো আলাদা শাখায়, LCA তাদের ওপরে। নিশ্চিন্তে লাফ: u = 4, v = 6.
  6. k = 0: এক ধাপ. up[0][4] = 2, up[0][6] = 3 — এখনো আলাদা, লাফ: u = 2, v = 3. দুজনেরই parent 1, কিন্তু সেটি দেখার আর লাফ বাকি নেই।
  7. উত্তর. লুপ শেষে u = 2, v = 3 — দুজনেই LCA-র ঠিক নিচে। LCA = up[0][2] = 1. দূরত্ব = 5 + 4 − 2·0 = 9: পথ 12 → 10 → 7 → 4 → 2 → 1 → 3 → 6 → 9 → 11. দুই phase-এ LOG = 4 টি করে, মোট ৮টি পরীক্ষা।

কেন ঠিক — একটি invariant আর binary search. দ্বিতীয় phase-এ একটি কথা সব সময় সত্য: “u আর v একই depth-এ, আলাদা node, আর LCA দুজনেরই কঠোরভাবে ওপরে।” শুরুতে সত্য (u ≠ v). লাফ দিই শুধু তখন, যখন up[k][u] ≠ up[k][v] — তখন নতুন u, v-ও আলাদা, তাই LCA তখনো ওপরে; invariant টিকে থাকে। এখন ভাবুন t = depth[u] − depth[LCA] − 1, অর্থাৎ “LCA-র ঠিক নিচে পৌঁছাতে কত ধাপ লাগবে”. j ধাপ ওপরের পূর্বপুরুষ দুজনের আলাদা হয় ঠিক তখন, যখন j ≤ t — এটি একটি monotonic predicate (পর্ব ১৯): ছোট j-এ সত্য, একটি সীমার পর চিরকাল মিথ্যা। বড় থেকে ছোট 2k চেষ্টা করে “সত্য থাকলে লাফ” দেওয়া মানে t-এর binary রূপ ওপরের bit থেকে একটি একটি করে বের করা — পর্ব ১৮-এর binary search, শুধু ধাপের দৈর্ঘ্য দুইয়ের ঘাতে। t < 2LOG বলে প্রতিটি bit একবারই দেখা যথেষ্ট, আর শেষে মোট ঠিক t ধাপ ওঠা হয়: u আর v LCA-র ঠিক নিচে, তাই up[0][u] = LCA. ∎

খরচ. দুই phase-এ LOG টি করে পরীক্ষা, প্রতিটি O(1) — প্রতি প্রশ্নে O(log n). প্রস্তুতি O(n log n). n = q = 2·105-এ মোট কাজ প্রায় 2·105 × 36 ≈ 7·106 ধাপ — অংশ ৩-এর 4·1010-এর তুলনায় কয়েক হাজার গুণ কম।

দুই node-এর দূরত্ব

u থেকে v-এর পথ সব সময় LCA দিয়ে যায়: u থেকে LCA পর্যন্ত ওঠা, তারপর v পর্যন্ত নামা। প্রথম অংশে depth[u] − depth[w] টি edge, দ্বিতীয় অংশে depth[v] − depth[w] টি (w = LCA). যোগ করলে:

dist(u, v) = depth[u] + depth[v] − 2·depth[LCA(u, v)]

নিচের প্রোগ্রাম প্রধান উদাহরণের edge-তালিকা থেকে tree ঝুলিয়ে up টেবিল বানায়, কয়েকটি প্রশ্নের LCA আর দূরত্ব ছাপে, তারপর সব ১৩ × ১৩ = ১৬৯টি জোড়ায় binary lifting-এর উত্তর অংশ ৩-এর সরল পদ্ধতির সাথে মিলিয়ে দেখে — ছোট আকারের stress testing.

#include <iostream>
#include <queue>
#include <utility>
#include <vector>
using namespace std;
int main() {
    const int n = 13, LOG = 4;                                  // 2^LOG > n
    vector<pair<int, int>> edges{{1,2},{1,3},{2,4},{2,5},{3,6},{4,7},{4,8},{6,9},{7,10},{9,11},{10,12},{11,13}};
    vector<vector<int>> adj(n + 1), up(LOG, vector<int>(n + 1));
    for (auto [a, b] : edges) { adj[a].push_back(b); adj[b].push_back(a); }
    vector<int> depth(n + 1, -1); depth[1] = 0; up[0][1] = 1;   // root's parent = itself
    queue<int> q; q.push(1);
    while (!q.empty()) {                                        // BFS from the root: parent and depth
        int u = q.front(); q.pop();
        for (int v : adj[u]) if (depth[v] < 0) { depth[v] = depth[u] + 1; up[0][v] = u; q.push(v); }
    }
    for (int k = 1; k < LOG; ++k)
        for (int v = 1; v <= n; ++v) up[k][v] = up[k - 1][up[k - 1][v]];   // 2^k = 2^(k-1) + 2^(k-1)
    auto lca = [&](int u, int v) {
        if (depth[u] < depth[v]) swap(u, v);
        for (int k = LOG - 1; k >= 0; --k)                      // lift u to v's depth
            if (depth[u] - (1 << k) >= depth[v]) u = up[k][u];
        if (u == v) return u;
        for (int k = LOG - 1; k >= 0; --k)                      // lift both, staying below the LCA
            if (up[k][u] != up[k][v]) { u = up[k][u]; v = up[k][v]; }
        return up[0][u];
    };
    auto naive = [&](int u, int v) {                            // walk up one edge at a time
        while (depth[u] > depth[v]) u = up[0][u];
        while (depth[v] > depth[u]) v = up[0][v];
        while (u != v) { u = up[0][u]; v = up[0][v]; }
        return u;
    };
    for (auto [u, v] : vector<pair<int, int>>{{12, 8}, {12, 11}, {10, 5}, {4, 12}, {13, 9}}) {
        int w = lca(u, v);
        cout << "lca(" << u << "," << v << ") = " << w << "  dist = " << depth[u] + depth[v] - 2 * depth[w] << '\n';
    }
    int bad = 0;
    for (int u = 1; u <= n; ++u) for (int v = 1; v <= n; ++v) bad += lca(u, v) != naive(u, v);
    cout << "all " << n * n << " pairs checked, mismatches: " << bad << '\n';
}

আউটপুট:

lca(12,8) = 4  dist = 4
lca(12,11) = 1  dist = 9
lca(10,5) = 2  dist = 4
lca(4,12) = 4  dist = 3
lca(13,9) = 9  dist = 2
all 169 pairs checked, mismatches: 0

LCA(12, 11) = 1 আর দূরত্ব 9 — ডায়াগ্রামের সাথে মেলে। LCA(4, 12) = 4 আর LCA(13, 9) = 9 দেখায় “একজন নিজেই অন্যজনের পূর্বপুরুষ” ক্ষেত্রটি: প্রথম phase-এর পরেই u == v, তাই সেখানেই উত্তর। শেষ লাইনটি সবচেয়ে গুরুত্বপূর্ণ: ১৬৯টি জোড়ার (u = v-সহ) একটিতেও দ্রুত আর সরল পদ্ধতি আলাদা উত্তর দেয়নি। up[0][1] = 1 লাইনটিই root-এর ওপরের লাফকে root-এ আটকে রাখে। এটি বাদ দিলে up[0][1] = 0 থাকত, আর node 0 একটি sentinel-এর কাজ করত (সব up[k][0] = 0) — সেটিও চলে। কিন্তু root-এর parent −1 রাখলে up[k][-1] পড়া হয়: C++-এ অ্যারের বাইরে (undefined behavior), Python-এ নীরবে শেষ ঘর — দুটিই ভুল উত্তরের উৎস। BFS O(n), টেবিল O(n log n), প্রতিটি প্রশ্ন O(log n).

from collections import deque

n, LOG = 13, 4                                   # 2**LOG > n
edges = [(1,2),(1,3),(2,4),(2,5),(3,6),(4,7),(4,8),(6,9),(7,10),(9,11),(10,12),(11,13)]
adj = [[] for _ in range(n + 1)]
for a, b in edges:
    adj[a].append(b); adj[b].append(a)
depth = [-1] * (n + 1); depth[1] = 0
up = [[0] * (n + 1) for _ in range(LOG)]; up[0][1] = 1   # root's parent = itself
q = deque([1])
while q:                                         # BFS from the root: parent and depth
    u = q.popleft()
    for v in adj[u]:
        if depth[v] < 0:
            depth[v] = depth[u] + 1; up[0][v] = u; q.append(v)
for k in range(1, LOG):
    for v in range(1, n + 1):
        up[k][v] = up[k - 1][up[k - 1][v]]       # 2^k = 2^(k-1) + 2^(k-1)

def lca(u, v):
    if depth[u] < depth[v]: u, v = v, u
    for k in reversed(range(LOG)):               # lift u to v's depth
        if depth[u] - (1 << k) >= depth[v]: u = up[k][u]
    if u == v: return u
    for k in reversed(range(LOG)):               # lift both, staying below the LCA
        if up[k][u] != up[k][v]: u, v = up[k][u], up[k][v]
    return up[0][u]

def naive(u, v):                                 # walk up one edge at a time
    while depth[u] > depth[v]: u = up[0][u]
    while depth[v] > depth[u]: v = up[0][v]
    while u != v: u, v = up[0][u], up[0][v]
    return u

for u, v in [(12, 8), (12, 11), (10, 5), (4, 12), (13, 9)]:
    w = lca(u, v)
    print(f"lca({u},{v}) = {w}  dist = {depth[u] + depth[v] - 2 * depth[w]}")
bad = sum(lca(u, v) != naive(u, v) for u in range(1, n + 1) for v in range(1, n + 1))
print(f"all {n * n} pairs checked, mismatches: {bad}")

আউটপুট C++-এর সাথে হুবহু এক:

lca(12,8) = 4  dist = 4
lca(12,11) = 1  dist = 9
lca(10,5) = 2  dist = 4
lca(4,12) = 4  dist = 3
lca(13,9) = 9  dist = 2
all 169 pairs checked, mismatches: 0

up একটি list-এর list: up[k][v]. reversed(range(LOG)) দেয় 3, 2, 1, 0 — বড় লাফ আগে। u, v = up[k][u], up[k][v] দুটি মান একসাথে বদলায়, তাই ডান পাশের দুটিই পুরোনো u, v থেকে পড়া হয়। sum(...)-এর ভেতরে প্রতিটি অমিল True = 1 হিসেবে যোগ হয়। Python-এ বড় ইনপুটে (n ≈ 2·105) এই টেবিল বানাতে প্রায় ৩৬ লাখ ঘর লেখা হয় — কাজ চলে, তবে C++-এর চেয়ে কয়েক গুণ ধীর; কনটেস্টে প্রতিটি সারি list comprehension দিয়ে বানালে দ্রুত হয়: up[k] = [prev[prev[v]] for v in range(n + 1)].

৬ · পথের ওপর হিসাব — ওজনসহ দূরত্ব, সবচেয়ে ধীর লিংক

বাস্তবের tree-তে edge-এর সাধারণত একটি ওজন থাকে। একটি প্রতিষ্ঠানের নেটওয়ার্ক ভাবুন, যেখানে router-গুলো একটি tree আকারে যুক্ত (অনেক নেটওয়ার্কে লুপ ঠেকাতে লিংকগুলোকে ইচ্ছা করেই একটি tree-তে নামানো হয়)। প্রতিটি লিংকের একটি latency আছে — মিলিসেকেন্ডে কত দেরি হয়। দুই router-এর মাঝে প্যাকেট কত দেরিতে পৌঁছাবে? এটি একটি weighted tree-তে দূরত্বের প্রশ্ন।

ওজনসহ দূরত্ব. BFS-এর সময় depth-এর পাশাপাশি rootDist[v] = root থেকে v পর্যন্ত ওজনের যোগফল রাখুন (rootDist[সন্তান] = rootDist[parent] + edge-এর ওজন)। তারপর ঠিক আগের যুক্তিতে: dist(u, v) = rootDist[u] + rootDist[v] − 2·rootDist[w], w = LCA(u, v). LCA বের করতে ওজন লাগে না — শুধু depth (edge-সংখ্যা) দিয়েই lifting চলে। এটি আসলে পর্ব ১৭-এর prefix sum-এর tree সংস্করণ: অ্যারেতে sum(l..r) = P[r] − P[l − 1]; tree-তে root থেকে prefix, আর LCA হলো “বিয়োগের বিন্দু”.

পথের সর্বোচ্চ বা সর্বনিম্ন. “দুই router-এর পথে সবচেয়ে ধীর লিংক কোনটি?” — যোগফলের মতো বিয়োগ করে এটি পাওয়া যায় না, কারণ max-এর কোনো উল্টো কাজ নেই। সমাধান: up টেবিলের পাশে আরেকটি টেবিল রাখুন, যেখানে প্রতিটি লাফের সাথে সেই লাফে পার হওয়া edge-গুলোর সর্বোচ্চ জমা থাকে।

Pseudocode — maximum edge weight on a path (w(v) = weight of edge v–parent(v))

mx[0][v] = w(v)                                       // the single edge above v
mx[k][v] = max( mx[k-1][v], mx[k-1][ up[k-1][v] ] )   // first half of the jump, then second half

pathMax(u, v):  best = -infinity
    run lca(u, v); before EVERY jump of u:  best = max(best, mx[k][u]);  same for v
    after phase 2 (if u != v):              best = max(best, mx[0][u], mx[0][v])
    return best

একটি লাফ মানে u থেকে up[k][u] পর্যন্ত ঠিক 2k টি edge, আর mx[k][u] তাদের সর্বোচ্চ। LCA খোঁজার সময় u আর v যে লাফগুলো দেয়, সেগুলো মিলে ঠিক u → LCA আর v → LCA পথ দুটি ঢেকে ফেলে, কোনো edge দুবার না গুনে — তাই উত্তর সঠিক, সময় O(log n), বাড়তি জায়গা আরেকটি Θ(n log n) টেবিল। একই ছাঁচে min, gcd, যোগফল — যেকোনো associative operation. (যোগফলের জন্য অবশ্য rootDist-এর বিয়োগই সহজ।) কনটেস্টে এর ক্লাসিক ব্যবহার: minimum spanning tree (পর্ব ৪৫)-এ একটি নতুন edge যোগ করলে কোন edge বাদ যাবে — u থেকে v-এর পথের সবচেয়ে ভারী edge.

নেটওয়ার্কের latency

Tree-আকারের router নেটওয়ার্কে দুই যন্ত্রের মাঝে দেরি = rootDist-এর সূত্র; সবচেয়ে ধীর লিংক = mx টেবিল। লাখো প্রশ্নে প্রতিটি O(log n).

ফাইলের আপেক্ষিক ঠিকানা

/home/rahim/docs/cv থেকে /home/rahim/music: LCA rahim. .. লাগবে depth[cv] − depth[rahim] = 2 বার — ../../music.

বংশতালিকা আর জীববিজ্ঞান

দুই আত্মীয়ের সম্পর্কের দূরত্ব, আর বিবর্তনের tree-তে দুই প্রজাতির সবচেয়ে কাছের সাধারণ পূর্বপুরুষ — একই প্রশ্ন।

“পূর্বপুরুষ কি না?”

u, v-এর ancestor ঠিক তখন, যখন LCA(u, v) = u. অংশ ৮-এর entry/exit time দিয়ে এটি O(1)-এও হয়।

৭ · আরও দ্রুত — Euler tour + sparse table, আর offline পদ্ধতি

পর্ব ৩৭-এ (sparse table) একটি ঝলক দেখেছিলেন: LCA-কে একটি অ্যারের range minimum query-তে বদলে প্রতিটি প্রশ্ন O(1)-এ করা যায়। প্রধান উদাহরণে বিস্তারিত দেখি। root থেকে DFS চালান, আর প্রতিটি node-এ ঢোকার সময় এবং প্রতিটি সন্তান থেকে ফিরে আসার সময় সেই node-টি লিখুন। এই ক্রমের নাম Euler tour; n টি node-এ দৈর্ঘ্য 2n − 1 (প্রতিটি edge-এ একবার নামা আর একবার ওঠা, সাথে শুরুর root).

pos   :  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
node  :  1  2  4  7 10 12 10  7  4  8  4  2  5  2  1  3  6  9 11 13 11  9  6  3  1
depth :  0  1  2  3  4  5  4  3  2  3  2  1  2  1  0  1  2  3  4  5  4  3  2  1  0

first[12] = 5, first[8] = 9   ->  min depth on pos 5..9  is 2 at pos 8  ->  LCA(12, 8) = 4
first[12] = 5, first[13] = 19 ->  min depth on pos 5..19 is 0 at pos 14 ->  LCA(12, 13) = 1

first[v] হলো tour-এ v-এর প্রথম অবস্থান। কেন কাজ করে: ধরুন first[u] ≤ first[v] আর w = LCA(u, v). DFS u-তে ঢোকার পর v-তে পৌঁছানোর আগে w-এর subtree থেকে বের হয় না (v তো w-এর subtree-তেই), তাই এই অংশে w-এর চেয়ে কম গভীর কেউ নেই। আবার u আর v আলাদা শাখায় থাকলে u-এর শাখা থেকে v-এর শাখায় যেতে DFS-কে w-তে ফিরে আসতেই হয় — তাই w নিজে এই অংশে আছে। (u নিজে v-এর পূর্বপুরুষ হলে w = u, যে অংশের শুরুতেই আছে।) ফলে অংশটির সবচেয়ে কম depth-এর node-টিই w.

এখন tour-এর depth অ্যারের ওপর পর্ব ৩৭-এর sparse table বানান — min একটি idempotent operation, তাই দুটি ওভারল্যাপ করা ব্লকে প্রতিটি প্রশ্ন O(1). প্রস্তুতি: tour O(n), sparse table O(n log n) — অ্যারের দৈর্ঘ্য 2n − 1. টেবিলে depth-এর বদলে tour-এর index রাখুন, যাতে সর্বনিম্নের node-টি জানা যায়।

পদ্ধতিপ্রস্তুতিপ্রতি প্রশ্নজায়গাকখন ভালো
সরল (এক ধাপ করে ওঠা)O(n)O(h)O(n)অল্প প্রশ্ন, বা যাচাইয়ের brute force
Binary liftingO(n log n)O(log n)O(n log n)k-তম পূর্বপুরুষ, পথের max/min দরকার হলে; কনটেস্টের প্রথম পছন্দ
Euler tour + sparse tableO(n log n)O(1)O(n log n)বিপুল সংখ্যক শুধু-LCA প্রশ্ন
Tarjan-এর offline (DSU)মোট O((n + q) · α(n))O(n + q)সব প্রশ্ন আগে থেকে জানা থাকলে

আরও গভীরে — Tarjan-এর offline LCA: সব প্রশ্ন যদি আগে থেকেই জানা থাকে (offline), তাহলে একটি DFS আর পর্ব ৩৩-এর union-find (DSU) দিয়ে সব উত্তর একবারে বের হয়। DFS-এ একটি node u-র প্রতিটি সন্তান শেষ হলে সন্তানের সেটকে u-র সেটের সাথে union করুন, আর সেটের “প্রতিনিধি পূর্বপুরুষ” রাখুন u. u-র কাজ শেষ হলে u-কে “দেখা” চিহ্নিত করুন, আর (u, v) প্রশ্নের v আগেই দেখা হয়ে থাকলে উত্তর = find(v)-এর সেটের পূর্বপুরুষ — কারণ v-এর যে পূর্বপুরুষের subtree-তে DFS এখনো কাজ করছে, সবচেয়ে গভীর সেই node-ই u-রও পূর্বপুরুষ। Robert Tarjan ১৯৭৯ সালে path compression-এর প্রয়োগ নিয়ে লেখা গবেষণায় এটি দেখান; path compression আর union by rank মিলিয়ে মোট সময় প্রায় রৈখিক, O((n + q) · α(n)) — α হলো পর্ব ৩৩-এর inverse Ackermann.

৮ · Subtree-র প্রশ্ন — entry/exit time আর একটানা রেঞ্জ

এবার অন্য ধরনের প্রশ্ন। একটি কোম্পানির ক্রমবিন্যাসে প্রতিটি কর্মীর একটি বেতন আছে, আর প্রশ্ন আসে: “এই ম্যানেজারের অধীনের সবার (তিনি নিজেসহ) মোট বেতন কত?”, মাঝে মাঝে “অমুকের বেতন বাড়ল”. অর্থাৎ subtree-র যোগফল, সাথে point update. প্রতিবার subtree ঘুরে যোগ করলে O(n). চতুর সমাধান: tree-কে এমন একটি সারিতে সাজান, যেখানে প্রতিটি subtree একটি একটানা অংশ.

DFS-এ প্রতিটি node-এ প্রথম ঢোকার সময় একটি ঘড়ির মান দিন — tin[v] = 0, 1, 2, … ক্রমে — আর v-এর পুরো subtree শেষ হলে tout[v] = এ পর্যন্ত দেওয়া সবচেয়ে বড় tin. এই দুটি সংখ্যাকে বলে entry/exit time (অনেকে এটিকেও Euler tour বলেন)। DFS v-তে ঢুকে তার subtree শেষ না করে বের হয় না, তাই v-এর subtree-র সবাই ঠিক tin[v] থেকে tout[v]-এর মধ্যে নম্বর পায় — আর অন্য কেউ সেখানে নয়।

প্রধান উদাহরণ — DFS, সন্তান ছোট থেকে বড় ক্রমেসবুজ = এই ধাপে ঢোকা/বেরোনো

            
প্রতিটি ধাপের পর
এই ধাপের কাজ
tin ক্রমে সারি
নতুন tin / tout
  1. নামতে নামতে নম্বর. DFS 1 থেকে বাঁয়ের শাখা ধরে 2, 4, 7, 10, 12-তে নামে। প্রত্যেকে ঢোকার মুহূর্তে পরের নম্বর পায়: tin 0 থেকে 5.
  2. প্রথম বেরোনো. 12 leaf, তাই তার subtree শেষ: tout[12] = 5. ফিরে 10 আর 7-এরও আর সন্তান নেই — tout 5. 4-এর পরের সন্তান 8: tin = tout = 6. তারপর 4 শেষ, tout[4] = 6. 4-এর subtree {4, 7, 10, 12, 8} পেয়েছে ঠিক নম্বর 2 থেকে 6.
  3. 5 আর 2. 2-এর পরের সন্তান 5: tin = tout = 7. এরপর 2 শেষ: tout[2] = 7, তাই subtree(2) = রেঞ্জ [1, 7].
  4. ডান শাখা. 3, 6, 9, 11, 13 পায় 8 থেকে 12; সবাই শেষ হয় 12-এ, শেষে root-ও: tout[1] = 12 = n − 1. প্রতিটি node একবার ঢোকা আর একবার বেরোনো — O(n).
  5. প্রশ্ন. subtree(4) = tin ক্রমের সারির অবস্থান 2 থেকে 6 = 4 7 10 12 8. এখন সারিটির ওপর একটি Fenwick tree (পর্ব ৩৬) রাখুন, যেখানে অবস্থান tin[v]-এ v-এর বেতন: subtree-র যোগফল = sum(tin[v]..tout[v]), বেতন বদলানো = একটি point update — দুটিই O(log n).

এভাবে tree-র প্রশ্ন অ্যারের প্রশ্নে নেমে আসে, আর পুরো মডিউল ৫-এর হাতিয়ার কাজে লাগে: subtree-তে যোগ বা সর্বোচ্চ — segment tree (পর্ব ৩৪) বা Fenwick tree (পর্ব ৩৬); “পুরো subtree-র সবার বেতন ৫% বাড়াও” — lazy propagation (পর্ব ৩৫). আর একটি উপহার: u, v-এর পূর্বপুরুষ কি না, তা O(1)-এ — শর্ত tin[u] ≤ tin[v] এবং tout[v] ≤ tout[u]. অনেক binary lifting কোডে প্রথম phase-এর বদলে এই পরীক্ষা ব্যবহার করা হয়।

Subtree রেঞ্জ হয়, কিন্তু পথ হয় না। tin ক্রমে u থেকে v-এর পথ সাধারণত একটানা থাকে না — যেমন পথ 8 → 4 → 2 → 5-এর tin 6, 2, 1, 7. পথের যোগফলে বদল আসতে থাকলে (“এই পথের সব edge-এর ওজন বদলাও”) দরকার অংশ ৯-এর heavy-light decomposition, বা root থেকে prefix-এর কৌশল: tin[v]-এ +x আর tout[v] + 1-এ −x যোগ করলে prefix sum(tin[u]) দেয় root থেকে u-এর পথের যোগফল।

৯ · Tree-এর diameter — দুইবার BFS, আর কেন তা ঠিক

পর্ব ২৬-এ binary tree-তে diameter বের করেছিলেন একটি postorder দিয়ে — প্রতিটি node-এ “বাঁকের পথ” মেপে। যেকোনো সংখ্যক সন্তানের সাধারণ tree-তে, যেখানে ইনপুট শুধু edge-তালিকা, আরেকটি আশ্চর্য সরল পদ্ধতি আছে:

  1. যেকোনো node s থেকে BFS চালিয়ে সবচেয়ে দূরের node a বের করুন।
  2. a থেকে আবার BFS চালিয়ে সবচেয়ে দূরের node b বের করুন। a থেকে b-এর দূরত্বই diameter, আর a–b পথটিই একটি সবচেয়ে লম্বা পথ।
প্রধান উদাহরণ (এখানে root-এর কোনো ভূমিকা নেই)সবুজ = এই ধাপের মূল node

            
প্রতিটি ধাপে
দূরত্ব (node:দূরত্ব)
ফল
  1. যেকোনো শুরু. ইচ্ছা করেই এমন node বেছে নিলাম, যা কোনো লম্বা পথের প্রান্ত নয় — 5, একটি ছোট leaf. 5 থেকে BFS প্রতিটি node-এর edge-দূরত্ব দেয়।
  2. প্রথম প্রান্ত. 5 থেকে সবচেয়ে দূরে 13, দূরত্ব 7 (5 → 2 → 1 → 3 → 6 → 9 → 11 → 13). দাবি: 13 কোনো একটি diameter-এর প্রান্ত — নিচে প্রমাণ।
  3. দ্বিতীয় BFS. এবার 13 থেকে BFS. সবচেয়ে দূরে 12, দূরত্ব 10. প্রথমটির প্রান্ত সঠিক হলে, তার থেকে সবচেয়ে দূরের node-ই diameter-এর অন্য প্রান্ত।
  4. Diameter. পথ 12 → 10 → 7 → 4 → 2 → 1 → 3 → 6 → 9 → 11 → 13: ১০টি edge. দুটি BFS, প্রতিটি O(n) — মোট O(n), আর সব জোড়া মেপে দেখার O(n²) brute force-এর সাথে নিচের প্রোগ্রাম মিলিয়ে দেখবে।

প্রমাণের রূপরেখা. মূল দাবি: যেকোনো s থেকে সবচেয়ে দূরের node a কোনো একটি সবচেয়ে লম্বা পথের প্রান্ত। ধরুন x–y একটি diameter, দৈর্ঘ্য D, আর d(·, ·) হলো tree-র দূরত্ব। দুই ক্ষেত্র:

  • s থেকে a-এর পথ x–y পথকে ছোঁয়. s থেকে চলতে গিয়ে x–y পথে প্রথম যে node পড়ে, তার নাম c. তাহলে s থেকে x-এর আর y-এর পথও c দিয়ে যায়, তাই a সবচেয়ে দূরে বলে d(c, a) ≥ d(c, x) এবং d(c, a) ≥ d(c, y). c থেকে a-এর পথ x–y পথ ধরে বড়জোর একটি দিকে যেতে পারে; উল্টো দিকের প্রান্তটি নিন — ধরুন x. তাহলে x → c → a একটি সরল পথ, দৈর্ঘ্য d(x, c) + d(c, a) ≥ d(x, c) + d(c, y) = D. কোনো পথ D-এর চেয়ে লম্বা হতে পারে না — তাই x → c → a নিজেই একটি diameter, আর a তার প্রান্ত।
  • দুই পথ একটিও node ভাগ করে না. tree connected, তাই দুই পথকে জোড়া দেওয়া একটি সংযোগ-পথ আছে: s–a পথের যে node p x–y পথের সবচেয়ে কাছে, তার থেকে x–y পথের সবচেয়ে কাছের node q পর্যন্ত, দৈর্ঘ্য ≥ 1. s থেকে x–y পথের যেকোনো node-এ যেতে হলে p আর q পেরোতেই হয়। ধরুন d(q, y) ≥ d(q, x), তাই d(q, y) ≥ D/2. a সবচেয়ে দূরে বলে d(s, a) ≥ d(s, y) = d(s, p) + d(p, q) + d(q, y), অর্থাৎ d(p, a) ≥ d(p, q) + d(q, y). তাহলে y → q → p → a পথের দৈর্ঘ্য d(y, q) + d(q, p) + d(p, a) ≥ 2·d(q, y) + 2·d(p, q) > D — অসম্ভব। তাই এই ক্ষেত্র ঘটেই না।

a একটি diameter-এর প্রান্ত হলে, a থেকে সবচেয়ে দূরের node b পর্যন্ত দূরত্ব অন্তত D (diameter-এর অন্য প্রান্ত তো আছেই), আর D-এর বেশি হতে পারে না — তাই d(a, b) = D. ∎ প্রমাণটি শুধু ধনাত্মক (বা শূন্য) edge-ওজনেই খাটে: ওজনসহ tree-তে BFS-এর বদলে DFS বা BFS দিয়ে rootDist-এর মতো ওজনের দূরত্ব মাপুন — tree-তে দুই node-এর পথ একটিই, তাই Dijkstra (পর্ব ৪৩) লাগে না। ঋণাত্মক ওজনে প্রমাণ ভাঙে; তখন পর্ব ২৬-এর মতো “বাঁকের পথ”-এর DP ব্যবহার করুন।

#include <algorithm>
#include <iostream>
#include <queue>
#include <utility>
#include <vector>
using namespace std;
const int n = 13;
vector<vector<int>> adj(n + 1);
pair<int, vector<int>> bfs(int s) {                  // farthest node from s, and all distances
    vector<int> d(n + 1, -1); d[s] = 0; int far = s;
    queue<int> q; q.push(s);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        if (d[u] > d[far]) far = u;
        for (int v : adj[u]) if (d[v] < 0) { d[v] = d[u] + 1; q.push(v); }
    }
    return {far, d};
}
int main() {
    vector<pair<int, int>> edges{{1,2},{1,3},{2,4},{2,5},{3,6},{4,7},{4,8},{6,9},{7,10},{9,11},{10,12},{11,13}};
    for (auto [x, y] : edges) { adj[x].push_back(y); adj[y].push_back(x); }
    auto [a, d1] = bfs(5);                           // any start node works
    auto [b, d2] = bfs(a);
    cout << "BFS from 5: farthest = " << a << " at distance " << d1[a] << '\n';
    cout << "BFS from " << a << ": farthest = " << b << " at distance " << d2[b] << '\n';
    cout << "diameter path:";
    for (int x = b; ; ) {                            // walk back from b towards a
        cout << ' ' << x;
        if (x == a) break;
        for (int v : adj[x]) if (d2[v] == d2[x] - 1) { x = v; break; }
    }
    int brute = 0;                                   // O(n^2) check: BFS from every node
    for (int s = 1; s <= n; ++s) { auto [f, d] = bfs(s); brute = max(brute, d[f]); }
    cout << "\nbrute force diameter = " << brute << '\n';
}

আউটপুট:

BFS from 5: farthest = 13 at distance 7
BFS from 13: farthest = 12 at distance 10
diameter path: 12 10 7 4 2 1 3 6 9 11 13
brute force diameter = 10

ডায়াগ্রামের সাথে হুবহু মেলে। bfs একটি জোড়া ফেরত দেয় — সবচেয়ে দূরের node আর পুরো দূরত্বের অ্যারে; auto [a, d1] সেটি দুই নামে খুলে নেয়। পথটি আলাদা parent অ্যারে ছাড়াই পাওয়া গেছে: b থেকে শুরু করে প্রতিবার এমন প্রতিবেশীতে যাওয়া, যার a থেকে দূরত্ব এক কম — tree-তে এমন প্রতিবেশী ঠিক একটি, তাই পথটিও অনন্য। শেষ লাইনে প্রতিটি node থেকে BFS (মোট O(n²)) চালিয়ে সবচেয়ে বড় দূরত্ব নেওয়া হয়েছে — একই 10. ছোট tree-তে এমন brute force-ই আপনার দ্রুত কোডের সবচেয়ে ভালো পরীক্ষক।

from collections import deque

n = 13
edges = [(1,2),(1,3),(2,4),(2,5),(3,6),(4,7),(4,8),(6,9),(7,10),(9,11),(10,12),(11,13)]
adj = [[] for _ in range(n + 1)]
for a, b in edges:
    adj[a].append(b); adj[b].append(a)

def bfs(s):                                      # farthest node from s, and all distances
    d = [-1] * (n + 1); d[s] = 0; far = s
    q = deque([s])
    while q:
        u = q.popleft()
        if d[u] > d[far]: far = u
        for v in adj[u]:
            if d[v] < 0: d[v] = d[u] + 1; q.append(v)
    return far, d

a, d1 = bfs(5)                                   # any start node works
b, d2 = bfs(a)
print(f"BFS from 5: farthest = {a} at distance {d1[a]}")
print(f"BFS from {a}: farthest = {b} at distance {d2[b]}")
path = [b]                                       # walk back from b towards a
while path[-1] != a:
    x = path[-1]
    path.append(next(v for v in adj[x] if d2[v] == d2[x] - 1))
print("diameter path:", *path)
brute = max(max(bfs(s)[1]) for s in range(1, n + 1))   # O(n^2) check: BFS from every node
print(f"brute force diameter = {brute}")

আউটপুট C++-এর সাথে হুবহু এক:

BFS from 5: farthest = 13 at distance 7
BFS from 13: farthest = 12 at distance 10
diameter path: 12 10 7 4 2 1 3 6 9 11 13
brute force diameter = 10

deque-এর popleft() O(1) — সাধারণ list-এর pop(0) O(n), তাই BFS-এ সব সময় deque (পর্ব ১৪). next(v for v in adj[x] if ...) প্রথম মানানসই প্রতিবেশীটি দেয়। max(bfs(s)[1])-এ অ্যারের index 0-র মান −1 থাকলেও ক্ষতি নেই, কারণ আসল দূরত্বগুলো ≥ 0. BFS recursion ব্যবহার করে না, তাই লাখো node-এর শিকলেও কোনো RecursionError নেই।

Diameter-এর একটি উপহার — tree-র কেন্দ্র. একটি node-এর eccentricity হলো তার থেকে সবচেয়ে দূরের node-এর দূরত্ব; যার eccentricity সবচেয়ে কম, সে tree-র কেন্দ্র (center). প্রমাণ করা যায়, কেন্দ্র সব সময় diameter-এর ঠিক মাঝখানে — D জোড় হলে একটি node, বিজোড় হলে দুটি পাশাপাশি node — আর সর্বনিম্ন eccentricity ⌈D/2⌉. প্রধান উদাহরণে D = 10, তাই কেন্দ্র পথের ষষ্ঠ node, অর্থাৎ 1, আর তার eccentricity 5. একটি tree-নেটওয়ার্কে সার্ভার বসাতে চাইলে, যাতে সবচেয়ে দূরের যন্ত্রের দেরি সবচেয়ে কম হয় — কেন্দ্রই উত্তর।

আরও উন্নত কৌশলের এক ঝলক

Rerooting DP

“প্রতিটি node-কে root ধরলে উত্তর কত?” — যেমন প্রতিটি node থেকে সবার দূরত্বের যোগফল। দুটি DFS: প্রথমটিতে নিচ থেকে ওপরে, দ্বিতীয়টিতে root বদলানোর সময় উত্তর O(1)-এ সংশোধন। মোট O(n). পর্ব ৫৫-এর tree DP-র অংশ।

Centroid decomposition

যে node সরালে কোনো টুকরো n/2-এর বড় থাকে না, সে centroid. তাকে সরিয়ে প্রতিটি টুকরোয় একই কাজ — গভীরতা O(log n). “দূরত্ব k-এর কয়টি জোড়া” ধরনের পথ-গোনার প্রশ্নে O(n log n) বা কাছাকাছি।

Heavy-light decomposition

প্রতিটি node থেকে সবচেয়ে বড় subtree-র সন্তানের দিকে “ভারী” edge. যেকোনো পথ O(log n) টি ভারী শিকলে ভাঙে, আর শিকলগুলো একটি segment tree-তে একটানা — পথে update আর query O(log² n).

পরীক্ষা ও ইন্টারভিউতে:

  • n টি node আর q টি LCA প্রশ্নে binary lifting-এর মোট সময় কত? — প্রস্তুতি O(n log n), প্রতি প্রশ্ন O(log n): মোট O((n + q) log n); জায়গা O(n log n).
  • LCA জানা থাকলে দুই node-এর দূরত্ব? — depth[u] + depth[v] − 2·depth[LCA]; ওজনসহ হলে depth-এর জায়গায় root থেকে ওজনের যোগফল।
  • দ্বিতীয় phase-এ up[k][u] == up[k][v] হলে কেন লাফ দেওয়া যাবে না? — সেটি একটি সাধারণ পূর্বপুরুষ, কিন্তু LCA-র ওপরেও হতে পারে; লাফ দিলে সবচেয়ে গভীরটি হারিয়ে যায়।
  • দুইবার BFS-এ diameter কেন ঠিক, আর কোথায় ভাঙে? — যেকোনো node থেকে সবচেয়ে দূরের node একটি diameter-এর প্রান্ত (দুই ক্ষেত্রের প্রমাণ); ঋণাত্মক ওজনে বা চক্রওয়ালা graph-এ খাটে না।
  • Subtree-র যোগফল দ্রুত কীভাবে? — DFS-এর tin/tout-এ subtree = রেঞ্জ [tin, tout]; তার ওপর Fenwick বা segment tree, O(log n).

১০ · AI-এর যুগে tree-এর কৌশল — দেখতে ঠিক, লুকানো ভুল

Binary lifting-এর কোড ২০ লাইনের, আর AI এটি প্রায় সব সময় পরিচিত চেহারায় লেখে। কিন্তু ভুল লুকিয়ে থাকে ঠিক সেই জায়গাগুলোয়, যেখানে ছোট পরীক্ষায় ধরা পড়ে না: LOG এক কম, root-এর parent 0 বা −1 রেখে অ্যারের বাইরে পড়া, দ্বিতীয় phase-এ !=-এর বদলে ==, বা টেবিলের লুপের ক্রম উল্টো। দুইবার BFS-এর ক্ষেত্রে AI প্রায়ই দাবি করে পদ্ধতিটি সাধারণ graph-এও চলে, বা ঋণাত্মক ওজনেও — দুটিই ভুল।

AI-এর ভুলকীভাবে ধরবেন
LOG = ⌊log₂ n⌋ বা ১৭ ধরা, অথচ depth 2LOG ছুঁতে পারেn টি node-এর শিকলে সবচেয়ে নিচের আর root-এর LCA চান
root-এর parent −1, আর up[k][-1] পড়াroot নিজে একটি প্রশ্নে থাকুক: LCA(root, leaf); sanitizer বা Python-এর negative index দেখুন
দ্বিতীয় phase-এ সমান হলে লাফ, বা শেষে up[0][u]-এর বদলে u ফেরতসরল LCA-র সাথে সব জোড়া মিলিয়ে দেখুন (অংশ ৫-এর প্রোগ্রামের মতো)
প্রথম phase বাদ দিয়ে সরাসরি দুজনকে একসাথে তোলাআলাদা depth-এর একটি জোড়া দিন, যেমন (12, 8)
recursive DFS দিয়ে rooting, ২ লাখ node-এর শিকলে crashলম্বা শিকল ইনপুট দিয়ে চালান; BFS বা নিজের stack চান
“দুইবার BFS যেকোনো graph-এ diameter দেয়”একটি চক্র (যেমন ৫টি node-এর বৃত্ত আর একটি লেজ) দিয়ে brute force-এর সাথে মেলান

AI-কে কোড লিখে দিতে বলার চেয়ে আপনার কোডের ফাঁক খুঁজতে আর পরীক্ষার ইনপুট বানাতে বলুন:

আমি DSA শিখছি, এখন binary lifting দিয়ে LCA. নিচে আমার কোড আর একটি ছোট tree-তে আমার হাতে করা up টেবিল দিলাম: [এখানে কোড আর টেবিল পেস্ট করুন] পুরো সমাধান নতুন করে লিখে দিও না। শুধু এগুলো করো: ১. আমার LOG-এর মান n টি node-এর একটি শিকলের জন্য যথেষ্ট কি না, হিসাব করে দেখাও। ২. root-এর parent আমি কী রেখেছি, আর তাতে কোনো লাফ অ্যারের বাইরে যায় কি না, বলো। ৩. এমন ৩টি ছোট প্রশ্ন দাও (একজন অন্যজনের পূর্বপুরুষ, u = v, আলাদা depth), যেখানে এ ধরনের কোড সাধারণত ভুল করে — উত্তর বলো না, আমি নিজে trace করব। ৪. আমার কোডের time আর space complexity বলো, আর প্রতিটি দাবির কারণ এক লাইনে দাও।

LCA আর diameter-এর কোড যাচাইয়ের চেকলিস্ট

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

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

প্র ০১LCA প্রশ্নের O(1) সমাধান খুঁজতে গবেষকদের কয়েক দশক লেগেছিল — এই ইতিহাস থেকে “তাত্ত্বিক সেরা” আর “ব্যবহারে সেরা”-র পার্থক্য সম্পর্কে কী শেখা যায়?

LCA প্রশ্নটি একটি আনুষ্ঠানিক অ্যালগরিদমিক সমস্যা হিসেবে আলোচিত হয় ১৯৭০-এর দশকের শুরুতে। Alfred Aho, John Hopcroft আর Jeffrey Ullman ১৯৭৩ সালে “On finding lowest common ancestors in trees” নামে একটি গবেষণাপত্রে প্রশ্নটির কয়েকটি রূপ বিশ্লেষণ করেন। ১৯৭৯ সালে Robert Tarjan দেখান, সব প্রশ্ন আগে জানা থাকলে union-find দিয়ে প্রায় রৈখিক সময়ে সব উত্তর পাওয়া যায় — অংশ ৭-এর offline পদ্ধতি।

কিন্তু online রূপে — প্রশ্ন একটি একটি করে আসবে, আর প্রতিটির উত্তর সাথে সাথে চাই — রৈখিক প্রস্তুতি আর ধ্রুবক সময়ের প্রশ্ন একসাথে পাওয়া সম্ভব কি না, তা পরিষ্কার হয় ১৯৮৪ সালে, যখন Dov Harel আর Robert Tarjan দেখান যে এটি সম্ভব। তবে তাঁদের পদ্ধতি এতই জটিল ছিল যে বাস্তবে প্রায় কেউ লিখতেন না। ১৯৮৮ সালে Baruch Schieber আর Uzi Vishkin একটি সরলতর পদ্ধতি দেন। শেষে ২০০০ সালে Michael Bender আর Martín Farach-Colton “The LCA Problem Revisited” প্রবন্ধে দেখান, Euler tour দিয়ে LCA-কে RMQ-তে নামিয়ে, আর পাশাপাশি দুটি depth-এর পার্থক্য সব সময় ±1 — এই বৈশিষ্ট্য কাজে লাগিয়ে ছোট ব্লকে ভাগ করলে, তুলনামূলক সহজ কোডে O(n) প্রস্তুতি আর O(1) প্রশ্ন পাওয়া যায়।

মজার ব্যাপার, কনটেস্টে আজও সবচেয়ে বেশি লেখা হয় এদের কোনোটিই নয় — binary lifting, যার প্রস্তুতি O(n log n) আর প্রশ্ন O(log n), অর্থাৎ তাত্ত্বিকভাবে “খারাপ”. কারণ এটি ১৫–২০ লাইনে নির্ভুল লেখা যায়, k-তম পূর্বপুরুষ আর পথের max/min একই টেবিলে দেয়, আর n = 2·105-এ log n মাত্র ১৮ — বাস্তবে পার্থক্য সামান্য। অন্যদিকে বিশাল phylogenetic tree বা জিনোম-ডেটার মতো জায়গায়, যেখানে কোটি কোটি প্রশ্ন আর মেমরি সীমিত, সেখানে রৈখিক-জায়গার পদ্ধতিগুলোর গুরুত্ব আছে।

মূল উপলব্ধি: তাত্ত্বিক সেরা বাউন্ড প্রথমে প্রমাণ করে “এটি সম্ভব”, তারপর বছরের পর বছর সরলীকরণে সেটি ব্যবহারযোগ্য হয়। বাস্তবে বেছে নিন সেটি, যা আপনার ইনপুটের আকার, সময়সীমা আর নির্ভুল লেখার সামর্থ্যের সাথে মেলে।

প্র ০২“দ্বিগুণ করা” (doubling) কৌশল binary lifting ছাড়া আর কোথায় কোথায় আসে — sparse table, binary exponentiation, skip list আর সমান্তরাল কম্পিউটিংয়ে?

Binary lifting-এর মূল কথা দুটি: (১) 2k ধাপের ফল বানানো যায় 2k−1 ধাপের দুটি ফল জুড়ে, আর (২) যেকোনো সংখ্যা দুইয়ের ঘাতের যোগফল। এই দুটি ধারণা এই কোর্সে বারবার এসেছে। পর্ব ০৭-এর binary exponentiation-এ a2k = (ak)2, আর a13 = a8 · a4 · a1 — ঠিক k-তম পূর্বপুরুষের মতো bit ধরে ধরে গুণ। পর্ব ৩৭-এর sparse table-এ দৈর্ঘ্য 2k-এর অংশের উত্তর দুটি 2k−1 অংশ থেকে। পর্ব ৩৬-এর Fenwick tree-তে k-তম উপাদান খোঁজাকেও “BIT-এর ওপর binary lifting” বলা হয়, আর পর্ব ৩৮-এর skip list আসলে এলোমেলোভাবে বানানো লাফের সিঁড়ি — ওপরের স্তরে বড় লাফ, নিচে ছোট।

কনটেস্টে এর একটি জনপ্রিয় প্রয়োগ হলো functional graph: প্রতিটি node থেকে ঠিক একটি তীর (যেমন “এই শহর থেকে পরের দিন কোন শহরে যাব”). প্রশ্ন: “x থেকে শুরু করে 1018 দিন পর কোথায়?” এখানে চক্র থাকতে পারে, তাই tree নয়, কিন্তু next[k][v] = next[k−1][next[k−1][v]] টেবিল ঠিক একইভাবে বানানো যায়, আর ৬০টি লাফে উত্তর মেলে।

সমান্তরাল অ্যালগরিদমে একই ধারণার নাম pointer jumping. একটি linked list-এর প্রতিটি node-কে তার শেষ পর্যন্ত দূরত্ব জানাতে (list ranking), সব প্রসেসর একসাথে নিজের next-কে next.next দিয়ে বদলায় — প্রতিটি রাউন্ডে লাফের দৈর্ঘ্য দ্বিগুণ, তাই log n রাউন্ডে কাজ শেষ। James Wyllie-র ১৯৭৯ সালের গবেষণায় এই কৌশলটি সমান্তরাল কম্পিউটিংয়ের আলোচনায় পরিচিত হয়ে ওঠে। আজ GPU-তে গ্রাফের কিছু অ্যালগরিদমে এর রূপান্তর ব্যবহৃত হয়।

লক্ষণীয়, প্রতিটি ক্ষেত্রে মূল্য একই — Θ(n log n) জায়গার একটি টেবিল (বা log n রাউন্ড), আর লাভও একই — লম্বা পথ log সংখ্যক ধাপে পার। অংশ ৫-এর প্রমাণে দেখেছেন, “বড় থেকে ছোট লাফ, সীমা না ছাড়ালে নাও” আসলে binary search-এরই রূপ।

মূল উপলব্ধি: Binary lifting কোনো আলাদা কৌশল নয়, বরং “দ্বিগুণ করো, তারপর bit ধরে জোড়ো” — এই সার্বজনীন ছাঁচের tree-সংস্করণ। ছাঁচটি চিনলে নতুন সমস্যায়ও লাফের টেবিল বানানোর সুযোগ চোখে পড়বে।

প্র ০৩নেটওয়ার্ক কেন ইচ্ছা করে tree আকারে চালানো হয়, আর সেই tree-তে LCA, দূরত্ব আর কেন্দ্রের প্রশ্নগুলো বাস্তবে কোথায় আসে?

একটি অফিসের Ethernet নেটওয়ার্কে নির্ভরযোগ্যতার জন্য switch-গুলোকে প্রায়ই একাধিক পথে জোড়া হয়। কিন্তু Ethernet frame-এ “কত ধাপ পেরিয়েছে” গোনার ব্যবস্থা নেই, তাই চক্র থাকলে একটি broadcast অনন্তকাল ঘুরতে পারে আর পুরো নেটওয়ার্ক অচল করে দিতে পারে। এর সমাধান হিসেবে Radia Perlman ১৯৮০-এর দশকে Digital Equipment Corporation-এ কাজ করার সময় Spanning Tree Protocol উদ্ভাবন করেন: switch-গুলো নিজেরা আলোচনা করে একটি root বেছে নেয়, আর কিছু লিংক বন্ধ রেখে বাকিগুলোকে একটি spanning tree-তে নামায়। ফলে প্যাকেট আসলে একটি tree-র ওপর চলে — আর দুই যন্ত্রের পথ মানে tree-তে LCA দিয়ে যাওয়া পথ।

এই tree-তে প্রশ্নগুলো এই পর্বেরই। দুই যন্ত্রের মাঝে মোট latency — ওজনসহ দূরত্ব, rootDist[u] + rootDist[v] − 2·rootDist[LCA]. পথের সবচেয়ে কম bandwidth-এর লিংক — mx টেবিলের min সংস্করণ। একটি নতুন সার্ভার কোথায় বসালে সবচেয়ে দূরের যন্ত্রও দ্রুত পৌঁছায় — tree-র কেন্দ্র, যা diameter-এর মাঝখানে। Content delivery network বা multicast-এর বিতরণ-tree-তেও একই ধরনের হিসাব আসে।

অন্য দিকে ফাইল সিস্টেমও একটি tree: cd ../../music-এর মতো আপেক্ষিক ঠিকানা বানাতে দুই ফোল্ডারের LCA খুঁজতে হয়। Git-এর git merge-base দুই branch-এর সবচেয়ে ভালো সাধারণ পূর্বপুরুষ commit খোঁজে — তবে commit-এর ইতিহাস tree নয়, একটি DAG (পর্ব ৪২), কারণ merge commit-এর দুটি parent থাকে; তাই সেখানে একাধিক “সবচেয়ে কাছের” সাধারণ পূর্বপুরুষ থাকতে পারে, আর এই পর্বের অ্যালগরিদম সরাসরি খাটে না। জীববিজ্ঞানে বিবর্তনের tree-তে দুই প্রজাতির “most recent common ancestor” — হুবহু LCA.

মূল উপলব্ধি: বাস্তব ব্যবস্থায় tree প্রায়ই আপনাআপনি আসে না — চক্রের বিপদ এড়াতে ইচ্ছা করে বানানো হয়। আর tree বানানোর পর তার ওপর পথের প্রশ্নগুলো সব LCA-তে নেমে আসে। তবে কাঠামোটি সত্যিই tree কি না (না DAG), আগে যাচাই করুন।

প্র ০৪দুইবার BFS tree-তে diameter দেয়, কিন্তু সাধারণ graph-এ কেন দেয় না? আর graph-এর diameter বের করা কেন এত কঠিন বলে মনে করা হয়?

অংশ ৯-এর প্রমাণ দুটি তথ্যের ওপর দাঁড়িয়ে: tree-তে দুই node-এর মধ্যে পথ ঠিক একটি, আর দুটি পথ যেখানে মেলে, সেখান থেকে দূরত্বগুলো সরাসরি যোগ-বিয়োগ করা যায়। চক্র এলে দুটিই ভাঙে। একটি সরল উদাহরণ: ছয়টি node-এর একটি বৃত্ত নিন — তার diameter 3 (বিপরীত দুই node). যেকোনো node থেকে শুরু করলে সবচেয়ে দূরের node-টি বিপরীত দিকে, দূরত্ব 3, তাই এখানে দুইবার BFS সঠিক উত্তরই দেয়। কিন্তু বৃত্তের সাথে লেজ ও শাখা জুড়ে এমন graph বানানো যায়, যেখানে প্রথম BFS-এর সবচেয়ে দূরের node কোনো diameter-এর প্রান্ত নয়, আর দ্বিতীয় BFS আসল diameter-এর চেয়ে ছোট সংখ্যা দেয়। তাই সাধারণ graph-এ দুইবার BFS শুধু একটি নিম্নসীমা দেয়: ফলটি diameter-এর চেয়ে বড় হতে পারে না, আর সহজ যুক্তিতে দেখানো যায় এটি অন্তত diameter-এর অর্ধেক।

সাধারণ unweighted graph-এ নির্ভুল diameter-এর সরল উপায় হলো প্রতিটি node থেকে BFS — O(V·(V + E)), বিরল graph-এ প্রায় O(V²). এর চেয়ে মৌলিকভাবে দ্রুত কিছু পাওয়া কঠিন বলে বিশ্বাস করা হয়: ২০১৩ সালে Liam Roditty আর Virginia Vassilevska Williams দেখান, বিরল graph-এ diameter 2 না 3, শুধু এটুকু আলাদা করার একটি “সত্যিই উপ-দ্বিঘাত” অ্যালগরিদম পেলে Strong Exponential Time Hypothesis নামের একটি বহুল-বিশ্বাসিত অনুমান ভুল প্রমাণিত হবে। অর্থাৎ যে সমস্যা tree-তে দুটি BFS-এ O(n), সেটিই চক্র এলে সূক্ষ্ম জটিলতা-তত্ত্বের (fine-grained complexity) কেন্দ্রীয় প্রশ্ন।

বাস্তবে সোশ্যাল নেটওয়ার্ক বা ওয়েব-graph-এর মতো বিশাল graph-এ diameter-এর আনুমানিক মান বের করতে দুইবার বা কয়েকবার BFS-ই চালানো হয় — বারবার “সবচেয়ে দূরের node থেকে আবার” — কারণ এটি দ্রুত একটি ভালো নিম্নসীমা দেয়, আর অনেক বাস্তব graph-এ উত্তরের খুব কাছে পৌঁছায়। কিন্তু তখন সেটিকে “নিম্নসীমা” বা “আনুমানিক” বলতে হয়, নির্ভুল নয়।

মূল উপলব্ধি: একটি অ্যালগরিদমের প্রমাণ যে কাঠামোর ওপর দাঁড়িয়ে (এখানে “পথ একটিই”), সেই কাঠামো সরে গেলে অ্যালগরিদমের দাবিও সরে যায়। প্রমাণ জানলে বোঝা যায়, পদ্ধতিটি কোথায় খাটবে আর কোথায় শুধু আনুমানিক।

প্র ০৫Heavy-light আর centroid decomposition কীভাবে যেকোনো tree-কে “log n”-এর সীমায় বেঁধে ফেলে — আর কনটেস্টে কখন এগুলোর দরকার, কখন binary lifting-ই যথেষ্ট?

Tree-র প্রশ্নে বিপদ একটিই: উচ্চতা n − 1 পর্যন্ত হতে পারে। Binary lifting এর সমাধান দেয় লাফের টেবিলে, কিন্তু টেবিলটি স্থির — পথের edge-এর ওজন বদলাতে থাকলে mx টেবিল আবার বানাতে হয়। দুটি decomposition এই সমস্যা অন্যভাবে মেটায়।

Heavy-light decomposition-এর ধারণা এসেছে Daniel Sleator আর Robert Tarjan-এর ১৯৮৩ সালের link-cut tree-র গবেষণা থেকে। প্রতিটি node থেকে সবচেয়ে বড় subtree-র সন্তানের edge-কে বলুন “ভারী”, বাকিগুলো “হালকা”. একটি হালকা edge ধরে নিচে নামলে subtree-র আকার অন্তত অর্ধেক হয়ে যায়, তাই root থেকে যেকোনো পথে হালকা edge বড়জোর log₂ n টি। ভারী edge-গুলো মিলে লম্বা শিকল তৈরি করে, আর DFS-এ ভারী সন্তানকে আগে দেখলে প্রতিটি শিকল tin ক্রমে একটানা থাকে। ফলে যেকোনো পথ O(log n) টি একটানা রেঞ্জে ভাঙে, আর প্রতিটি রেঞ্জে segment tree — পথে update আর query দুটোই O(log² n).

Centroid decomposition-এর ভিত্তি আরও পুরোনো: ১৮৬৯ সালে Camille Jordan প্রমাণ করেন, প্রতিটি tree-তে একটি বা দুটি centroid থাকে — যে node সরালে কোনো টুকরোয় n/2-এর বেশি node থাকে না। centroid সরিয়ে প্রতিটি টুকরোয় আবার একই কাজ করলে একটি নতুন “centroid tree” পাওয়া যায়, যার উচ্চতা O(log n). মূল tree-র প্রতিটি পথের জন্য ঠিক একটি centroid আছে, যাকে সরানোর সময় পথটি প্রথমবার ভাঙে — পথটি সেই centroid দিয়ে যায়, আর তার টুকরোর ভেতরেই থাকে। তাই প্রতিটি পথকে ঠিক একবার, সেই centroid-এর হিসাবে গোনা যায়, আর “দূরত্ব ঠিক k এমন কয়টি জোড়া” বা “প্রতিটি node-এর কাছের লাল node” ধরনের প্রশ্ন প্রতিটি centroid-এ আলাদা করে গোনা যায়।

কখন কোনটি? প্রশ্নে শুধু LCA, দূরত্ব, k-তম পূর্বপুরুষ বা স্থির ওজনের পথ-max থাকলে binary lifting যথেষ্ট আর সবচেয়ে কম ঝুঁকির। Subtree-তে update হলে entry/exit time + segment tree. পথে update থাকলে heavy-light. আর সব জোড়া পথের ওপর গোনার প্রশ্নে centroid. কোড দীর্ঘ হয় এই ক্রমেই, তাই সহজটি দিয়ে কাজ হলে কঠিনটিতে যাবেন না।

মূল উপলব্ধি: তিনটি কৌশলই একই প্রতিশ্রুতি দেয় — যেকোনো আকারের tree-কে log n গভীরতার কাঠামোয় আনা — কিন্তু ভিন্ন পথে: লাফের টেবিল, ভারী শিকল, আর অর্ধেক করে ভাঙা।

অনুশীলন

খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।

  1. হাতে trace করুন: ৯টি node-এর একটি tree-র parent: par[2] = par[3] = 1, par[4] = par[5] = 2, par[6] = 3, par[7] = 4, par[8] = 7, par[9] = 6; root 1. up[0], up[1], up[2] সারি লিখুন (root-এর parent root নিজে), তারপর binary lifting দিয়ে LCA(8, 5) আর LCA(8, 9) বের করুন, সাথে দুটির দূরত্ব।

    depth: 1:0, 2:1, 3:1, 4:2, 5:2, 6:2, 7:3, 8:4, 9:3. up[0] (v = 1…9): 1 1 1 2 2 3 4 7 6. up[1]: 1 1 1 1 1 1 2 4 3. up[2]: সবই 1 (যেমন up[2][8] = up[1][up[1][8]] = up[1][4] = 1).

    LCA(8, 5): depth 4 আর 2, পার্থক্য 2 = 10₂ → k = 1-এ লাফ: u = up[1][8] = 4. এখন 4 আর 5, আলাদা। k = 2: 1 = 1, লাফ নয়; k = 1: 1 = 1, লাফ নয়; k = 0: up[0][4] = 2 = up[0][5], লাফ নয়। উত্তর up[0][4] = 2. দূরত্ব 4 + 2 − 2·1 = 4 (8 → 7 → 4 → 2 → 5).

    LCA(8, 9): depth 4 আর 3, পার্থক্য 1 → u = up[0][8] = 7. 7 আর 9: k = 2: 1 = 1; k = 1: up[1][7] = 2, up[1][9] = 3 — আলাদা, লাফ: u = 2, v = 3. k = 0: 1 = 1, লাফ নয়। উত্তর up[0][2] = 1. দূরত্ব 4 + 3 − 0 = 7.

  2. Entry/exit time: অনুশীলন ১-এর tree-তে root 1 থেকে DFS করুন (সন্তান ছোট থেকে বড় ক্রমে)। প্রতিটি node-এর tin আর tout লিখুন। subtree(2) কোন রেঞ্জ? tin/tout দেখে বলুন 4 কি 8-এর পূর্বপুরুষ, আর 3 কি 8-এর?

    DFS ক্রম: 1, 2, 4, 7, 8, 5, 3, 6, 9 → tin যথাক্রমে 0 থেকে 8. tout: 8 → 4, 7 → 4, 4 → 4, 5 → 5, 2 → 5, 9 → 8, 6 → 8, 3 → 8, 1 → 8.

    subtree(2) = রেঞ্জ [1, 5] = node 2, 4, 7, 8, 5. 4 কি 8-এর পূর্বপুরুষ? tin[4] = 2 ≤ tin[8] = 4 আর tout[8] = 4 ≤ tout[4] = 4 — হ্যাঁ। 3? tin[3] = 6 > 4 — না।

  3. হিসাব করুন: n = 2·105 node আর q = 2·105 প্রশ্ন। (ক) LOG কত নেবেন, আর int-এর up টেবিল কত মেমরি নেবে? (খ) tree একটি শিকল হলে সরল LCA আর binary lifting-এ মোটামুটি কত ধাপ লাগবে? (গ) Euler tour-এর ওপর sparse table-এ কয়টি সারি আর কত ঘর?

    (ক) 217 = 131072 < 2·105 < 218 = 262144, তাই LOG = 18. ঘর 18 × 200001 ≈ 3.6·106, প্রতিটি ৪ byte — প্রায় ১৪.৪ MB. সাধারণ ২৫৬ MB সীমায় চলে।

    (খ) শিকলে depth n − 1 পর্যন্ত, তাই সরল পদ্ধতিতে প্রতি প্রশ্নে বড়জোর প্রায় 2·105 ধাপ, মোট প্রায় 4·1010 — সময়সীমার অনেক বাইরে। Binary lifting-এ প্রতি প্রশ্নে 2 × 18 = 36 পরীক্ষা, মোট প্রায় 7.2·106.

    (গ) Tour-এর দৈর্ঘ্য 2n − 1 = 399999. 218 < 399999 < 219, তাই সারি k = 0 থেকে 18 — ১৯টি, ঘর প্রায় 19 × 4·105 ≈ 7.6·106. প্রশ্ন দ্রুততর (O(1)), কিন্তু মেমরি binary lifting-এর প্রায় দ্বিগুণ।

  4. কোড লিখুন: প্রতিটি edge-এর একটি ধনাত্মক ওজন আছে (w[v] = v আর তার parent-এর মাঝের edge). binary lifting দিয়ে একটি ফাংশন pathMax(u, v) লিখুন, যা u থেকে v-এর পথের সবচেয়ে ভারী edge-এর ওজন দেয় (u = v হলে 0). Time ও space complexity বলুন।

    up-এর পাশে mx[k][v] = v থেকে 2k ধাপ ওপরে যেতে পার হওয়া edge-গুলোর সর্বোচ্চ। প্রতিটি লাফের আগে সেই লাফের mx উত্তরে যোগ করুন:

    // mx[0][v] = w[v] (root: 0);  mx[k][v] = max(mx[k-1][v], mx[k-1][up[k-1][v]])
    int pathMax(int u, int v) {
        int best = 0;
        if (depth[u] < depth[v]) swap(u, v);
        for (int k = LOG - 1; k >= 0; --k)
            if (depth[u] - (1 << k) >= depth[v]) { best = max(best, mx[k][u]); u = up[k][u]; }
        if (u == v) return best;
        for (int k = LOG - 1; k >= 0; --k)
            if (up[k][u] != up[k][v]) {
                best = max({best, mx[k][u], mx[k][v]});
                u = up[k][u]; v = up[k][v];
            }
        return max({best, mx[0][u], mx[0][v]});   // the two edges into the LCA
    }

    প্রতিটি লাফ ঠিক যে edge-গুলো পেরোয়, সেগুলোর max-ই যোগ হয়; সব লাফ মিলে u → LCA আর v → LCA পথ ঠিক একবার করে ঢাকে, শেষ লাইনে LCA-তে ঢোকার দুটি edge. প্রস্তুতি O(n log n) সময় ও জায়গা (দুটি টেবিল), প্রতি প্রশ্ন O(log n). যাচাই: ছোট এলোমেলো tree-তে প্রতিটি জোড়ার জন্য পথ ধরে হেঁটে সরল max-এর সাথে মিলিয়ে নিন।

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

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

ক্রমup টেবিল তৈরি থাকার পর binary lifting দিয়ে LCA(u, v) বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. দরকার হলে u আর v অদলবদল করা, যাতে u বেশি গভীর হয়
  2. বড় থেকে ছোট k-তে লাফ দিয়ে u-কে v-এর depth-এ তোলা
  3. এখন u = v হলে সেটিই উত্তর হিসেবে ফেরত দেওয়া
  4. বড় থেকে ছোট k-তে, up[k][u] ≠ up[k][v] হলে দুজনকেই লাফ দেওয়া
  5. শেষে up[0][u] — অর্থাৎ u-এর parent — উত্তর হিসেবে ফেরত দেওয়া

Binary lifting-এ up[k][v] বলতে v-এর 2k-তম পূর্বপুরুষ বোঝায়। up[k][v] বানানোর সঠিক সূত্র কোনটি?

  • up[k][v] = up[k − 1][up[0][v]]
  • up[k][v] = up[k − 1][v] + 1
  • up[k][v] = up[k − 1][up[k − 1][v]]
  • up[k][v] = up[0][up[0][v]]

n টি node-এর একটি tree-তে binary lifting-এর টেবিল বানাতে আর তারপর q টি LCA প্রশ্নের উত্তর দিতে মোট সময় কত?

  • O(n + q)
  • O((n + q) log n)
  • O(n · q)
  • O(q · n log n)

একটি rooted tree-তে depth[u] = 6, depth[v] = 4, আর LCA(u, v)-এর depth 1. u আর v-এর দূরত্ব (edge-এ) কত?

  • 2
  • 10
  • 9
  • 8

Binary lifting-এ LCA খোঁজার দ্বিতীয় phase-এ (u আর v একই depth-এ, আলাদা) কোনো k-তে up[k][u] = up[k][v] হলে কী করা উচিত?

  • লাফ না দিয়ে পরের ছোট k-তে যাওয়া
  • সেটিকেই LCA হিসেবে ফেরত দেওয়া
  • দুজনকেই লাফ দেওয়া
  • শুধু u-কে লাফ দেওয়া

DFS-এ প্রতিটি node-এর entry time tin আর exit time tout (subtree-র সবচেয়ে বড় tin) রাখা হলো। কোন শর্তে u হলো v-এর পূর্বপুরুষ (u = v-সহ)?

  • tin[u] < tin[v]
  • depth[u] < depth[v]
  • tin[u] ≤ tin[v] এবং tout[v] ≤ tout[u]
  • tout[u] < tin[v]

একটি tree-তে (সব edge-এর দৈর্ঘ্য 1) node s থেকে BFS চালিয়ে সবচেয়ে দূরের node a পাওয়া গেল, তারপর a থেকে BFS চালিয়ে সবচেয়ে দূরের node b. কোন কথাটি সব সময় সত্য?

  • d(s, a) = diameter
  • d(a, b) = diameter
  • s সব সময় diameter-এর একটি প্রান্ত
  • পদ্ধতিটি যেকোনো connected graph-এ diameter দেয়

শব্দকোষ

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

মনে রাখুন

  1. Edge-তালিকা থেকে tree ঝোলাতে root থেকে BFS: আবিষ্কারক node-ই parent, depth = parent-এর depth + 1 — O(n), recursion ছাড়া।
  2. up[k][v] = up[k−1][up[k−1][v]], root-এর parent root নিজে, 2LOG > n. টেবিল Θ(n log n); k-তম পূর্বপুরুষ = k-এর bit ধরে লাফ, O(log n).
  3. LCA: আগে সমান depth, তারপর বড় থেকে ছোট k-তে আলাদা হলে লাফ, শেষে parent. দূরত্ব = depth[u] + depth[v] − 2·depth[LCA]; পথের max/min — পাশের mx টেবিলে।
  4. Euler tour + sparse table-এ LCA O(1); সব প্রশ্ন আগে জানা থাকলে Tarjan-এর offline DSU. tin/tout-এ subtree = রেঞ্জ, তার ওপর BIT বা segment tree.
  5. Diameter: যেকোনো node থেকে সবচেয়ে দূরের a, তারপর a থেকে সবচেয়ে দূরের b — দুটি BFS, O(n); শুধু tree আর অঋণাত্মক ওজনে। কেন্দ্র diameter-এর মাঝখানে।

পরের পর্বে: মডিউল ৬-এর গ্রাফ আর tree শেষ — এবার অ্যালগরিদম ডিজাইনের কৌশল। শুরু divide and conquer দিয়ে: Karatsuba-র গুণ, Strassen-এর matrix গুণ আর সবচেয়ে কাছের দুই বিন্দু — সমস্যা ভাঙা, সমাধান জোড়া, আর master theorem দিয়ে তার হিসাব।

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

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