এই পর্বে যা শিখবেন
- Range query আর point update মেশানো সমস্যা চেনা, আর prefix sum কোথায় আটকে যায় তা বোঝা
- Segment tree হাতে আঁকা, অ্যারেতে রাখা (2v, 2v + 1), আর 4n ঘর কেন যথেষ্ট তা প্রমাণ করা
- Build, point update আর range query হাতে trace করা, আর “প্রতি স্তরে ≤ ৪ node” প্রমাণ লেখা
- যোগের জায়গায় min, max, gcd, XOR বা struct বসিয়ে নতুন প্রশ্নের tree বানানো
- Iterative bottom-up tree আর tree-র ওপর descent লেখা, আর Fenwick tree ও sparse table-এর সাথে তুলনা করে সঠিকটি বেছে নেওয়া
১ · সমস্যা — দোকানের হিসাব, প্রশ্ন আর সংশোধন একসাথে
ঢাকার একটি সুপারশপ চেইনের আটটি শাখা — মিরপুর থেকে শুরু করে যাত্রাবাড়ী পর্যন্ত, ক্রমানুসারে ০ থেকে ৭ নম্বর। প্রতিটি শাখার আজকের বিক্রি (লাখ টাকায়) একটি অ্যারেতে: a = [5, 8, 6, 3, 2, 7, 2, 6]. হেড অফিসের ম্যানেজার সারাদিন দুই ধরনের কাজ করেন। প্রশ্ন: “১ থেকে ৬ নম্বর শাখার মোট বিক্রি কত?” আর সংশোধন: “৫ নম্বর শাখার হিসাবে ভুল ছিল, ৭ নয়, আসলে ১০”. এক দিনে হাজার হাজার প্রশ্ন আর সংশোধন মিশে আসে, আর শাখা যদি আটটির বদলে দশ লাখটি হিসাবের ঘর হয় (প্রতি মিনিটের বিক্রি, প্রতিটি পণ্যের মজুদ), তখন প্রতিটি কাজ দ্রুত হওয়া চাই।
এই ধরনের প্রশ্নের নাম range query: অ্যারের একটি অবিচ্ছিন্ন অংশ a[l..r] নিয়ে প্রশ্ন — যোগফল, সর্বনিম্ন (min), সর্বোচ্চ (max). আর একটি ঘরের মান বদলানো হলো point update. দুটি সরল সমাধান আমরা জানি, আর দুটিরই একটি করে দুর্বলতা:
| পদ্ধতি | প্রস্তুতি | রেঞ্জের প্রশ্ন | একটি ঘর বদল | q টি মিশ্র কাজ |
|---|---|---|---|---|
| সরাসরি লুপ | — | O(n) | O(1) | O(qn) |
| Prefix sum (পর্ব ১৭) | O(n) | O(1) | O(n) | O(qn) |
| Segment tree | O(n) | O(log n) | O(log n) | O(n + q log n) |
Prefix sum (পর্ব ১৭) প্রশ্নে অসাধারণ: P[r + 1] − P[l], এক বিয়োগ। কিন্তু a[5] বদলালে P[6], P[7], P[8] — পরের সব prefix বদলে যায়, worst case-এ O(n). উল্টো দিকে সরাসরি লুপে বদল O(1), কিন্তু প্রশ্নে O(n). দুই ক্ষেত্রেই n = q = 105 হলে প্রায় 1010 ধাপ — কয়েক মিনিটের কাজ, যেখানে সময়সীমা এক সেকেন্ড। তার ওপর min আর max-এ prefix কৌশল কাজই করে না, কারণ min-এর কোনো “উল্টো” নেই: [0..7]-এর min আর [0..2]-এর min জেনে [3..7]-এর min বের করা যায় না।
উপমা — হেড অফিসের হিসাবের স্তর। চেইনটি আঞ্চলিকভাবে সাজানো: প্রতি দুই শাখার একজন এলাকা-ম্যানেজার, প্রতি দুই এলাকার একজন জোন-ম্যানেজার, আর সবার ওপরে হেড অফিস। প্রত্যেক ম্যানেজার শুধু নিজের অধীনের মোট বিক্রি জানেন। “১ থেকে ৬ নম্বর শাখা” জানতে আটজনকে আলাদা ফোন না করে কয়েকজন ম্যানেজারের মোট যোগ করলেই চলে। আর একটি শাখার হিসাব বদলালে শুধু তার এলাকা-ম্যানেজার, জোন-ম্যানেজার আর হেড অফিস — এই কয়জনের খাতা ঠিক করতে হয়। এটিই segment tree.
লক্ষ্য তাই একটি মাঝামাঝি ভারসাম্য: প্রশ্ন আর বদল দুটোই O(log n). এমন একটি ভারসাম্যের সরল রূপ হলো অ্যারেকে √n আকারের খণ্ডে ভাগ করা (sqrt decomposition, পর্ব ৩৭) — বদল O(1), প্রশ্ন O(√n). খণ্ডের ভেতরে আবার খণ্ড, তার ভেতরে আবার — এভাবে ভাগ করতে থাকলে আমরা পাই একটি tree, আর √n নেমে আসে log n-এ।
২ · অংশের tree — প্রতিটি node একটি রেঞ্জের হিসাব রাখে
Segment tree একটি binary tree (পর্ব ২৬), যার প্রতিটি node অ্যারের একটি অবিচ্ছিন্ন অংশের (segment) দায়িত্বে। নিয়ম তিনটি:
- Root-এর দায়িত্ব পুরো অ্যারে, [0, n − 1].
- যে node-এর দায়িত্ব [tl, tr] আর tl < tr, তার মাঝখান tm = ⌊(tl + tr)/2⌋; বাঁ সন্তান [tl, tm], ডান সন্তান [tm + 1, tr]. এই অর্থে এটি divide and conquer-এর ভাগাভাগি, যা merge sort-এও দেখেছেন (পর্ব ২১)।
- tl = tr হলে node-টি একটি leaf — ঠিক একটি ঘর a[tl]. আর প্রতিটি internal node-এর মান = দুই সন্তানের মানের যোগ।
আমাদের আট শাখার অ্যারে দিয়ে নিচ থেকে ওপরে tree-টি বানিয়ে দেখুন। প্রতিটি node-এর ওপরের লাইনে তার রেঞ্জ ([2-3] মানে index 2 থেকে 3), নিচের লাইনে সেই রেঞ্জের যোগফল:
| এই ধাপে | |
| নতুন হিসাব | |
| এ পর্যন্ত মোট node |
- শুরু. আট শাখার বিক্রি
a = [5, 8, 6, 3, 2, 7, 2, 6]. Tree-র আকার আগে থেকেই ঠিক: root [0-7], তার নিচে দুই অর্ধেক, তারপর চার জোড়া, তারপর আটটি একক ঘর। মানগুলো এখনো খালি (.)। - Leaf. প্রতিটি leaf ঠিক একটি শাখা, তাই তার মান অ্যারের মানই: [0] = 5, [1] = 8, … [7] = 6. এখানে কোনো হিসাব নেই, শুধু কপি।
- জোড়ার স্তর. প্রতিটি node তার দুই সন্তানের যোগ: [0-1] = 5 + 8 = 13, [2-3] = 6 + 3 = 9, [4-5] = 2 + 7 = 9, [6-7] = 2 + 6 = 8. এরা আমাদের উপমার এলাকা-ম্যানেজার।
- অর্ধেকের স্তর. [0-3] = 13 + 9 = 22, [4-7] = 9 + 8 = 17 — দুই জোন-ম্যানেজার। খেয়াল করুন, কেউ মূল অ্যারে আবার পড়েনি; প্রত্যেকে শুধু দুই সন্তানের মান যোগ করেছে।
- Root. [0-7] = 22 + 17 = 39, পুরো চেইনের বিক্রি। মোট node 8 + 4 + 2 + 1 = 15 = 2n − 1, আর প্রতিটি node-এ একটি যোগ — তাই build O(n). নিচের লাইনে একই tree অ্যারে আকারে,
t[1..15](পরের অংশ)।
এই tree-র তিনটি বৈশিষ্ট্য পুরো পর্বের ভিত্তি। (১) উচ্চতা ছোট: প্রতি স্তরে রেঞ্জের দৈর্ঘ্য প্রায় অর্ধেক হয়, তাই ⌈log₂ n⌉ স্তর পরে দৈর্ঘ্য ১ — উচ্চতা ⌈log₂ n⌉; n = 106-এ মাত্র ২০. (২) node 2n − 1 টি: প্রতিটি internal node-এর ঠিক দুই সন্তান আর leaf n টি, তাই internal node n − 1 টি (প্রতিটি internal node দুটি অংশকে এক করে, n টি অংশ থেকে ১টিতে আসতে n − 1 বার জোড়া)। (৩) একই স্তরের node-গুলোর রেঞ্জ পরস্পর-বিচ্ছিন্ন, আর মিলে সেই স্তরের পুরো অংশ ঢেকে দেয় — query-র প্রমাণে এটিই কাজে লাগবে।
n যদি 2-এর ঘাত না হয়? নিয়ম একই থাকে, শুধু tree আর নিখুঁত (perfect) থাকে না। যেমন n = 6-এ root [0-5], সন্তান [0-2] আর [3-5]; [0-2]-এর সন্তান [0-1] আর [2-2] — একটি leaf এক স্তর আগেই থেমে যায়। উচ্চতা তবু ⌈log₂ 6⌉ = 3, আর node তবু 2·6 − 1 = 11.
৩ · অ্যারেতে tree — index 2v আর 2v + 1, আর 4n কেন যথেষ্ট
Pointer দিয়ে node বানানো যায়, কিন্তু segment tree-র আকার কখনো বদলায় না, তাই একটি সরল অ্যারেই যথেষ্ট — heap-এর মতো (পর্ব ২৩)। 1-based নিয়ম: root-এর index 1; node v-এর বাঁ সন্তান 2v, ডান সন্তান 2v + 1, parent ⌊v/2⌋. আগের ডায়াগ্রামের tree তাই অ্যারেতে এভাবে বসে:
| স্তর | index v | রেঞ্জ | t[v] |
|---|---|---|---|
| ০ | 1 | [0-7] | 39 |
| ১ | 2, 3 | [0-3], [4-7] | 22, 17 |
| ২ | 4, 5, 6, 7 | [0-1], [2-3], [4-5], [6-7] | 13, 9, 9, 8 |
| ৩ (leaf) | 8 … 15 | [0] … [7] | 5, 8, 6, 3, 2, 7, 2, 6 |
অনেকে 0-based নিয়ম পছন্দ করেন: root 0, সন্তান 2v + 1 আর 2v + 2, parent ⌊(v − 1)/2⌋. দুটিই ঠিক; শুধু একটি কোডে দুটি মেশাবেন না। 1-based-এ গুণ ২ মানে এক bit বাঁয়ে সরানো (v << 1, v << 1 | 1), আর parent মানে v >> 1 — এজন্য কনটেস্টে 1-based বেশি চলে। এই পর্বের সব কোড 1-based tree আর 0-based অ্যারে index ব্যবহার করে।
কত ঘর লাগবে — 4n-এর প্রমাণ
Node 2n − 1 টি, কিন্তু 2v/2v + 1 নিয়মে index-গুলো ফাঁকা ফাঁকা পড়ে যখন n 2-এর ঘাত নয়। n = 6-এ ব্যবহৃত index 1–9, 12, 13 — সবচেয়ে বড় 13, অথচ 2n = 12. তাই 2n ঘর সবসময় যথেষ্ট নয়। দাবি: 4n ঘর (index 0 থেকে 4n − 1) সবসময় যথেষ্ট। যুক্তি: উচ্চতা h = ⌈log₂ n⌉, আর গভীরতা d-এর node-এর index 2d থেকে 2d+1 − 1-এর মধ্যে, তাই সবচেয়ে বড় index ≤ 2h+1 − 1. যেহেতু h < log₂ n + 1, তাই 2h < 2n, আর 2h+1 − 1 < 4n − 1. ∎ সীমাটি প্রায় আঁটসাঁট: n = 10-এ সবচেয়ে বড় index 25 (2.5n), আর n = 4160-এ 16257 — প্রায় 3.9n. তাই vector t(4 * n) লেখা নিয়ম; জায়গা Θ(n), কিন্তু ধ্রুবক ৪.
Pseudocode — build (1-based tree, 0-based array)
build(v, tl, tr):
if tl == tr: t[v] = a[tl]; return // leaf
tm = (tl + tr) / 2 // integer division
build(2v, tl, tm); build(2v+1, tm+1, tr) // children first
t[v] = t[2v] + t[2v+1] // then combine
call: build(1, 0, n-1)Build-এর সময়: প্রতিটি node-এ একবার ঢোকা আর একটি যোগ — মোট Θ(2n − 1) = Θ(n). Recurrence-এ লিখলে T(n) = 2T(n/2) + O(1), যার সমাধান master theorem-এ (পর্ব ০৯) Θ(n). Recursion ডায়াগ্রামের মতো স্তর ধরে হিসাব করে না, বরং postorder-এ (আগে বাঁ subtree পুরো, তারপর ডান, তারপর নিজে) — কিন্তু “সন্তান আগে, parent পরে” নিয়মটি দুই ক্রমেই মানা হয়, তাই ফল একই। Recursion-এর গভীরতা উচ্চতার সমান, ⌈log₂ n⌉ + 1 — ১০ লাখ উপাদানেও প্রায় ২১টি stack frame, কোনো ভাষাতেই চিন্তার বিষয় নয়।
৪ · Point update — শুধু leaf থেকে root পর্যন্ত একটি পথ
৫ নম্বর শাখার হিসাব ৭ থেকে ১০ হলো। কোন কোন node-এর মান বদলাবে? ঠিক সেগুলো, যাদের রেঞ্জে index 5 আছে: [5], [4-5], [4-7], [0-7]. প্রতি স্তরে ঠিক একটি — কারণ একই স্তরের রেঞ্জগুলো পরস্পর-বিচ্ছিন্ন। এরা মিলে leaf থেকে root পর্যন্ত একটি পথ। Recursive update root থেকে নামে (pos ≤ tm হলে বাঁয়ে, নইলে ডানে), leaf-এ মান বসায়, আর ফেরার পথে প্রতিটি node নতুন করে দুই সন্তানের যোগ হিসাব করে:
| এই ধাপে | |
| হিসাব | |
| বদলানো node |
- শুরু. অংশ ২-এর tree, root 39. সংশোধন: a[5] = 10. Prefix sum হলে এখানে P[6], P[7], P[8] — তিনটি ঘর বদলাতে হতো, আর n বড় হলে n টি পর্যন্ত।
- নামা. root [0-7]-এর মাঝখান tm = 3; 5 > 3, তাই ডানে [4-7] (index 3). সেখানে tm = 5; 5 ≤ 5, বাঁয়ে [4-5] (index 6). সেখানে tm = 4; 5 > 4, ডানে [5] (index 13) — leaf. চারটি node-এর পথ, মান এখনো বদলায়নি।
- Leaf.
t[13] = 10. এখন ওপরের node-গুলোর মান পুরোনো — ভুল। তাদের ঠিক করতে হবে, কিন্তু শুধু এই পথের ওপরেরগুলো। - ফেরার পথে [4-5]. recursion ফিরে এসে
t[6] = t[12] + t[13] = 2 + 10 = 12. ভাই node [6-7] বা বাঁ দিকের পুরো অর্ধেক ছোঁয়াই হয়নি। - [4-7].
t[3] = t[6] + t[7] = 12 + 8 = 20. - Root.
t[1] = t[2] + t[3] = 22 + 20 = 42— ঠিক ৩ বেশি, কারণ একটি শাখা ৩ বাড়ল। মোট চারটি node বদলাল = উচ্চতা + 1. তাই point update Θ(log n): প্রতি স্তরে একটি node আর O(1) কাজ।
একটি সূক্ষ্ম বিষয়: update-এ আমরা নতুন মান বসিয়েছি (assign). “a[5]-এ ৩ যোগ করুন” ধরনের বদলও একই পথে হয়, শুধু leaf-এ t[v] += 3. আর যোগের ক্ষেত্রে ফেরার পথে আবার হিসাব না করে পথের প্রতিটি node-এ সরাসরি ৩ যোগ করলেও চলে — কিন্তু min বা max-এ সেটি চলে না (min-এর কোনো ঘর বাড়লে min কত হবে, শুধু পুরোনো min দেখে বলা যায় না)। তাই নিরাপদ নিয়ম: leaf বদলান, তারপর ওপরের প্রতিটি node দুই সন্তান থেকে আবার হিসাব করুন।
৫ · Range query — ভেতরে, বাইরে আর আংশিক
এবার মূল প্রশ্ন: “১ থেকে ৬ নম্বর শাখার মোট বিক্রি?” (হালনাগাদ অ্যারে [5, 8, 6, 3, 2, 10, 2, 6]-এ উত্তর 8 + 6 + 3 + 2 + 10 + 2 = 31). Recursive query root থেকে শুরু করে প্রতিটি node-এ দেখে, তার রেঞ্জ [tl, tr] প্রশ্নের রেঞ্জ [l, r]-এর সাথে কীভাবে মেলে। তিনটিই সম্ভাবনা:
- পুরো বাইরে (r < tl বা tr < l): এই node-এর কিছুই লাগবে না — 0 ফেরত দিন (যোগের identity).
- পুরো ভেতরে (l ≤ tl আর tr ≤ r): node-এর জমানো মানটিই পুরোটা কাজে লাগে —
t[v]ফেরত দিন, নিচে নামার দরকার নেই। - আংশিক (বাকি সব): দুই সন্তানকে একই প্রশ্ন করুন, উত্তর দুটি যোগ করুন।
| এই ধাপে | |
| জমা যোগফল | |
| দেখা node, স্তর ০/১/২/৩ |
- Root. [0-7] আর [1, 6] — আংশিক: কিছু অংশ লাগবে, সবটা নয়। তাই 42 ফেরত দেওয়া যাবে না; দুই সন্তানের কাছে যাই। প্রথমে বাঁ দিক।
- বাঁ অর্ধেক. [0-3] আংশিক, তার বাঁ সন্তান [0-1]-ও আংশিক। [0-1]-এর দুই সন্তান: [0] পুরো বাইরে (0 < 1) → 0; [1] পুরো ভেতরে → 8. জমা 8.
- [2-3]. 1 ≤ 2 আর 3 ≤ 6 — পুরো ভেতরে। এখানেই থামি, t = 9 নিই; 6 আর 3 আলাদা করে দেখার দরকার নেই। বাঁ অর্ধেকের উত্তর 8 + 9 = 17.
- ডান অর্ধেক. [4-7] আংশিক (7 > 6). তার বাঁ সন্তান [4-5] পুরো ভেতরে → 12. জমা 29.
- [6-7]. আংশিক; [6] ভেতরে → 2, [7] বাইরে (7 > 6) → 0. ডান অর্ধেক 12 + 2 = 14.
- উত্তর 17 + 14 = 31. রেঞ্জ [1, 6] ভেঙেছে চারটি পরস্পর-বিচ্ছিন্ন node-এ: [1], [2-3], [4-5], [6]. দেখা হয়েছে ১১টি node — স্তর অনুযায়ী 1, 2, 4, 4. ছয়টি ঘর এক এক করে যোগ করার চেয়ে কম মনে না হলেও, n = 106-এ এটি সর্বোচ্চ প্রায় ৮৪টি node, আর লুপে দশ লাখ ঘর।
প্রমাণ — প্রতি স্তরে সর্বোচ্চ ৪টি node
দাবি: query(l, r) tree-র প্রতিটি স্তরে সর্বোচ্চ ৪টি node দেখে, আর তাদের মধ্যে সর্বোচ্চ ২টি আংশিক। তাই মোট দেখা node ≤ 4(⌈log₂ n⌉ + 1), সময় O(log n).
যুক্তি: একটি node আংশিক মানে তার রেঞ্জ [tl, tr]-এ প্রশ্নের ভেতরের ঘরও আছে, বাইরের ঘরও। রেঞ্জটি অবিচ্ছিন্ন, তাই তাকে প্রশ্নের একটি “সীমানা” পার হতেই হবে: হয় সে l − 1 আর l দুটোই ধরে (বাঁ সীমানা), নয়তো r আর r + 1 দুটোই ধরে (ডান সীমানা)। একই স্তরের রেঞ্জগুলো পরস্পর-বিচ্ছিন্ন, তাই প্রতিটি সীমানা ঘর-জোড়া ঐ স্তরের সর্বোচ্চ একটি node-এ পড়ে — আসলে l − 1 আর l একই node-এ থাকলে সেই node-ই একমাত্র যে বাঁ সীমানা ধরে। সুতরাং প্রতি স্তরে আংশিক node ≤ ২. শুধু আংশিক node-ই সন্তানে নামে, তাই পরের স্তরে দেখা node ≤ 2 × 2 = ৪. (Root একা, স্তর ০-এ ১টি।) ∎
ডায়াগ্রামে ঠিক তাই: স্তর ২-এ দেখা ৪টি ([0-1], [2-3], [4-5], [6-7]), আংশিক ২টি ([0-1] বাঁ সীমানা 0|1 ধরে, [6-7] ডান সীমানা 6|7 ধরে). একই যুক্তি থেকে আরেকটি ফল: রেঞ্জটি সর্বোচ্চ প্রায় 2⌈log₂ n⌉ টি পুরো-ভেতরের node-এ ভাঙে — একে বলা হয় রেঞ্জের canonical decomposition. Lazy propagation, descent আর আরও অনেক কৌশল এই ভাঙনের ওপর দাঁড়িয়ে।
পুরো কোড — আট শাখা, একটি সংশোধন, আর brute force দিয়ে যাচাই
নিচের প্রোগ্রাম build, update আর query তিনটিই লেখে, tree অ্যারেটি ছাপে (ডায়াগ্রামের সাথে মেলানোর জন্য), তারপর সংশোধনের পর সব ৩৬টি রেঞ্জের উত্তর একটি সরল লুপের (brute force) সাথে মেলায়। নিজের লেখা প্রতিটি segment tree এভাবে যাচাই করার অভ্যাস করুন।
#include <iostream>
#include <vector>
using namespace std;
vector<long long> a, t; // t[1] = root, children 2v and 2v+1
void build(int v, int tl, int tr) {
if (tl == tr) { t[v] = a[tl]; return; }
int tm = (tl + tr) / 2;
build(2 * v, tl, tm); build(2 * v + 1, tm + 1, tr);
t[v] = t[2 * v] + t[2 * v + 1];
}
void update(int v, int tl, int tr, int pos, long long val) {
if (tl == tr) { t[v] = val; return; }
int tm = (tl + tr) / 2;
if (pos <= tm) update(2 * v, tl, tm, pos, val); else update(2 * v + 1, tm + 1, tr, pos, val);
t[v] = t[2 * v] + t[2 * v + 1]; // recompute on the way back up
}
long long query(int v, int tl, int tr, int l, int r) {
if (r < tl || tr < l) return 0; // outside
if (l <= tl && tr <= r) return t[v]; // fully inside
int tm = (tl + tr) / 2; // partial: ask both children
return query(2 * v, tl, tm, l, r) + query(2 * v + 1, tm + 1, tr, l, r);
}
int main() {
a = {5, 8, 6, 3, 2, 7, 2, 6}; // daily sales of 8 shops
int n = a.size();
t.assign(4 * n, 0);
build(1, 0, n - 1);
cout << "tree:"; for (int v = 1; v < 2 * n; ++v) cout << ' ' << t[v]; cout << '\n';
a[5] = 10; update(1, 0, n - 1, 5, 10);
cout << "after a[5] = 10, root = " << t[1] << " | sum[1..6] = " << query(1, 0, n - 1, 1, 6) << '\n';
int bad = 0;
for (int l = 0; l < n; ++l)
for (int r = l; r < n; ++r) {
long long s = 0;
for (int i = l; i <= r; ++i) s += a[i]; // brute force
bad += query(1, 0, n - 1, l, r) != s;
}
cout << "checked " << n * (n + 1) / 2 << " ranges, mismatches = " << bad << '\n';
}আউটপুট:
tree: 39 22 17 13 9 9 8 5 8 6 3 2 7 2 6
after a[5] = 10, root = 42 | sum[1..6] = 31
checked 36 ranges, mismatches = 0
প্রথম লাইন অংশ ৩-এর টেবিলের t[1..15] হুবহু: 39, তারপর 22 17, তারপর 13 9 9 8, তারপর আটটি leaf. সংশোধনের পর root 42 আর sum[1..6] = 31 — দুটিই ডায়াগ্রামের সাথে মেলে। শেষ লাইন সবচেয়ে গুরুত্বপূর্ণ: n(n + 1)/2 = ৩৬টি সম্ভাব্য রেঞ্জের প্রতিটিতে tree-র উত্তর brute force-এর সমান। Build Θ(n), প্রতিটি update Θ(log n), প্রতিটি query O(log n); জায়গা 4n টি long long = Θ(n). যোগফল long long-এ রাখা হয়েছে, কারণ ১০৫ টি ১০৯-আকারের মানের যোগ int ছাপিয়ে যায় (integer overflow).
def build(v, tl, tr):
if tl == tr:
t[v] = a[tl]
return
tm = (tl + tr) // 2
build(2 * v, tl, tm)
build(2 * v + 1, tm + 1, tr)
t[v] = t[2 * v] + t[2 * v + 1]
def update(v, tl, tr, pos, val):
if tl == tr:
t[v] = val
return
tm = (tl + tr) // 2
if pos <= tm:
update(2 * v, tl, tm, pos, val)
else:
update(2 * v + 1, tm + 1, tr, pos, val)
t[v] = t[2 * v] + t[2 * v + 1]
def query(v, tl, tr, l, r):
if r < tl or tr < l: # outside
return 0
if l <= tl and tr <= r: # fully inside
return t[v]
tm = (tl + tr) // 2 # partial: ask both children
return query(2 * v, tl, tm, l, r) + query(2 * v + 1, tm + 1, tr, l, r)
a = [5, 8, 6, 3, 2, 7, 2, 6] # daily sales of 8 shops
n = len(a)
t = [0] * (4 * n) # t[1] = root, children 2v and 2v+1
build(1, 0, n - 1)
print("tree:", *t[1:2 * n])
a[5] = 10
update(1, 0, n - 1, 5, 10)
print("after a[5] = 10, root =", t[1], "| sum[1..6] =", query(1, 0, n - 1, 1, 6))
bad = sum(query(1, 0, n - 1, l, r) != sum(a[l:r + 1]) for l in range(n) for r in range(l, n))
print("checked", n * (n + 1) // 2, "ranges, mismatches =", bad)আউটপুট C++-এর সাথে হুবহু এক:
tree: 39 22 17 13 9 9 8 5 8 6 3 2 7 2 6
after a[5] = 10, root = 42 | sum[1..6] = 31
checked 36 ranges, mismatches = 0
একই তিনটি ফাংশন, একই 1-based tree. Brute force এখানে এক লাইনে: sum(a[l:r + 1]) — Python-এর slice শেষ index বাদ দেয়, তাই r + 1. True/False-কে sum 1/0 হিসেবে গোনে, তাই bad হলো অমিলের সংখ্যা। Recursion-এর গভীরতা মাত্র ⌈log₂ n⌉ + 1, তাই Python-এর ১০০০-এর recursion সীমা নিয়ে চিন্তা নেই; তবে Python-এ প্রতিটি ফাংশন-ডাক ধীর, বড় ইনপুটে অংশ ৭-এর iterative রূপটি অনেক দ্রুত।
৬ · শুধু যোগ নয় — যেকোনো সহযোজী কাজ
কোডে যোগ এসেছে মাত্র দুই জায়গায়: parent-এর হিসাব t[2v] + t[2v+1], আর বাইরের node-এর ফেরত মান 0. এই দুটি বদলালেই নতুন প্রশ্নের tree. কিন্তু কোন কাজ বসানো যাবে? শর্ত দুটি:
- সহযোজিতা (associative): (x ∘ y) ∘ z = x ∘ (y ∘ z). Tree রেঞ্জটিকে যেভাবেই টুকরো করুক ([1] ∘ [2-3] ∘ [4-5] ∘ [6], বা অন্য কোনোভাবে), উত্তর একই আসতে হবে।
- Identity element e, যাতে e ∘ x = x — “বাইরে” node-এর ফেরত মান। এই দুই শর্ত মানা গঠনকে বীজগণিতে monoid বলে। (Identity না থাকলেও চলে, বাইরের দিকে না নামলেই হলো — কিন্তু থাকলে কোড সরল।)
| প্রশ্ন | parent = | identity (বাইরে) | উদাহরণ |
|---|---|---|---|
| যোগফল | L + R | 0 | মোট বিক্রি |
| সর্বনিম্ন (RMQ) | min(L, R) | +∞ (যেমন INT_MAX) | সবচেয়ে কম বিক্রির দিন |
| সর্বোচ্চ | max(L, R) | −∞ | সবচেয়ে বেশি তাপমাত্রা |
| GCD | gcd(L, R) | 0 (gcd(0, x) = x) | রেঞ্জের সবাইকে ভাগ করে এমন সবচেয়ে বড় সংখ্যা |
| XOR | L ^ R | 0 | parity, checksum |
| গুণফল mod p | L·R mod p | 1 | সম্ভাবনার গুণ |
| min আর তার সংখ্যা | ছোটটি নিন; সমান হলে সংখ্যা যোগ | (+∞, 0) | “সর্বনিম্ন মান কয়টি ঘরে আছে?” |
লক্ষ করুন, এখানে বিনিময়যোগ্যতা (commutative, x ∘ y = y ∘ x) লাগেনি। Recursive segment tree সবসময় বাঁ সন্তানকে বাঁয়ে আর ডানকে ডানে রেখে জোড়ে, তাই ক্রম ঠিক থাকে। ফলে matrix-এর গুণফল, string hash, বা “একের পর এক প্রয়োগ করা ফাংশন” — যেখানে ক্রম বদলালে উত্তর বদলায় — সেগুলোও segment tree-তে রাখা যায়।
একটি struct জুড়ে নতুন প্রশ্ন — রেঞ্জের মধ্যে সর্বোচ্চ subarray যোগফল
কঠিনতর প্রশ্ন: “শাখা l থেকে r-এর মধ্যে কোন পরপর শাখাগুলোর মোট লাভ সবচেয়ে বেশি?” (লাভ ঋণাত্মকও হতে পারে।) পুরো অ্যারের জন্য এটি maximum subarray সমস্যা, Kadane-এ O(n). কিন্তু প্রতিটি রেঞ্জে নতুন করে Kadane চালালে আবার O(n) প্রতি প্রশ্ন। সমাধান: প্রতিটি node-এ একটি সংখ্যার বদলে চারটি রাখুন —
struct Node { long long sum, pref, suf, best; }; // best = max subarray sum inside
Node leaf(long long x) { return {x, x, x, x}; }
Node combine(const Node& L, const Node& R) {
return { L.sum + R.sum,
max(L.pref, L.sum + R.pref), // best prefix: stays left, or takes all of L
max(R.suf, R.sum + L.suf), // best suffix: stays right, or takes all of R
max({L.best, R.best, L.suf + R.pref}) }; // inside L, inside R, or across the middle
}যুক্তি: দুই অংশ জোড়া লাগালে সেরা subarray হয় পুরোপুরি বাঁয়ে, পুরোপুরি ডানে, নয়তো মাঝখান পার হয় — আর মাঝ-পার-হওয়াটি হলো বাঁ অংশের সেরা suffix + ডান অংশের সেরা prefix. এই combine সহযোজী (চারটি মানই একটি অবিচ্ছিন্ন অংশের নিখুঁত বর্ণনা, তাই কোন ক্রমে টুকরো জোড়া হলো তাতে কিছু আসে যায় না), কিন্তু বিনিময়যোগ্য নয় — L আর R অদলবদল করলে prefix আর suffix গুলিয়ে যায়। Query-তে শুধু যোগের জায়গায় combine, আর “বাইরে” node বাদ দিন (এখানে identity হিসেবে sum = 0 আর বাকি তিনটি −∞ নেওয়া যায়)। প্রতিটি প্রশ্ন O(log n) — এক struct-এর ধারণা থেকেই। এটি augmented data structure-এর একটি সুন্দর উদাহরণ।
যা segment tree-তে সরাসরি চলে না: median, mode (সবচেয়ে বেশিবার আসা মান), বা “কয়টি আলাদা মান” — দুই অর্ধেকের median জেনে পুরোটার median বলা যায় না, তাই কোনো ছোট struct দিয়ে এদের জোড়া যায় না। এগুলোর জন্য আলাদা কৌশল লাগে: merge sort tree, persistent segment tree, বা offline-এ Mo’s algorithm (পর্ব ৩৭)। নিয়মটি মনে রাখুন: দুই অংশের ছোট সারাংশ থেকে পুরো অংশের সারাংশ বের করা গেলে তবেই segment tree.
৭ · Iterative segment tree — 2n ঘরে, recursion ছাড়া
Recursive tree পড়তে সহজ, কিন্তু প্রতিটি প্রশ্নে কয়েক ডজন ফাংশন-ডাক আর 4n ঘর। কনটেস্টে আর বাস্তব কোডে জনপ্রিয় একটি বিকল্প আছে, যা নিচ থেকে ওপরে কাজ করে — iterative (bottom-up) segment tree. নিয়ম: অ্যারের আকার ঠিক 2n; leaf a[i] বসে t[n + i]-এ; তারপর v = n − 1 থেকে 1 পর্যন্ত t[v] = t[2v] ∘ t[2v+1]. n যদি 2-এর ঘাত হয় (আমাদের n = 8), এই tree অংশ ৩-এর tree-র সাথে হুবহু এক — leaf t[8..15], root t[1].
- Update(p, x):
p += n; t[p] = x;তারপর p > 1 যতক্ষণ,t[p/2] = t[p] ∘ t[p^1], p /= 2. এখানেp ^ 1হলো p-এর ভাই (শেষ bit উল্টে দিলে 2v ↔ 2v + 1). - Query [l, r) — half-open, r বাদ: l += n, r += n; যতক্ষণ l < r: l বিজোড় হলে t[l] নিন আর l++; r বিজোড় হলে r−− আর t[r] নিন; তারপর দুটিকেই ২ দিয়ে ভাগ (এক স্তর ওপরে)।
কেন কাজ করে: যদি l একটি ডান সন্তান হয় (বিজোড়), তার parent-এর রেঞ্জে l-এর বাঁয়ের ঘরও আছে, যা প্রশ্নের বাইরে — তাই l-কে আলাদা করে নিয়ে এক ঘর ডানে সরতে হয়। বাঁ সন্তান হলে parent পুরোটা ভেতরে, তাই ওপরে উঠলেই চলে। r-এর দিকে ঠিক উল্টো। হালনাগাদ অ্যারেতে sum[1, 7) (অর্থাৎ index 1..6) হাতে চালান:
| স্তর | l, r (আগে) | নেওয়া node | জমা |
|---|---|---|---|
| leaf | 9, 15 | l বিজোড়: t[9] = 8; r বিজোড়: t[14] = 2 | 10 |
| জোড়া | 5, 7 | l বিজোড়: t[5] = 9 ([2-3]); r বিজোড়: t[6] = 12 ([4-5]) | 31 |
| অর্ধেক | 3, 3 | l = r, থামুন | 31 |
একই চারটি node — [1], [6], [2-3], [4-5] — শুধু ভিন্ন ক্রমে, আর কোনো recursion বা আংশিক node-এর ঝামেলা ছাড়া। প্রতি স্তরে সর্বোচ্চ দুটি node নেওয়া হয়, তাই O(log n). নিচের প্রোগ্রাম এই রূপে range minimum রাখে, ইচ্ছে করে n = 10 নিয়ে (2-এর ঘাত নয়), আর সব ৫৫টি রেঞ্জ brute force-এর সাথে মেলায়:
#include <algorithm>
#include <climits>
#include <initializer_list>
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
struct MinTree { // iterative, bottom-up: leaves at n..2n-1
int n; vector<int> t;
MinTree(const vector<int>& a) : n(a.size()), t(2 * a.size()) {
for (int i = 0; i < n; ++i) t[n + i] = a[i];
for (int v = n - 1; v >= 1; --v) t[v] = min(t[2 * v], t[2 * v + 1]);
}
void set(int p, int val) {
p += n; t[p] = val;
for (; p > 1; p /= 2) t[p / 2] = min(t[p], t[p ^ 1]); // p ^ 1 = sibling
}
int query(int l, int r) { // min of a[l..r-1], half-open
int res = INT_MAX; // identity for min
for (l += n, r += n; l < r; l /= 2, r /= 2) {
if (l & 1) res = min(res, t[l++]);
if (r & 1) res = min(res, t[--r]);
}
return res;
}
};
int main() {
vector<int> a = {5, 8, 6, 3, 2, 7, 2, 6, 9, 4}; // n = 10, not a power of two
MinTree st(a);
cout << "min[0,5) = " << st.query(0, 5) << ", min[5,10) = " << st.query(5, 10) << '\n';
for (auto [p, v] : {pair{4, 11}, pair{6, 1}}) { a[p] = v; st.set(p, v); }
cout << "after updates: min[0,5) = " << st.query(0, 5) << ", min[5,10) = " << st.query(5, 10) << '\n';
int bad = 0, cnt = 0;
for (int l = 0; l < st.n; ++l)
for (int r = l + 1; r <= st.n; ++r, ++cnt)
bad += st.query(l, r) != *min_element(a.begin() + l, a.begin() + r);
cout << "checked " << cnt << " ranges, mismatches = " << bad << '\n';
}আউটপুট:
min[0,5) = 2, min[5,10) = 2
after updates: min[0,5) = 3, min[5,10) = 1
checked 55 ranges, mismatches = 0
শুরুতে প্রথম পাঁচ ঘরের min 2 (index 4), শেষ পাঁচ ঘরেরও 2 (index 6). দুটি update-এ a[4] = 11 আর a[6] = 1: এখন প্রথম অর্ধেকের min 3, শেষ অর্ধেকের 1. শেষ লাইন: n = 10-এ সব ১০·১১/২ = ৫৫টি অ-খালি রেঞ্জে উত্তর min_element-এর সাথে মেলে। n 2-এর ঘাত না হলে এই tree আর “সুন্দর” binary tree থাকে না — কিছু node-এর রেঞ্জ অ্যারের শেষ থেকে শুরুতে ঘুরে আসে — তবু query ঠিক থাকে, কারণ লুপ শুধু সেই node-গুলো নেয় যাদের পুরো রেঞ্জ [l, r)-এর ভেতরে। জায়গা ঠিক 2n টি int, build Θ(n), set আর query Θ(log n).
import math
class MinTree: # iterative, bottom-up: leaves at n..2n-1
def __init__(self, a):
self.n = n = len(a)
self.t = [0] * n + list(a)
for v in range(n - 1, 0, -1):
self.t[v] = min(self.t[2 * v], self.t[2 * v + 1])
def set(self, p, val):
p += self.n
self.t[p] = val
while p > 1:
self.t[p // 2] = min(self.t[p], self.t[p ^ 1])
p //= 2
def query(self, l, r): # min of a[l..r-1], half-open
res = math.inf
l, r = l + self.n, r + self.n
while l < r:
if l & 1:
res = min(res, self.t[l]); l += 1
if r & 1:
r -= 1; res = min(res, self.t[r])
l //= 2; r //= 2
return res
a = [5, 8, 6, 3, 2, 7, 2, 6, 9, 4] # n = 10, not a power of two
st = MinTree(a)
print(f"min[0,5) = {st.query(0, 5)}, min[5,10) = {st.query(5, 10)}")
for p, v in [(4, 11), (6, 1)]:
a[p] = v
st.set(p, v)
print(f"after updates: min[0,5) = {st.query(0, 5)}, min[5,10) = {st.query(5, 10)}")
pairs = [(l, r) for l in range(st.n) for r in range(l + 1, st.n + 1)]
bad = sum(st.query(l, r) != min(a[l:r]) for l, r in pairs)
print("checked", len(pairs), "ranges, mismatches =", bad)আউটপুট C++-এর সাথে হুবহু এক:
min[0,5) = 2, min[5,10) = 2
after updates: min[0,5) = 3, min[5,10) = 1
checked 55 ranges, mismatches = 0
Python-এ INT_MAX-এর জায়গায় math.inf identity; অ-খালি রেঞ্জে লুপ অন্তত একটি ঘর নেয়, তাই ফল সবসময় অ্যারের একটি পূর্ণসংখ্যা, inf ছাপা হয় না। self.t = [0] * n + list(a) এক লাইনে leaf-গুলো n..2n − 1-এ বসায়; index 0 ব্যবহার হয় না। Recursion নেই, ফাংশন-ডাক কম — Python-এ recursive রূপের চেয়ে কয়েক গুণ দ্রুত চলে।
একটি সতর্কতা: এই query-তে বাঁ দিক থেকে আর ডান দিক থেকে নেওয়া node একই res-এ মেশে, ফলে ক্রম ঠিক থাকে না। min, যোগ, gcd-এর মতো বিনিময়যোগ্য কাজে সমস্যা নেই। কিন্তু অংশ ৬-এর max-subarray struct-এর মতো ক্রম-নির্ভর কাজে দুটি আলাদা accumulator রাখুন — resL = resL ∘ t[l++] আর resR = t[--r] ∘ resR — আর শেষে resL ∘ resR.
৮ · Tree-র ওপর নামা — prefix ≥ k-এর প্রথম index
নতুন প্রশ্ন: “শাখা ০ থেকে শুরু করে ক্রমানুসারে যোগ করলে কোন শাখায় পৌঁছে মোট বিক্রি প্রথমবার ২০ লাখ ছাড়ায় (≥ 20)?” Prefix sum-এর ওপর binary search (পর্ব ১৮) করা যায় — কিন্তু প্রতিটি prefix নিজেই একটি O(log n) query, তাই মোট O(log² n). তার চেয়ে ভালো: tree-র মধ্য দিয়ে সরাসরি নামুন। প্রতিটি node-এ বাঁ সন্তানের যোগফল দেখুন — সেটি ≥ k হলে উত্তর বাঁ দিকেই, নইলে বাঁ দিকের পুরো যোগফল k থেকে বাদ দিয়ে ডানে যান। এই descent এক পথে root থেকে leaf: O(log n).
| এই ধাপে | |
| বাকি k | |
| যাচাই |
- শুরু. root-এর যোগফল 42 ≥ 20, তাই এমন একটি index আছেই (42 < 20 হলে উত্তর “নেই”, −1). k = 20 নিয়ে root থেকে নামা শুরু।
- [0-7] থেকে. বাঁ সন্তান [0-3]-এর যোগফল 22 ≥ 20 — প্রথম চার শাখাতেই ২০ ছাড়িয়ে যায়, তাই উত্তর বাঁ অর্ধেকে। ডান অর্ধেক আর দেখার দরকার নেই।
- [0-3] থেকে. বাঁ সন্তান [0-1] = 13 < 20 — প্রথম দুই শাখায় যথেষ্ট নয়। তাই এই ১৩ নিশ্চিতভাবে যোগ হবে; k থেকে বাদ দিই: k = 7, আর ডানে [2-3]-এ যাই।
- [2-3] থেকে. বাঁ সন্তান [2] = 6 < 7 — শাখা ২ পর্যন্তও যথেষ্ট নয়। k = 1, ডানে [3].
- Leaf [3]. এখানে 3 ≥ 1, উত্তর index 3. যাচাই: 5 + 8 + 6 = 19 < 20, কিন্তু 19 + 3 = 22 ≥ 20. চারটি node, প্রতি স্তরে একটি — O(log n). শর্ত একটাই: সব মান অঋণাত্মক, যাতে prefix শুধু বাড়ে।
Pseudocode — first index with prefix sum ≥ k (values ≥ 0; 1-based tree)
firstPrefix(k):
if t[1] < k: return -1
v = 1; tl = 0; tr = n-1
while tl != tr:
tm = (tl + tr) / 2
if t[2v] >= k: v = 2v; tr = tm // answer is on the left
else: k = k - t[2v]; v = 2v+1; tl = tm+1 // skip the whole left part
return tlএকই ছাঁচে আরও প্রশ্ন: (১) max tree-তে “প্রথম index যেখানে a[i] ≥ x” — বাঁ সন্তানের max ≥ x হলে বাঁয়ে, নইলে ডানে। (২) k-তম ১: 0/1 অ্যারের sum tree-তে k-তম 1-এর অবস্থান — অর্থাৎ “এখনো উপস্থিত উপাদানগুলোর মধ্যে k-তম কে?” কোনো উপাদান মুছতে শুধু তার ঘরে 0 বসান। মানগুলোকে index ধরে (প্রয়োজনে coordinate compression করে) গণনার tree বানালে এটি একটি পরিবর্তনশীল সংগ্রহের order statistic O(log n)-এ দেয়। (৩) একটি রেঞ্জ [l, r]-এর ভেতরে প্রথম শর্তপূরণকারী index — canonical decomposition-এর node-গুলো বাঁ থেকে ডানে দেখুন, প্রথম যে node-এ শর্ত মেলে শুধু তার ভেতরে নামুন; মোট O(log n).
৯ · কোনটি কখন — Fenwick, sparse table, lazy আর মেমরির হিসাব
রেঞ্জের প্রশ্নের জন্য এই মডিউলে আরও কয়েকটি হাতিয়ার আসছে। প্রত্যেকটির নিজস্ব জায়গা আছে:
| কাঠামো | প্রস্তুতি | প্রশ্ন | point update | জায়গা | কোন কাজে |
|---|---|---|---|---|---|
| Prefix sum (পর্ব ১৭) | O(n) | O(1) | O(n) | n + 1 | উল্টো আছে (যোগ, XOR), কোনো বদল নেই |
| Fenwick tree (পর্ব ৩৬) | O(n) | O(log n) | O(log n) | n + 1 | যোগ/XOR-এর মতো উল্টো-সহ কাজ; সবচেয়ে কম কোড |
| Sparse table (পর্ব ৩৭) | O(n log n) | O(1) | — (আবার বানাতে হয়) | n log n | min, max, gcd — বদল নেই |
| Segment tree (এই পর্ব) | O(n) | O(log n) | O(log n) | 2n থেকে 4n | যেকোনো monoid, struct, descent |
| Segment tree + lazy (পর্ব ৩৫) | O(n) | O(log n) | রেঞ্জ: O(log n) | 4n × ২ | “l..r সবার সাথে v যোগ করুন” ধরনের range update |
Fenwick tree বনাম segment tree: শুধু point update আর range sum লাগলে Fenwick ছোট (দশ লাইনের কম), দ্রুত (ধ্রুবক ছোট) আর n + 1 ঘরে চলে। কিন্তু Fenwick prefix-এর উত্তর দেয়; যেকোনো [l, r] পেতে prefix(r) − prefix(l − 1) লাগে, অর্থাৎ বিয়োগ — উল্টো। min-এর উল্টো নেই, তাই সাধারণ range min-এ Fenwick চলে না, segment tree চলে। Sparse table বনাম segment tree: অ্যারে কখনো না বদলালে sparse table min/max-এর প্রতিটি প্রশ্ন O(1)-এ দেয় — দুটি পরস্পর-ঢাকা (overlapping) ঘাত-দৈর্ঘ্যের অংশের min নিয়ে, যা চলে কারণ min(x, x) = x (idempotent). যোগে overlapping চলে না (একই ঘর দুবার গোনা হয়)। আর একটিও update এলে sparse table অচল।
Lazy propagation (পর্ব ৩৫), এক নজরে: “৩ থেকে ৬ নম্বর শাখার সবার বিক্রিতে ২ যোগ করুন” — এই রেঞ্জ আপডেট এখনকার tree-তে প্রতিটি ঘর আলাদা করে বদলায়, O(n log n). Lazy propagation-এর বুদ্ধি: canonical decomposition-এর O(log n) টি node-এ শুধু একটি “বাকি কাজ” চিহ্ন রেখে দিন, আর নিচে তখনই নামান যখন কোনো প্রশ্ন সত্যিই সেখানে যায়। বিস্তারিত পরের পর্বে।
মেমরি আর recursion — বাস্তবের খুঁটিনাটি
- জায়গা: recursive রূপে 4n, iterative-এ 2n. n = 106 আর
long longহলে 4n মানে ৩২ MB — অনেক judge-এর ২৫৬ MB সীমায় চলে, কিন্তু একাধিক tree বা struct-node হলে হিসাব করে নিন। মানint-এ ধরলেintরাখুন। - Recursion: গভীরতা মাত্র ⌈log₂ n⌉ + 1, তাই stack overflow-এর ভয় নেই (DSU-এর লম্বা শিকল বা DFS-এর মতো নয়)। খরচটি গতির: প্রতিটি query কয়েক ডজন ফাংশন-ডাক। C++-এ এটি সাধারণত যথেষ্ট দ্রুত; Python-এ বড় ইনপুটে iterative রূপ নিন।
- Off-by-one: এক কোডে inclusive [l, r] আর অন্য কোডে half-open [l, r) মেশানো segment tree-র সবচেয়ে প্রচলিত বাগ। একটি নিয়ম বেছে ফাংশনের পাশে মন্তব্যে লিখে রাখুন।
- খুব বড় index (যেমন 109 পর্যন্ত অবস্থান, কিন্তু মাত্র 105 টি ব্যবহৃত): 4·109 ঘর অসম্ভব। হয় coordinate compression, নয়তো dynamic segment tree — node শুধু দরকার হলে তৈরি হয়, প্রতি update-এ O(log U) টি নতুন node.
আরও গভীরে — persistent segment tree: point update শুধু একটি পথের O(log n) টি node বদলায়। তাই পুরোনো node মুছে না ফেলে পথের নতুন কপি বানালে (বাকি সব node পুরোনো সংস্করণের সাথে ভাগাভাগি) প্রতিটি update-এর পর tree-র একটি নতুন সংস্করণ পাওয়া যায়, মাত্র O(log n) বাড়তি জায়গায় — একে বলে persistent segment tree, path copying-এর কৌশলে (Driscoll, Sarnak, Sleator ও Tarjan-এর ১৯৮৯ সালের persistence-এর কাজের একটি সরল প্রয়োগ)। এর বিখ্যাত কনটেস্ট-প্রয়োগ: “a[l..r]-এর k-তম ছোট মান” — প্রতিটি prefix-এর জন্য একটি গণনার tree-সংস্করণ রেখে, দুই সংস্করণের পার্থক্যের ওপর অংশ ৮-এর descent চালিয়ে, প্রতি প্রশ্ন O(log n).
পরীক্ষা ও ইন্টারভিউতে:
- n টি উপাদানের segment tree-তে node কয়টি, আর অ্যারেতে কত ঘর রাখবেন? — node 2n − 1; recursive 2v/2v + 1 layout-এ 4n ঘর যথেষ্ট, iterative রূপে 2n.
- Build, point update আর range query-র সময়? — Θ(n), Θ(log n), O(log n); জায়গা Θ(n).
- Range query কেন O(log n)? — প্রতি স্তরে সর্বোচ্চ ২টি আংশিক node (একটি বাঁ সীমানার, একটি ডান সীমানার), তাই দেখা node ≤ ৪ প্রতি স্তরে।
- Segment tree-তে কোন কাজ বসানো যায়? — যেকোনো সহযোজী কাজ (identity থাকলে সুবিধা): sum, min, max, gcd, XOR, struct-এর combine; median নয়।
- Fenwick tree থাকতে segment tree কেন? — Fenwick-এ উল্টো লাগে (range min চলে না), segment tree যেকোনো monoid, descent আর lazy-তে রেঞ্জ আপডেট সামলায়।
১০ · AI-এর যুগে segment tree — সুন্দর কোড, লুকানো off-by-one
“Segment tree লিখে দাও” — AI প্রায় নিশ্চিতভাবে চলনসই ৩০ লাইন দেবে। কিন্তু segment tree-র বাগ প্রায় কখনো compile error দেয় না, আর ছোট উদাহরণে প্রায়ই উত্তর ঠিক আসে: n = 8-এ 2n ঘর যথেষ্ট, n = 6-এ নয়; সব মান ধনাত্মক হলে min-এর identity 0 ভুল হলেও ধরা পড়ে না, ঋণাত্মক এলে পড়ে। তাই AI-এর কোড নিজের brute force দিয়ে যাচাই না করে জমা দেবেন না।
Segment tree-এর কোডে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| Recursive 2v/2v + 1 tree-র জন্য 2n ঘর | n = 6 বা n = 9 নিয়ে চালান — index সীমার বাইরে যায় (C++-এ undefined behavior, Python-এ IndexError) |
| min tree-তে বাইরের node-এর ফেরত মান 0 | সব মান ধনাত্মক এমন অ্যারেতে min প্রশ্ন করুন — উত্তর 0 আসবে |
| Query-তে inclusive আর half-open মেশানো | এক-ঘরের রেঞ্জ [i, i] আর পুরো অ্যারে [0, n − 1] প্রশ্ন করুন |
| Update-এ leaf বদলে parent আবার হিসাব না করা | update-এর পরে root-এর মান অ্যারের যোগফলের সমান কি না assert করুন |
| ক্রম-নির্ভর combine (struct) iterative রূপে এক accumulator-এ | max subarray-এর ফল brute force-এর সাথে এলোমেলো রেঞ্জে মেলান |
| “Build O(n log n)” বা “query O(1)” দাবি | Build-এ প্রতিটি node একবার: Θ(n). Query-র ৪-node-প্রতি-স্তর যুক্তি চান |
AI-কে কোড লিখে দিতে না বলে, আপনার কোড আর trace পরীক্ষা করতে বলুন — আর প্রতিটি complexity দাবির যুক্তি চান:
Segment tree-এর কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১“Segment tree” নামটি computational geometry থেকে এসেছে — সেখানে এর মানে কী ছিল, আর কনটেস্টের segment tree-র সাথে পার্থক্য কোথায়?
১৯৭৭ সালে গণিতবিদ Victor Klee একটি প্রশ্ন তোলেন: সংখ্যারেখায় n টি interval দেওয়া থাকলে তাদের মিলিত দৈর্ঘ্য (union-এর মাপ) কি O(n log n)-এর চেয়ে দ্রুত বের করা যায়? সর্ট করে O(n log n)-এ হয়, আর Michael Fredman ও Bruce Weide ১৯৭৮ সালে দেখান যে একটি স্বাভাবিক গণনা-মডেলে এর চেয়ে ভালো সম্ভব নয়। প্রশ্নটির বড় রূপ — দুই বা আরও বেশি মাত্রায় আয়তক্ষেত্রগুলোর মিলিত ক্ষেত্রফল — আজ “Klee’s measure problem” নামে পরিচিত। একই সময়ে Jon Bentley এর জন্য O(n log n)-এর একটি পদ্ধতি দেন: আয়তক্ষেত্রগুলোর ওপর দিয়ে একটি উল্লম্ব রেখা বাঁ থেকে ডানে সরান (sweep line), আর রেখার ওপর কোন অংশগুলো এখন ঢাকা, তা একটি tree-তে রাখুন। সেই tree-র প্রতিটি node একটি মৌলিক অংশের (elementary segment) দায়িত্বে, আর প্রতিটি interval canonical decomposition-এর O(log n) টি node-এ “জমা” হয়। এই কাঠামোকেই computational geometry-র বইয়ে (যেমন de Berg ও সহলেখকদের Computational Geometry: Algorithms and Applications) segment tree বলা হয়।
সেই মূল segment tree-র কাজ হলো interval রাখা আর “এই বিন্দুটি কোন কোন interval-এর ভেতরে?” (stabbing query) উত্তর দেওয়া। কনটেস্টে আমরা যাকে segment tree বলি, তার কাঠামো একই — রেঞ্জ অর্ধেক করে ভাগ, প্রতিটি node একটি অংশ — কিন্তু প্রতিটি node-এ রাখা হয় অ্যারের সেই অংশের একটি সারাংশ (যোগ, min), আর প্রশ্ন হলো রেঞ্জের সারাংশ। কিছু বইয়ে এই কনটেস্ট-রূপটিকে “range tree” বা “interval tree”-ও বলা হয়, যদিও geometry-তে ঐ দুটি নাম আবার ভিন্ন কাঠামোর। নামের এই জট নতুনদের বিভ্রান্ত করে: পাঠ্যবই আর কনটেস্টের ব্লগ একই শব্দে আলাদা জিনিস বোঝাতে পারে।
মজার ব্যাপার, Klee-র প্রশ্নের সমাধানে আজও এই পর্বের কৌশলই চলে: sweep line-এর প্রতিটি ঘটনায় একটি রেঞ্জে +1 বা −1 (রেঞ্জ আপডেট, পর্ব ৩৫-এর lazy-র আত্মীয়), আর প্রতিটি node-এ “কত দৈর্ঘ্য ঢাকা” রাখা। কনটেস্টে “আয়তক্ষেত্রগুলোর মোট ক্ষেত্রফল” একটি ক্লাসিক segment tree সমস্যা। উচ্চতর মাত্রায় এই সমস্যার সঠিক জটিলতা নিয়ে গবেষণা এখনো চলছে।
মূল উপলব্ধি: segment tree-র আসল ধারণা হলো “যেকোনো রেঞ্জকে O(log n) টি প্রস্তুত অংশে ভাঙা” — node-এ কী রাখবেন, interval-এর তালিকা না সংখ্যার সারাংশ, তা সমস্যা ঠিক করে।
প্র ০২4n ঘরের প্রায় অর্ধেক অব্যবহৃত থাকতে পারে — segment tree-র মেমরি কমানোর কী কী উপায় আছে, আর বাস্তবে মেমরির layout গতিকে কীভাবে প্রভাবিত করে?
Node মাত্র 2n − 1 টি, অথচ recursive 2v/2v + 1 নিয়মে 4n ঘর নিতে হয় — n = 2k + 1 হলে প্রায় অর্ধেক ঘর ফাঁকা। তিনটি পরিচিত সমাধান আছে। প্রথমটি এই পর্বের iterative রূপ: ঠিক 2n ঘর, কোনো ফাঁকা নেই (index 0 বাদে)। দ্বিতীয়টি recursive রেখেও 2n − 1 ঘরে আঁটানো: node-গুলো preorder ক্রমে সাজান — node v-এর বাঁ সন্তান v + 1, আর ডান সন্তান v + 2·(বাঁ অংশের দৈর্ঘ্য), কারণ বাঁ subtree-তে ঠিক 2·(দৈর্ঘ্য) − 1 টি node থাকে। এটি cp-algorithms-এর মতো জনপ্রিয় রেফারেন্সে “memory efficient” রূপ হিসেবে দেখানো হয়। তৃতীয়টি হলো n-কে পরের 2-এর ঘাতে বাড়িয়ে নেওয়া (বাড়তি ঘরে identity) — তখন tree নিখুঁত, 2·2⌈log n⌉ < 4n ঘর, আর কোড সরল।
কিন্তু শুধু ঘরের সংখ্যা নয়, কোন ঘর কোথায় বসে সেটিও গতির বিষয়। আধুনিক CPU-তে মেমরি থেকে একটি মান আনতে cache-এ না থাকলে শত শত cycle লাগে, আর আসে একসাথে ৬৪ byte-এর একটি cache line. 2v/2v + 1 layout-এ ওপরের স্তরগুলো অ্যারের শুরুতে পাশাপাশি থাকে, তাই প্রায় সবসময় cache-এ — ভালো। কিন্তু নিচের স্তরে একটি query-র দুই সীমানা অনেক দূরের ঘর ছোঁয় — cache locality কমে। বাইনারি সার্চের জন্য এই একই layout-কে Eytzinger layout বলা হয় (১৬শ শতকের এক বংশতালিকা-পদ্ধতির নামে), আর Paul-Virak Khuong ও Pat Morin-এর মতো গবেষকদের পরীক্ষায় দেখা গেছে সাজানো অ্যারের চেয়ে এটি অনেক সময় দ্রুত।
উচ্চ-গতির লাইব্রেরি তাই কখনো “wide” segment tree বানায় — প্রতিটি node-এ ২-এর বদলে ১৬ বা ৩২ সন্তান, যাতে একটি node-এর সব তথ্য এক-দুটি cache line-এ ধরে, আর SIMD নির্দেশে একসাথে অনেক তুলনা হয়। উচ্চতা log₂ n থেকে নেমে log₁₆ n — B-tree-র (পর্ব ৩০) সেই একই বুদ্ধি, শুধু ডিস্কের বদলে cache-এর জন্য।
অন্যদিকে, যখন অবস্থানের পরিসর বিশাল (109) কিন্তু ব্যবহৃত অল্প, তখন সমাধান dynamic tree: শুধু ছোঁয়া node তৈরি, pointer বা index দিয়ে সন্তান, প্রতি update O(log U) নতুন node. কনটেস্টে এটি coordinate compression-এর বিকল্প, বিশেষত যখন প্রশ্নগুলো আগে থেকে জানা নেই (online).
মূল উপলব্ধি: Asymptotic জায়গা সব রূপেই Θ(n); পার্থক্য ধ্রুবকে আর cache-এ। প্রথমে সঠিক আর সরল কোড, তারপর দরকার হলে 2n layout বা wide node — আর সবসময় মেপে দেখুন।
প্র ০৩Segment tree-র জন্য সহযোজিতা কেন যথেষ্ট, আর একই ধারণা parallel computing-এর “scan” আর MapReduce-এ কীভাবে ফিরে আসে? Floating-point যোগে কী সমস্যা হয়?
Segment tree একটি রেঞ্জকে কয়েকটি টুকরোয় ভাঙে আর টুকরোর ফল জোড়ে, কিন্তু কোন টুকরোগুলো, তা রেঞ্জ অনুযায়ী বদলায়। উত্তর যাতে টুকরো-ভাগের ওপর নির্ভর না করে, তার নিশ্চয়তা দেয় সহযোজিতা: (x ∘ y) ∘ z = x ∘ (y ∘ z) মানে যেকোনো বন্ধনী-বিন্যাসে একই ফল, শুধু বাঁ-ডান ক্রম ঠিক থাকতে হবে। বীজগণিতের ভাষায় এমন কাঠামো (identity-সহ) একটি monoid, আর segment tree আসলে “monoid-এর রেঞ্জ-গুণফল” হিসাবের যন্ত্র।
একই বীজগণিত parallel computing-এর ভিত্তি। Guy Blelloch ১৯৯০ সালের দিকে “Prefix sums and their applications” লেখায় দেখান যে যেকোনো সহযোজী কাজের সব prefix (scan) একটি balanced binary tree-র ওপর দুই পাসে — ওপরে ওঠা (up-sweep) আর নিচে নামা (down-sweep) — O(n) মোট কাজ আর O(log n) গভীরতায় করা যায়। আকৃতিটি হুবহু segment tree: up-sweep হলো build. আজ GPU-তে sort, stream compaction বা histogram — অনেক কিছুর ভেতরে এই scan. আর MapReduce বা Spark-এর “reduce” আর “combiner”-এর শর্তও একই: কাজটি সহযোজী (আর প্রায়ই বিনিময়যোগ্য) হলে তবেই বিভিন্ন মেশিনের আংশিক ফল যেকোনো ক্রমে জোড়া যায়।
এখানে একটি বাস্তব ফাঁদ আছে: কম্পিউটারের floating-point যোগ আসলে সহযোজী নয়। double-এ (0.1 + 0.2) + 0.3-এর ফল 0.6000000000000001, অথচ 0.1 + (0.2 + 0.3)-এর ফল 0.6 — কারণ প্রতিটি যোগের পর rounding হয়। ফলে double-এর segment tree-র রেঞ্জ-যোগ আর বাঁ থেকে ডানে লুপের যোগ শেষ কয়েক অঙ্কে আলাদা হতে পারে — দুটিই “ঠিক”, শুধু rounding-এর ক্রম আলাদা। Stress test-এ তাই double তুলনা করুন একটি ছোট সহনশীলতা (যেমন 10−9 আপেক্ষিক) দিয়ে, == দিয়ে নয়। সম্ভব হলে টাকার হিসাব পয়সায় পূর্ণসংখ্যা হিসেবে রাখুন। মজার দিক: tree-ভিত্তিক (pairwise) যোগে rounding-এর ভুল সাধারণত লুপের চেয়ে কমই জমে — O(log n) স্তর, n নয়।
মূল উপলব্ধি: “এই কাজটি কি সহযোজী?” — এই প্রশ্নটিই বলে দেয় কাজটি segment tree-তে, GPU-র scan-এ বা বহু মেশিনে ভাগ করে চালানো যাবে কি না; আর floating-point এই নিয়মের একটি সূক্ষ্ম ব্যতিক্রম।
প্র ০৪Range minimum প্রশ্ন তত্ত্বে O(n) প্রস্তুতি আর O(1) প্রশ্নে করা যায় — তাহলে কনটেস্টে কেন এখনো segment tree আর sparse table?
Range minimum query (RMQ)-এর ইতিহাস lowest common ancestor (LCA, পর্ব ৪৮)-এর সাথে জড়ানো। ১৯৮৪ সালে Dov Harel আর Robert Tarjan দেখান যে একটি tree-র যেকোনো দুই node-এর LCA O(n) প্রস্তুতির পর O(1)-এ বের করা যায় — কিন্তু তাঁদের পদ্ধতি এতই জটিল যে বাস্তবে প্রায় কেউ লিখতেন না। পরে বোঝা যায় যে LCA আর RMQ আসলে একই সমস্যার দুই রূপ: tree-র Euler tour-এর গভীরতার অ্যারেতে RMQ করলেই LCA, আর যেকোনো অ্যারের Cartesian tree বানালে RMQ হয় LCA. ২০০০ সালে Michael Bender আর Martín Farach-Colton “The LCA Problem Revisited” লেখায় একটি অনেক সরল O(n)/O(1) পদ্ধতি দেন: অ্যারেকে (log n)/2 আকারের ব্লকে ভাগ, ব্লকগুলোর min-এর ওপর sparse table, আর ব্লকের ভেতরের প্রশ্নের জন্য সব সম্ভাব্য ছোট ব্লক-আকৃতির উত্তর আগে থেকে হিসাব করা। পরে Johannes Fischer ও Volker Heun আরও কমিয়ে দেখান যে মাত্র প্রায় 2n bit বাড়তি জায়গায় O(1) প্রশ্ন সম্ভব।
তাহলে কনটেস্টে এগুলো কেন কম দেখা যায়? প্রথমত, ধ্রুবক: O(n) প্রস্তুতির পদ্ধতির ভেতরে কয়েকটি স্তর আর টেবিল, প্রশ্ন O(1) হলেও সেই “১”-এর ভেতরে অনেক কাজ। n = 106-এ sparse table-এর n log n = প্রায় ২ কোটি ঘর সাধারণত মেমরিতে ধরে যায়, আর প্রশ্ন মাত্র দুটি টেবিল-পড়া। দ্বিতীয়ত, কোডের দৈর্ঘ্য আর ভুলের ঝুঁকি — চাপের মধ্যে ১০০ লাইনের চতুর কোডের চেয়ে ১৫ লাইনের sparse table বা segment tree নিরাপদ। তৃতীয়ত, প্রায় সব সমস্যায় update-ও থাকে, আর তখন O(1) পদ্ধতিগুলো অচল; segment tree তখনো O(log n).
তত্ত্বের এই ফলগুলো অর্থহীন নয়: suffix array-ভিত্তিক string অ্যালগরিদমে (পর্ব ৫৯) LCP অ্যারের ওপর RMQ, বা bioinformatics-এর বিশাল genome সূচিতে, যেখানে n শত কোটি আর জায়গা অমূল্য, সেখানে succinct RMQ সত্যিই ব্যবহার হয়। আর সমস্যার আকার বুঝে কোনটি বেছে নেবেন, সেটিই প্রকৌশল।
মূল উপলব্ধি: asymptotic-ভাবে সেরা পদ্ধতি আর বাস্তবে সেরা পদ্ধতি সবসময় এক নয়। প্রশ্ন বদলায় কি না, n কত, মেমরি কত আর কোড কত লম্বা — এই চারটি দেখে segment tree, sparse table বা তাত্ত্বিক O(1) বেছে নিন।
প্র ০৫Josephus সমস্যা — দুই হাজার বছরের পুরোনো একটি ধাঁধা segment tree-র descent দিয়ে O(n log n)-এ কীভাবে সমাধান হয়?
প্রথম শতাব্দীর ইহুদি ইতিহাসবিদ Flavius Josephus রোমানদের বিরুদ্ধে যুদ্ধের বিবরণে একটি ঘটনার কথা লেখেন: একটি গুহায় অবরুদ্ধ সৈন্যরা ধরা দেওয়ার বদলে লটারির মাধ্যমে একে একে প্রাণ দেওয়ার সিদ্ধান্ত নেয়, আর শেষে টিকে যাওয়া দুজনের একজন ছিলেন তিনি নিজে। পরে এই গল্প থেকে একটি গাণিতিক ধাঁধা জন্ম নেয়, যা আজ Josephus problem: n জন বৃত্তে দাঁড়িয়ে, প্রতি k-তম জনকে সরিয়ে দেওয়া হয়; কে শেষে থাকবে, আর কোন ক্রমে সবাই সরবে? (ঐতিহাসিক বিবরণের খুঁটিনাটি নিয়ে পণ্ডিতদের মতভেদ আছে; ধাঁধাটি তার পরিচিত গাণিতিক রূপ।)
শুধু শেষ জনকে চাইলে একটি সুন্দর recurrence আছে: J(1) = 0, J(n) = (J(n − 1) + k) mod n — O(n). কিন্তু পুরো সরানোর ক্রম চাইলে? সরল simulation-এ একটি linked list বা অ্যারে নিয়ে প্রতিবার k ধাপ হাঁটলে O(nk), আর অ্যারে থেকে মুছতে O(n) — মোট O(n²) পর্যন্ত। n = 106-এ অচল।
এখানে অংশ ৮-এর “k-তম 1” descent কাজে আসে। n ঘরের একটি 0/1 অ্যারে রাখুন, শুরুতে সবাই 1 (উপস্থিত), আর তার ওপর sum segment tree. বর্তমানে যে অবস্থান থেকে গোনা শুরু, সেটিকে উপস্থিতদের মধ্যে ক্রম-নম্বর (rank) দিয়ে রাখুন: উপস্থিত m জনের মধ্যে পরের সরানো জনের rank = (বর্তমান rank + k − 1) mod m. সেই rank-এর মানুষ কে — অর্থাৎ (rank + 1)-তম 1 কোন index-এ — descent দিয়ে O(log n)-এ পাওয়া যায়; তাকে সরাতে তার ঘরে 0, point update O(log n). n বার এই কাজ, মোট O(n log n). Fenwick tree-র ওপর binary lifting দিয়েও একই কাজ হয় (পর্ব ৩৬)।
এই ছাঁচটি আরও অনেক জায়গায় আসে: “একটি তালিকা থেকে k-তম উপস্থিত উপাদান মুছতে থাকুন”, অনলাইনে আসা সংখ্যার মধ্যে চলমান median, বা একটি permutation-কে তার inversion-ক্রম থেকে পুনর্গঠন। সবখানে মূল কাজ দুটি — “k-তম উপস্থিত কে?” আর “একে মুছুন” — আর segment tree-র descent দুটোকেই O(log n)-এ সামলায়।
মূল উপলব্ধি: segment tree শুধু “রেঞ্জের যোগফল”-এর যন্ত্র নয়; গণনার tree-র ওপর descent একটি পরিবর্তনশীল তালিকায় “k-তম” প্রশ্নের উত্তর দেয় — পুরোনো ধাঁধা থেকে আধুনিক কনটেস্ট পর্যন্ত।
অনুশীলন
খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।
-
হাতে trace করুন: a = [3, 1, 4, 1, 5, 9] (n = 6, 0-based) দিয়ে একটি sum segment tree আঁকুন (1-based index, সন্তান 2v আর 2v + 1, মাঝখান ⌊(tl + tr)/2⌋)। প্রতিটি node-এর index, রেঞ্জ আর মান লিখুন। তারপর query(2, 4) চালিয়ে বলুন কোন node পুরো ভেতরে, কোনটি বাইরে, কোনটি আংশিক।
Tree: 1 [0-5] = 23; 2 [0-2] = 8; 3 [3-5] = 15; 4 [0-1] = 4; 5 [2] = 4; 6 [3-4] = 6; 7 [5] = 9; 8 [0] = 3; 9 [1] = 1; 12 [3] = 1; 13 [4] = 5. Index 10, 11 ব্যবহার হয় না (node 5 একটি leaf), সবচেয়ে বড় index 13 > 2n = 12 — তাই 2n ঘর এখানে যথেষ্ট নয়, 4n = 24 যথেষ্ট।
query(2, 4): 1 [0-5] আংশিক → 2 [0-2] আংশিক → 4 [0-1] বাইরে (0), 5 [2] ভেতরে (4); 3 [3-5] আংশিক → 6 [3-4] ভেতরে (6), 7 [5] বাইরে (0). উত্তর 4 + 6 = 10 = 4 + 1 + 5 ✓. দেখা হয়েছে ৭টি node, আংশিক ৩টি (প্রতি স্তরে ≤ ২)।
-
Combine হাতে চালান: max-subarray tree-তে (প্রতিটি node-এ sum, pref, suf, best) বাঁ অংশ [3, −4, 2] আর ডান অংশ [5, −1]. প্রথমে দুই অংশের চারটি মান আলাদা বের করুন, তারপর অংশ ৬-এর
combineদিয়ে পুরো [3, −4, 2, 5, −1]-এর মান বের করে সরাসরি হিসাবের সাথে মেলান।বাঁ: sum = 1; pref = max(3, −1, 1) = 3; suf = max(2, −2, 1) = 2; best = 3. ডান: sum = 4; pref = max(5, 4) = 5; suf = max(−1, 4) = 4; best = 5.
Combine: sum = 1 + 4 = 5; pref = max(3, 1 + 5) = 6; suf = max(4, 4 + 2) = 6; best = max(3, 5, 2 + 5) = 7. সরাসরি যাচাই: সেরা subarray [2, 5] = 7 ✓ (মাঝখান পার হওয়াটিই জিতেছে); সেরা prefix 3 − 4 + 2 + 5 = 6 ✓; সেরা suffix 2 + 5 − 1 = 6 ✓.
-
ভেবে বলুন: (ক) n = 8-এর tree-তে query(0, 7) আর query(3, 4) — কোনটিতে বেশি node দেখা হয়, আর কেন? (খ) iterative sum tree-তে n = 3, a = [4, 7, 1] হলে t অ্যারে (index 1..5) কী হবে? sum[0, 3) কোন কোন node নেয়?
(ক) query(0, 7) মাত্র ১টি node দেখে — root নিজেই পুরো ভেতরে। query(3, 4) সবচেয়ে খারাপদের একটি: রেঞ্জটি ঠিক মাঝখানের সীমানা পার হয়, তাই root আংশিক, তারপর [0-3] আর [4-7] দুটিই আংশিক, [2-3] আর [4-5] আংশিক, আর [0-1], [6-7] বাইরে — মোট 1 + 2 + 4 + 4 = ১১টি node, যদিও উত্তর মাত্র দুটি leaf. বড় রেঞ্জ মানেই বেশি কাজ নয়; কাজ নির্ভর করে সীমানা কত গভীরে tree-কে কাটে।
(খ) leaf: t[3] = 4, t[4] = 7, t[5] = 1. তারপর v = 2: t[2] = t[4] + t[5] = 8; v = 1: t[1] = t[2] + t[3] = 12. তাই t = [_, 12, 8, 4, 7, 1]. query [0, 3): l = 3, r = 6. l বিজোড় → t[3] = 4 নিন, l = 4; r জোড়. l = 2, r = 3. l জোড়; r বিজোড় → r = 2, t[2] = 8 নিন. l = 1, r = 1, থামুন। উত্তর 4 + 8 = 12 ✓. লক্ষ করুন, node 2-এর “রেঞ্জ” হলো a[1..2] আর node 3 হলো a[0] — n 2-এর ঘাত না হলে iterative tree-র node-গুলো সাজানো tree-র মতো দেখায় না, তবু সঠিক।
-
কোড লিখুন: একটি max segment tree-র ওপর
firstAtLeast(x)লিখুন: সবচেয়ে বাঁয়ের index i, যাতে a[i] ≥ x; না থাকলে −1. সময় O(log n) হতে হবে। a = [5, 8, 6, 3, 2, 10, 2, 6]-এ x = 9 আর x = 11 দিয়ে হাতে মেলান।অংশ ৮-এর descent-ই, শুধু শর্ত বদলেছে: বাঁ সন্তানের max ≥ x হলে উত্তর বাঁয়ে, নইলে ডানে। k বাদ দেওয়ার দরকার নেই, কারণ max-এ “জমা” হয় না।
// mx[] is a max segment tree (1-based, children 2v and 2v+1) over a[0..n-1] int firstAtLeast(int x) { if (mx[1] < x) return -1; // nobody reaches x int v = 1, tl = 0, tr = n - 1; while (tl != tr) { int tm = (tl + tr) / 2; if (mx[2 * v] >= x) { v = 2 * v; tr = tm; } // leftmost answer is on the left else { v = 2 * v + 1; tl = tm + 1; } // left half is all below x } return tl; }হাতে: max tree-তে root = 10, [0-3] = 8, [4-7] = 10, [4-5] = 10. x = 9: root 10 ≥ 9; বাঁ [0-3] = 8 < 9 → ডানে [4-7]; বাঁ [4-5] = 10 ≥ 9 → বাঁয়ে; বাঁ [4] = 2 < 9 → ডানে [5]. উত্তর 5. x = 11: root 10 < 11, উত্তর −1. সময় O(log n) — এক পথ; জায়গা O(1) বাড়তি।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমRecursive segment tree-তে point update(pos, val)-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- root (রেঞ্জ [0, n − 1]) থেকে শুরু করা
- মাঝখান tm বের করে pos ≤ tm হলে বাঁ সন্তানে, নইলে ডান সন্তানে নামা
- leaf-এ পৌঁছে তার মান val করা
- ফেরার পথে প্রতিটি node-এর মান দুই সন্তানের মান জুড়ে আবার হিসাব করা
- root-এর মান হালনাগাদ হলে কাজ শেষ — মোট উচ্চতা + 1 টি node বদলেছে
1-based অ্যারেতে রাখা segment tree-তে node 6-এর দুই সন্তান আর parent-এর index কী?
- সন্তান 7 ও 8, parent 3
- সন্তান 12 ও 13, parent 3
- সন্তান 13 ও 14, parent 2
- সন্তান 11 ও 12, parent 5
n টি উপাদানের recursive segment tree-তে range query tree-র একটি স্তরে সর্বোচ্চ কয়টি node দেখে?
- ১টি
- ২টি
- ৪টি
- স্তরের সব node, n পর্যন্ত
নিচের কোন প্রশ্নটি সাধারণ segment tree-তে (প্রতিটি node-এ একটি ছোট, নির্দিষ্ট আকারের সারাংশ রেখে) দক্ষভাবে করা যায় না?
- a[l..r]-এর সব সংখ্যার gcd
- a[l..r]-এর সর্বনিম্ন মান আর সেটি কতবার আছে
- a[l..r]-এর ভেতরে সর্বোচ্চ যোগফলের subarray
- a[l..r]-এর median
n = 105 উপাদান আর 105 টি মিশ্র কাজ (point update আর range sum প্রশ্ন)। কোন সমাধানে মোট সময় সবচেয়ে কম?
- Segment tree (বা Fenwick tree): O(n + q log n)
- Prefix sum, প্রতিটি update-এর পর নতুন করে বানিয়ে
- প্রতিটি প্রশ্নে সরাসরি লুপ
- Sparse table, প্রতিটি update-এর পর নতুন করে বানিয়ে
Recursive segment tree (সন্তান 2v আর 2v + 1, root 1) রাখতে অ্যারের আকার সাধারণত 4n নেওয়া হয়। কেন 2n যথেষ্ট নয়?
- কারণ node-এর সংখ্যা 2n-এর বেশি হতে পারে
- কারণ n 2-এর ঘাত না হলে কিছু index ফাঁকা থাকে আর সবচেয়ে বড় index 2n ছাড়াতে পারে
- কারণ lazy propagation-এর জন্য বাড়তি জায়গা রাখতে হয়
- কারণ recursion-এর stack-ও এই অ্যারেতে থাকে
একটি অ্যারে কখনো বদলায় না, আর লাখ লাখ range minimum প্রশ্নের প্রতিটির উত্তর যত দ্রুত সম্ভব চাই। কোনটি সবচেয়ে উপযুক্ত?
- Prefix minimum অ্যারে
- Fenwick tree
- Sparse table: প্রতি প্রশ্ন O(1)
- Segment tree: প্রতি প্রশ্ন O(log n)
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Segment tree: প্রতিটি node একটি রেঞ্জের সারাংশ, parent = বাঁ ∘ ডান; উচ্চতা ⌈log₂ n⌉, node 2n − 1, build Θ(n).
- অ্যারেতে: 1-based-এ সন্তান 2v, 2v + 1 (0-based-এ 2v + 1, 2v + 2); recursive রূপে 4n ঘর, iterative রূপে 2n.
- Point update: leaf থেকে root পর্যন্ত একটি পথ, Θ(log n); leaf বদলে ওপরের সবাইকে আবার হিসাব।
- Range query: ভেতরে → মান নিন, বাইরে → identity, আংশিক → দুই সন্তান; প্রতি স্তরে ≤ ৪ node, তাই O(log n).
- যেকোনো সহযোজী কাজ (sum, min, max, gcd, XOR, struct) চলে, median নয়; descent দিয়ে “প্রথম prefix ≥ k” O(log n); বদল না থাকলে sparse table, শুধু যোগে Fenwick, রেঞ্জ আপডেটে lazy (পর্ব ৩৫)।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৫ · Lazy propagation — রেঞ্জ আপডেটপরবর্তী পর্বরেঞ্জে যোগ বা মান বসানো, আর রেঞ্জের প্রশ্ন — দুটোই O(log n)-এ।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ১৭ · Prefix sum আর difference arrayআগের ভিত্তিবদল ছাড়া রেঞ্জের যোগফল O(1)-এ — segment tree-র শুরুর বিন্দু।
- পর্ব ২৩ · Heap, heapsort আর priority queueআগের ভিত্তিঅ্যারেতে binary tree রাখার একই 2i/2i + 1 কৌশল।
- C++ প্রোগ্রামিং · STL algorithmপাশের ট্র্যাক
min_element,accumulateআর brute force যাচাইয়ের তৈরি হাতিয়ার। - Python প্রোগ্রামিং · অ্যালগরিদম ও কনটেস্টে Pythonপাশের ট্র্যাকRecursion-এর খরচ, দ্রুত ইনপুট আর কনটেস্টে Python-এর কৌশল।