এই পর্বে যা শিখবেন
- Ordered rooted tree, universal address system আর ঠিকানার lexicographic order
- Preorder, inorder, postorder-এর recursive সংজ্ঞা, level order, আর সব traversal-এর Θ(n) সময়ের প্রমাণ
- Expression tree থেকে prefix, infix আর postfix লেখা, আর stack দিয়ে হিসাবের সঠিকতার প্রমাণ
- Shunting-yard ধারণায় infix থেকে postfix: precedence আর associativity কীভাবে কাজ করে
- Preorder আর inorder থেকে binary tree ফেরত পাওয়ার অনন্যতার প্রমাণ, আর preorder + postorder কেন যথেষ্ট নয়
১ · Ordered rooted tree আর universal address
বাজারঘরের অ্যাপে পণ্যের মেনু খুললে প্রথমে বড় বিভাগ (মুদি, গ্যাজেট, পোশাক), তার ভেতরে ছোট বিভাগ, তার ভেতরে আরও ছোট। মেনুটি একটি গাছ। কিন্তু অ্যাপকে মেনুটি পর্দায় দেখাতে, সার্চের সূচি বানাতে বা প্রতিটি বিভাগের মোট পণ্য গুনতে হলে গাছের প্রতিটি ঘরে একটি নির্দিষ্ট ক্রমে একবার করে যেতে হয়। কোন ক্রমে যাব, সেই নিয়মের নাম traversal. সরকারি চিঠির নথি নম্বর “৩.২.১” বা বইয়ের সূচিপত্রের “অধ্যায় ২.৩” একই ধারণা থেকে আসে।
প্রথমে কয়েকটি শব্দ ঝালিয়ে নিই; বিস্তারিত সংজ্ঞা আর প্রমাণ পর্ব ৬৫ “Tree — ধর্ম, rooted tree আর m-ary tree-এর হিসাব”-এ। একটি tree হলো cycle-হীন connected graph. একটি rooted tree-তে একটি vertex-কে root ধরা হয়, আর প্রতিটি edge-কে root থেকে দূরে যাওয়ার দিকে ভাবা হয়। তখন প্রতিটি অ-root vertex-এর ঠিক একজন parent; যাদের কোনো সন্তান নেই তারা পাতা (leaf), বাকিরা internal vertex. একটি vertex v আর তার সব বংশধর মিলে v-এর subtree. Root থেকে v পর্যন্ত path-এর দৈর্ঘ্য v-এর level, আর সবচেয়ে বড় level হলো গাছের height.
একটি ordered rooted tree হলো এমন rooted tree, যেখানে প্রতিটি internal vertex-এর সন্তানদের একটি নির্দিষ্ট ক্রম আছে: প্রথম, দ্বিতীয়, তৃতীয়, … (ছবিতে বাঁ থেকে ডানে)। ক্রম বদলালে গাছ বদলায়: সন্তান “মুদি, গ্যাজেট” আর “গ্যাজেট, মুদি” দুটি আলাদা ordered tree. Binary tree-এ আরও কড়া নিয়ম: প্রতিটি vertex-এর সন্তান বড়জোর দুটি, আর প্রতিটি সন্তান হয় “বাঁ” নয় “ডান”, তাই একমাত্র সন্তানও বাঁ না ডান, তা জানা থাকে।
ক্রম থাকলে প্রতিটি vertex-কে একটি নিখুঁত ঠিকানা দেওয়া যায়।
Universal address system-এ root-এর ঠিকানা 0 (বা খালি ক্রম)। কোনো vertex-এর ঠিকানা A হলে তার kটি সন্তানের ঠিকানা ক্রমে A.1, A.2, …, A.k; root-এর সন্তানদের ঠিকানা শুধু 1, 2, …, k. তাই level ℓ-এর একটি vertex-এর ঠিকানা ℓটি ধনাত্মক পূর্ণসংখ্যার একটি ক্রম a1.a2.⋯.aℓ, যা root থেকে কোন পথে নামতে হবে তা বলে দেয়: “a1-তম সন্তান, তারপর তার a2-তম সন্তান, …”। গ্রন্থাগারের Dewey পদ্ধতির নামে একে কখনো Dewey label-ও বলা হয়।
বাজারঘরের বিভাগ-গাছ আর প্রতিটি ঘরের ঠিকানা
৯টি vertex, height 3. প্রতিটি বাক্সের নিচে তার universal address. সবুজ বাক্সগুলো পাতা।
ঠিকানাগুলো তুলনা করার নিয়ম হলো lexicographic order, যা অভিধানে শব্দ সাজানোর নিয়মের মতো (পর্ব ৫৬-এ সংজ্ঞা আর প্রমাণ যে এটি একটি total order). দুটি ঠিকানা x = a1.⋯.am আর y = b1.⋯.bn-এর জন্য x < y যদি (ক) x হলো y-এর একটি প্রকৃত prefix (m < n আর প্রথম mটি সংখ্যা মেলে), অথবা (খ) প্রথম যে স্থানে i তারা আলাদা, সেখানে ai < bi. Root-এর খালি ঠিকানা সবার prefix, তাই সবার আগে। উদাহরণ: 1.2 < 2 (প্রথম সংখ্যা 1 < 2), 2.1 < 2.1.1 (prefix), 2.1.1 < 2.2 (দ্বিতীয় স্থানে 1 < 2). ছবির ৯টি ঠিকানা সাজালে পাই: 0, 1, 1.1, 1.2, 2, 2.1, 2.1.1, 2.2, 3.
ঠিকানাকে লেখা (string) হিসেবে সাজালে ভুল হয়: string-তুলনায় "2.10" আসে "2.9"-এর আগে, কারণ অক্ষর '1' < '9'. সঠিক তুলনা প্রতিটি অংশকে সংখ্যা হিসেবে ধরে, অর্থাৎ ঠিকানা হলো পূর্ণসংখ্যার tuple. Python-এ sorted(["2.9", "2.10"]) দেয় ['2.10', '2.9'], কিন্তু tuple (2, 9) < (2, 10).
২ · Preorder, inorder, postorder — তিনটি recursive সংজ্ঞা
একটি ordered rooted tree T-এর root r, আর তার সন্তানদের subtree ক্রমে T1, T2, …, Tk (k = 0 মানে T একটি একা পাতা)। তিনটি traversal-এর সংজ্ঞা পর্ব ১২-এর structural recursion: পাতার জন্য একটি নিয়ম, আর বড় গাছের জন্য ছোট subtree-গুলোর ফল জোড়া দেওয়ার নিয়ম।
- Preorder(T) = r, তারপর preorder(T1), preorder(T2), …, preorder(Tk).
- Inorder(T) = inorder(T1), তারপর r, তারপর inorder(T2), …, inorder(Tk).
- Postorder(T) = postorder(T1), …, postorder(Tk), তারপর r.
k = 0 হলে তিনটিই শুধু r. Binary tree-তে inorder-এর স্বাভাবিক রূপ: বাঁ subtree, root, ডান subtree, যেখানে খালি subtree কিছুই দেয় না। ফলে শুধু ডান সন্তান থাকলে root আগে আসে।
মনে রাখার সহজ উপায়: নামের “pre”, “in”, “post” বলে root কখন আসে: আগে, মাঝে, শেষে। প্রতিটি subtree-র ভেতরেও একই নিয়ম আবার খাটে। গাছের বাইরের সীমানা ধরে বাঁ দিক থেকে ঘুরে আসার কথা ভাবুন: প্রতিটি vertex-এর পাশ দিয়ে প্রথমবার যাওয়ার সময় নাম লিখলে preorder, শেষবার পেরোনোর সময় লিখলে postorder. নিচের stepper ছবির বিভাগ-গাছে চারটি ক্রম ধাপে ধাপে দেখায়।
1সব: মুদি, গ্যাজেট, পোশাক2preorder(মুদি)3preorder(সব)4inorder(সব)5postorder(সব)6level order(সব)
| ক্রম | |
| ঠিকানা |
- গাছ। Root “সব”-এর তিন সন্তান ক্রমে মুদি, গ্যাজেট, পোশাক. মুদি-র সন্তান চাল, ডাল; গ্যাজেট-এর সন্তান ফোন, ল্যাপটপ; ফোন-এর একমাত্র সন্তান চার্জার. পাতা পাঁচটি: চাল, ডাল, চার্জার, ল্যাপটপ, পোশাক.
- ছোট subtree আগে। preorder(মুদি) = মুদি, তারপর preorder(চাল) = চাল, তারপর preorder(ডাল) = ডাল. পাতায় recursion থামে। বড় গাছের preorder এমন টুকরো জোড়া দিয়েই তৈরি।
- পুরো preorder. সব, তারপর মুদি-র টুকরো (মুদি চাল ডাল), তারপর গ্যাজেট-এর টুকরো (গ্যাজেট ফোন চার্জার ল্যাপটপ), শেষে পোশাক. নিচের সারির ঠিকানাগুলো লক্ষ করুন: ঠিক lexicographic ক্রম, যা §৩-এ প্রমাণ করব।
- Inorder. প্রথম subtree মুদি-র inorder (চাল মুদি ডাল), তারপর root সব, তারপর গ্যাজেট-এর inorder (প্রথম subtree ফোন-এর inorder চার্জার ফোন, তারপর গ্যাজেট, তারপর ল্যাপটপ), শেষে পোশাক.
- Postorder. সব সন্তান আগে, root শেষে: চাল ডাল মুদি, চার্জার ফোন ল্যাপটপ গ্যাজেট, পোশাক, সব. প্রতিটি বিভাগের মোট পণ্য গুনতে এই ক্রমই লাগে, কারণ সন্তানদের হিসাব আগে শেষ হয়।
- Level order. Root থেকে দূরত্ব অনুযায়ী: level 0-এ সব; level 1-এ মুদি, গ্যাজেট, পোশাক; level 2-এ চাল, ডাল, ফোন, ল্যাপটপ; level 3-এ চার্জার. একই level-এ বাঁ থেকে ডানে (পরের Python ব্লকে চারটিই মেলানো)।
চতুর্থ ক্রমটি depth-first নয়, “breadth-first”: level order আগে কাছের সবাইকে দেখে, তারপর দূরের। এটি rooted tree-তে BFS-এর একটি বিশেষ রূপ, আর এর স্বাভাবিক সঙ্গী একটি queue (লাইন: যে আগে ঢোকে, সে আগে বের হয়)। Root-কে queue-তে রাখুন; তারপর বারবার সামনের vertex বের করে লিখুন আর তার সন্তানদের ক্রমে পেছনে যোগ করুন। Queue-তে সবসময় একটি level-এর শেষ অংশ আর পরের level-এর শুরু থাকে, এই invariant থেকেই প্রমাণ হয় যে vertex-গুলো level অনুযায়ী আসে; graph-এ BFS-এর একই প্রমাণ পর্ব ৫৯-এর §৮-এ।
নিচের ব্লক চারটি traversal একসঙ্গে চালায়, প্রতিটি vertex-এর ঠিকানা বানায়, আর ঠিকানা সাজিয়ে preorder-এর সঙ্গে মেলায়। প্রথমবার চালালে ব্রাউজারে Python (Pyodide) নামতে কয়েক সেকেন্ড লাগে।
from collections import deque
# (name, [children]) ; children are kept in order
T = ("সব", [("মুদি", [("চাল", []), ("ডাল", [])]),
("গ্যাজেট", [("ফোন", [("চার্জার", [])]), ("ল্যাপটপ", [])]),
("পোশাক", [])])
visits = 0
def pre(t):
global visits; visits += 1
return [t[0]] + [x for c in t[1] for x in pre(c)]
def post(t):
return [x for c in t[1] for x in post(c)] + [t[0]]
def ino(t): # first subtree, root, then the rest
if not t[1]:
return [t[0]]
return ino(t[1][0]) + [t[0]] + [x for c in t[1][1:] for x in ino(c)]
def level(t):
q, out = deque([t]), []
while q:
u = q.popleft(); out.append(u[0]); q.extend(u[1])
return out
def addresses(t, a=()): # universal address: path of child numbers
yield a, t[0]
for i, c in enumerate(t[1], 1):
yield from addresses(c, a + (i,))
P = pre(T)
print("preorder :", " ".join(P))
print("inorder :", " ".join(ino(T)))
print("postorder:", " ".join(post(T)))
print("level :", " ".join(level(T)))
A = list(addresses(T))
print("addresses:", ", ".join(".".join(map(str, a)) or "0" for a, _ in A))
lex = [name for a, name in sorted(A)] # tuples compare lexicographically
print("preorder == lexicographic order of addresses:", lex == P)
print("nodes:", len(A), "| calls made by preorder:", visits)আসল আউটপুট (ব্রাউজারের Python):
preorder : সব মুদি চাল ডাল গ্যাজেট ফোন চার্জার ল্যাপটপ পোশাক
inorder : চাল মুদি ডাল সব চার্জার ফোন গ্যাজেট ল্যাপটপ পোশাক
postorder: চাল ডাল মুদি চার্জার ফোন ল্যাপটপ গ্যাজেট পোশাক সব
level : সব মুদি গ্যাজেট পোশাক চাল ডাল ফোন ল্যাপটপ চার্জার
addresses: 0, 1, 1.1, 1.2, 2, 2.1, 2.1.1, 2.2, 3
preorder == lexicographic order of addresses: True
nodes: 9 | calls made by preorder: 9
চারটি লাইন stepper-এর চারটি ক্রম হুবহু দেয়। ঠিকানাগুলো Python-এর tuple, আর tuple-এর তুলনা নিজেই lexicographic, তাই sorted(A) ঠিকানার ক্রমে সাজায়; ফল preorder-এর সমান (True). শেষ লাইন দেখায় preorder ফাংশন ঠিক ৯ বার ডাকা হয়েছে, প্রতিটি vertex-এর জন্য একবার: §৩-এর Θ(n) প্রমাণের মূল কথা। Level order-এ recursion নেই, শুধু একটি deque (দুই মুখ খোলা queue).
৩ · দুটি উপপাদ্য — ঠিকানার ক্রম আর Θ(n) সময়
যেকোনো ordered rooted tree-তে preorder ঠিক সেই ক্রমে vertex দেয়, যে ক্রমে তাদের universal address lexicographic order-এ সাজানো থাকে।
প্রমাণ (গাছের vertex-সংখ্যা n-এর ওপর strong induction; ঠিকানাকে root থেকে মাপা পূর্ণসংখ্যার ক্রম ধরি, root-এর ঠিকানা খালি ক্রম):
- ভিত্তি n = 1: একটিই vertex, দুই তালিকাই শুধু root. — base case
- ধরুন n ≥ 2, root r, সন্তানদের subtree T1, …, Tk, আর n-এর চেয়ে ছোট সব গাছে দাবি সত্য। Ti-এর প্রতিটি vertex-এর T-তে ঠিকানা হলো “i.” তারপর Ti-এর ভেতরে তার নিজের ঠিকানা। — সংজ্ঞা ১.২
- Lexicographic order-এ খালি ক্রম সবার আগে, তাই r প্রথম। i < j হলে Ti-এর প্রতিটি ঠিকানা Tj-এর প্রতিটি ঠিকানার আগে, কারণ প্রথম স্থানেই i < j. — lexicographic-এর নিয়ম (খ)
- একই Ti-এর দুটি ঠিকানার প্রথম অংশ i একই, তাই তুলনা বাকি অংশে, অর্থাৎ Ti-এর ভেতরের ঠিকানায়; হাইপোথিসিস অনুযায়ী সেই ক্রম preorder(Ti). — inductive hypothesis, |Ti| < n
- তাই সাজানো তালিকা = r, preorder(T1), …, preorder(Tk) = preorder(T). ∎ — সংজ্ঞা ২.১
এজন্যই বইয়ের সূচিপত্র (১, ১.১, ১.২, ২, …) আসলে অধ্যায়-গাছের preorder: অধ্যায়ের নাম আগে, তারপর তার উপ-অধ্যায়। একই কারণে কম্পিউটারের ফোল্ডার-তালিকা দেখানোর বহু টুল (যেমন সব ফাইলের পূর্ণ path সাজিয়ে দেখানো) প্রায় preorder দেয়।
n vertex-এর ordered rooted tree-তে preorder, inorder আর postorder প্রতিটি vertex ঠিক একবার তালিকায় দেয়। আর প্রতিটি vertex-এ কাজ সন্তান-সংখ্যার একটি রৈখিক ফাংশন দিয়ে সীমিত হলে মোট সময় Θ(n).
- একবার করে: n-এর ওপর strong induction. পাতার জন্য স্পষ্ট। n ≥ 2 হলে T1, …, Tk-এর vertex-সেট পরস্পর আলাদা, আর root ছাড়া T-এর প্রতিটি vertex ঠিক একটি Ti-তে। হাইপোথিসিস অনুযায়ী প্রতিটি traversal(Ti) Ti-এর প্রত্যেককে একবার দেয়, আর root একবার যোগ হয়। — structural recursion-এর গঠন
- ওপরের সীমা: vertex v-কে ঘিরে একটি ডাকে কাজ ≤ c · (1 + d(v)), যেখানে d(v) = v-এর সন্তান-সংখ্যা আর c একটি ধ্রুবক (নিজেকে লেখা, আর প্রতিটি সন্তানকে ডাকা ও ফল জোড়া)। — অনুমান
- প্রতিটি অ-root vertex ঠিক একজনের সন্তান, তাই ∑v d(v) = n − 1. মোট কাজ ≤ c · ∑(1 + d(v)) = c(2n − 1). — parent অনন্য
- নিচের সীমা: তালিকায় nটি নাম লিখতেই অন্তত nটি ধাপ। মিলিয়ে সময় Θ(n). ∎
একটি সতর্কতা: ধাপ ২-এর অনুমান কোডের ওপর নির্ভর করে। §২-এর Python কোড তালিকা জোড়া দিতে নতুন list বানায়, যা গভীর গাছে বাড়তি খরচ আনতে পারে; বাস্তবের কোড একটি ফল-তালিকায় append করে বা generator দিয়ে Θ(n) রাখে। আরেকটি বাস্তব কথা: recursion-এর গভীরতা গাছের height-এর সমান, তাই খুব লম্বা “শিকলের মতো” গাছে recursion-এর সীমা ছাড়িয়ে যেতে পারে; তখন নিজের হাতে একটি stack রেখে একই traversal লেখা হয়। সেই কোড আর traversal-এর অনেক প্রয়োগ DSA কোর্সের পর্ব ২৬-এ।
প্রয়োগে কোন ক্রম কখন লাগে, তার একটি ছোট তালিকা:
- Preorder: মেনু বা ফোল্ডার-গাছ ইন্ডেন্ট করে ছাপা (আগে বিভাগের নাম, তারপর ভেতরের সব), গাছের কপি বানানো, গাছকে একটি লাইনে লিখে সংরক্ষণ।
- Postorder: প্রতিটি ফোল্ডারের মোট আকার হিসাব (আগে ভেতরের ফাইল-ফোল্ডার, তারপর নিজে), পুরো ফোল্ডার মুছে ফেলা (ভেতরের সব আগে মুছতে হয়), বিভাগ-গাছে মোট পণ্য গোনা, আর §৫-এর মতো expression-এর মান বের করা।
- Inorder: binary search tree-এ চাবিগুলো ছোট থেকে বড় ক্রমে দেয়; আর expression tree থেকে চেনা infix লেখা।
- Level order: root-এর সবচেয়ে কাছের vertex আগে খোঁজা, স্তর অনুযায়ী ছাপা, প্রতিটি level-এ কয়জন তা গোনা।
৪ · Expression tree — prefix, infix আর postfix
বাজারঘরের একটি অর্ডারে ৩ প্যাকেট চা (প্রতিটি 120 টাকা) আর ২ প্যাকেট বিস্কুট (প্রতিটি 45 টাকা), সঙ্গে 50 টাকার কুপন। মোট = 3 × 120 + 2 × 45 − 50. এই হিসাবের একটি গঠন আছে: আগে দুটি গুণ, তারপর যোগ, শেষে বিয়োগ। গঠনটি একটি গাছ।
Basis: একটি সংখ্যা বা variable একটি expression tree, যার একমাত্র vertex একটি পাতা। Recursive step: E1 আর E2 expression tree আর ∘ ∈ {+, −, ×, ÷} হলে, root-এ ∘, বাঁ subtree E1 আর ডান subtree E2 নিয়ে গঠিত binary tree-ও একটি expression tree. এর মান val: পাতার মান তার সংখ্যা, আর val(∘, E1, E2) = val(E1) ∘ val(E2).
3 × 120 + 2 × 45 − 50-এর expression tree
ভেতরের vertex-এ operator, পাতায় সংখ্যা. বাঁ আর ডান subtree-র ক্রম গুরুত্বপূর্ণ: বিয়োগে বাঁ দিক থেকে ডান দিক বাদ যায়।
এই গাছের তিনটি traversal তিনটি পরিচিত লেখার ধরন দেয়:
- Preorder → prefix বা Polish notation:
− + × 3 120 × 2 45 50. Operator আগে, তারপর তার দুই operand. - Postorder → postfix বা reverse Polish notation (RPN):
3 120 × 2 45 × + 50 −. দুই operand আগে, operator পরে। - Inorder → infix: 3 × 120 + 2 × 45 − 50, আমাদের চেনা লেখা, তবে প্রয়োজনে বন্ধনীসহ।
নাম এসেছে পোল্যান্ডের যুক্তিবিদ Jan Łukasiewicz থেকে, যিনি ১৯২০-এর দশকে logic-এর সূত্র বন্ধনী ছাড়া লেখার জন্য prefix রূপ ব্যবহার করেন (ইতিহাস ভাবনার প্রশ্ন ১-এ)।
Infix-এ কেন বন্ধনী লাগে
8 − 3 − 2 লেখাটি দুটি আলাদা গাছ থেকে আসতে পারে: (8 − 3) − 2 = 3, অথবা 8 − (3 − 2) = 7. দুই গাছের বন্ধনী-ছাড়া inorder একই: 8, −, 3, −, 2. তাই inorder একা গাছ চেনায় না; infix-এ বন্ধনী, অথবা precedence (কোন operator আগে) আর associativity (সমান operator পরপর এলে কোনটি আগে) নিয়মের চুক্তি লাগে (§৬)। Prefix আর postfix-এ এই সমস্যা নেই: (8 − 3) − 2-এর postfix 8 3 − 2 −, আর 8 − (3 − 2)-এর 8 3 2 − −. এটি কাকতালীয় নয়।
শুধু binary operator ধরলে: (ক) প্রতিটি expression tree-তে পাতার সংখ্যা = operator-এর সংখ্যা + 1. (খ) টোকেনের একটি তালিকা w কোনো expression tree-এর postfix, যদি এবং কেবল যদি w-এর প্রতিটি অশূন্য শুরু-অংশে (prefix) “operand-সংখ্যা − operator-সংখ্যা” ≥ 1, আর পুরো w-এ তা ঠিক 1. (গ) এমন w ঠিক একটি গাছের postfix.
- (ক) Structural induction: পাতায় 1 = 0 + 1. (∘, E1, E2)-এ পাতা = (o1 + 1) + (o2 + 1) = (o1 + o2 + 1) + 1, আর operator o1 + o2 + 1. এটি পর্ব ১২-এর full binary tree-এর “পাতা = internal + 1”-এরই রূপ। — structural induction
- (খ) ⇒ post(E) = post(E1) post(E2) ∘. হাইপোথিসিস অনুযায়ী post(E1)-এর শুরু-অংশে ফারাক ≥ 1, আর পুরোটায় 1; তারপর post(E2)-এর শুরু-অংশ যোগ হলে 1 + (≥ 1) ≥ 2; পুরো post(E2) শেষে 1 + 1 = 2; শেষ ∘ যোগ করলে 2 − 1 = 1. — structural induction
- (খ) ⇐ আর (গ) w-কে বাঁ থেকে পড়ে একটি stack-এ গাছ বানান: সংখ্যা হলে একক-পাতার গাছ push; operator হলে দুটি গাছ pop করে (প্রথমটি ডান, দ্বিতীয়টি বাঁ) নতুন গাছ push. Stack-এর আকার সবসময় ঠিক বর্তমান “ফারাক”, তাই শর্ত অনুযায়ী operator আসার আগে stack-এ অন্তত দুটি গাছ থাকে (operator যোগে ফারাক ≥ 1 থাকে মানে আগে ≥ 2 ছিল), আর শেষে ঠিক একটি গাছ। সেই গাছের postfix w (§৫-এর lemma-র মতো induction). — নির্মাণ
- প্রতিটি ধাপ বাধ্যতামূলক: যেকোনো গাছ E-এর জন্য post(E) = w হলে এই প্রক্রিয়া ঠিক E-ই বানায়, কারণ প্রক্রিয়ায় কোনো বিকল্প নেই। তাই গাছ অনন্য। ∎ — নির্ধারিত (deterministic) প্রক্রিয়া
Prefix-এর জন্য একই কথা, শুধু ডান থেকে বাঁয়ে পড়ে। এই “একভাবেই পড়া যায়” ধারণা পর্ব ১২-এ logic-এর বন্ধনীসহ সূত্রের unique readability হিসেবে দেখেছেন; Łukasiewicz-এর লক্ষ্যই ছিল বন্ধনী বাদ দিয়েও তা রাখা।
৫ · Stack দিয়ে postfix-এর হিসাব আর তার প্রমাণ
Postfix-এর সবচেয়ে বড় সুবিধা: হিসাব করতে বন্ধনী বা precedence জানতে হয় না, একবার বাঁ থেকে ডানে পড়লেই চলে। দরকার শুধু একটি stack, যেখানে শেষে রাখা জিনিস আগে বের হয়, যেমন বিয়েবাড়িতে থালার স্তূপ। নিয়ম:
- টোকেন সংখ্যা হলে stack-এ push.
- টোকেন operator ∘ হলে প্রথমে b = pop, তারপর a = pop, আর a ∘ b push. খেয়াল করুন, যেটি প্রথমে বের হয় সেটি ডান operand.
- শেষে stack-এ ঠিক একটি সংখ্যা থাকে: সেটিই মান।
1321203×425456×7+8509−
| কাজ | |
| stack |
- 3. সংখ্যা, তাই push. Stack [3].
- 120. আবার সংখ্যা, push. Stack [3, 120]; ওপরে 120.
- ×। প্রথম pop দেয় 120 (ডান operand), দ্বিতীয় pop দেয় 3 (বাঁ)। 3 × 120 = 360 push. Stack [360].
- 2. Push. 360 নিচে চুপচাপ অপেক্ষা করে; stack [360, 2].
- 45. Push. Stack [360, 2, 45].
- ×। Pop 45, pop 2, 2 × 45 = 90 push. Stack [360, 90]: দুটি উপ-হিসাব তৈরি।
- +. Pop 90 (ডান), pop 360 (বাঁ), 360 + 90 = 450 push. Stack [450].
- 50. Push. Stack [450, 50].
- −। Pop 50 (ডান), pop 450 (বাঁ), 450 − 50 = 400. Stack-এ একটিই সংখ্যা, 400: অর্ডারের মোট 400 টাকা। ভুল ক্রমে 50 − 450 নিলে উত্তর −400 হতো।
কেন এই সরল নিয়ম সবসময় ঠিক মান দেয়? চাবি হলো একটু শক্তিশালী একটি দাবি প্রমাণ করা, যাতে stack-এ আগে থেকে কিছু থাকলেও কথা খাটে। “শক্ত দাবি প্রমাণ সহজ” কৌশলটি পর্ব ১২-এ structural induction-এর সঙ্গে দেখেছেন।
যেকোনো expression tree E আর যেকোনো stack S-এর জন্য: S থেকে শুরু করে post(E)-এর টোকেনগুলো ওপরের নিয়মে চালালে (শূন্য দিয়ে ভাগ না হলে) শেষে stack হয় S-এর ওপর val(E), অর্থাৎ S · [val(E)], আর S-এর পুরোনো উপাদান কখনো pop হয় না।
প্রমাণ (E-এর ওপর structural induction):
- Basis: E একটি সংখ্যা x. post(E) = x, একটি push; ফল S · [x] = S · [val(E)]. — নিয়ম ১
- Recursive step: E = (∘, E1, E2), তাই post(E) = post(E1), post(E2), ∘. হাইপোথিসিস E1 আর E2-এর জন্য যেকোনো শুরুর stack-এ খাটে। — সংজ্ঞা ২.১
- S থেকে post(E1) চালালে stack S · [v1], যেখানে v1 = val(E1), আর S অক্ষত। — হাইপোথিসিস, শুরু S
- এবার S · [v1] থেকে post(E2) চালালে S · [v1, v2], আর S · [v1] অক্ষত। — হাইপোথিসিস, শুরু S · [v1]
- শেষ টোকেন ∘: প্রথম pop v2 = b, দ্বিতীয় pop v1 = a, push a ∘ b = v1 ∘ v2 = val(E). Stack S · [val(E)], আর S-এর কিছুই pop হয়নি। ∎ — নিয়ম ২, val-এর সংজ্ঞা
খালি S নিলে অনুসিদ্ধান্ত: post(E) চালালে শেষে stack-এ শুধু val(E). ধাপ ৪-এ হাইপোথিসিসটি খালি stack নয়, S · [v1] দিয়ে শুরু হয়েছে; তাই শুধু “খালি stack থেকে শুরু” দাবি নিয়ে induction করলে প্রমাণ আটকে যায়। ধাপ ৫ দেখায় pop-এর ক্রম কেন গুরুত্বপূর্ণ: + আর ×-এ ক্রম উল্টালেও ফল একই (commutative), কিন্তু − আর ÷-এ নয়।
Prefix-এর হিসাবেও একই stack: টোকেন ডান থেকে বাঁয়ে পড়ুন, আর operator এলে এবার প্রথম pop-ই বাঁ operand. বিকল্পভাবে prefix সরাসরি recursion দিয়ে পড়া যায়: প্রথম টোকেন operator হলে বাকি থেকে একটি পুরো expression পড়ুন (বাঁ), তারপর আরেকটি (ডান)। নিচের ব্লক এলোমেলো expression tree বানিয়ে তিনটি পদ্ধতি মেলায়: postfix-stack, prefix-stack, আর বন্ধনীসহ infix লিখে Python-এর নিজের eval. সংখ্যাগুলো Fraction (ভগ্নাংশ), তাই ভাগে দশমিকের ভুল নেই। eval এখানে নিরাপদ, কারণ string-টি আমাদের কোডই বানায়, আর তাতে শুধু অঙ্ক, চার operator আর বন্ধনী থাকে।
import random; from fractions import Fraction as F
OPS = {"+": lambda a, b: a + b, "-": lambda a, b: a - b,
"*": lambda a, b: a * b, "/": lambda a, b: a / b}
def rand_tree(depth): # leaf = int, node = (op, left, right)
if depth == 0 or random.random() < 0.3:
return random.randint(1, 9)
return (random.choice("+-*/"), rand_tree(depth - 1), rand_tree(depth - 1))
def prefix(t): return [t] if isinstance(t, int) else [t[0]] + prefix(t[1]) + prefix(t[2])
def postfix(t): return [t] if isinstance(t, int) else postfix(t[1]) + postfix(t[2]) + [t[0]]
def infix(t): # fully parenthesised, ready for eval
return f"F({t})" if isinstance(t, int) else f"({infix(t[1])} {t[0]} {infix(t[2])})"
def eval_postfix(tokens): # left to right, one stack
st = []
for x in tokens:
if x in OPS:
b = st.pop(); a = st.pop() # right operand comes off first
st.append(OPS[x](a, b))
else:
st.append(F(x))
assert len(st) == 1
return st[0]
def eval_prefix(tokens): # same idea, read right to left
st = []
for x in reversed(tokens):
st.append(OPS[x](st.pop(), st.pop()) if x in OPS else F(x))
return st[0]
t = ("-", ("+", ("*", 3, 120), ("*", 2, 45)), 50)
print(*prefix(t), "|", *postfix(t), "|", eval_postfix(postfix(t)))
random.seed(66); ok = skipped = 0
for _ in range(2000):
t = rand_tree(4)
try: truth = eval(infix(t), {"F": F})
except ZeroDivisionError: skipped += 1; continue
assert eval_postfix(postfix(t)) == truth == eval_prefix(prefix(t)); ok += 1
print(ok, "random trees: postfix stack = prefix stack = eval;", skipped, "had division by 0")আসল আউটপুট (ব্রাউজারের Python):
- + * 3 120 * 2 45 50 | 3 120 * 2 45 * + 50 - | 400
1971 random trees: postfix stack = prefix stack = eval; 29 had division by 0
প্রথম লাইন অর্ডারের গাছের prefix, postfix আর stack-এর ফল 400, ঠিক stepper-এর মতো। তারপর ২০০০টি এলোমেলো গাছের ১৯৭১টিতে তিন পদ্ধতির মান হুবহু এক (একটিও assert ব্যর্থ হয়নি); বাকি ২৯টিতে কোথাও শূন্য দিয়ে ভাগ ছিল, তাই বাদ। eval_prefix-এ OPS[x](st.pop(), st.pop()) লক্ষ করুন: Python বাঁ থেকে ডানে argument মূল্যায়ন করে, তাই প্রথম pop বাঁ operand হয়, যা ডান থেকে পড়ার জন্য ঠিক দরকার। Postfix-এর কোডে উল্টো: আগে b, পরে a.
৬ · Infix থেকে postfix — precedence, associativity আর shunting-yard
মানুষ infix লেখে, কিন্তু যন্ত্রের জন্য postfix বা গাছ সুবিধাজনক। তাই প্রতিটি calculator আর compiler-কে infix থেকে গাছ বা postfix বানাতে হয়। এর জন্য দুটি চুক্তি লাগে:
- Precedence: × আর ÷ “বেশি শক্ত করে বাঁধে”, + আর −-এর চেয়ে। তাই 3 + 4 × 5 মানে 3 + (4 × 5) = 23.
- Associativity: একই precedence-এর operator পরপর এলে কোনটি আগে। +, −, ×, ÷ left-associative: 8 − 3 − 2 = (8 − 3) − 2 = 3. Python-এর ঘাত
**right-associative:2 ** 3 ** 2= 2 ** (3 ** 2) = 512, 64 নয়।
Edsger Dijkstra-র shunting-yard পদ্ধতি (১৯৬০-এর দশকের শুরুতে বর্ণিত বলে উল্লেখ করা হয়; নামটি রেলের শান্টিং ইয়ার্ড থেকে, যেখানে বগি পাশের লাইনে সরিয়ে রেখে পরে ক্রম বদলে জোড়া হয়) একটি operator-stack রাখে। টোকেন বাঁ থেকে পড়া হয়:
- সংখ্যা সরাসরি output-এ যায়।
- Operator ∘ এলে, যতক্ষণ stack-এর ওপরে এমন operator আছে যা ∘-এর চেয়ে বেশি precedence-এর, অথবা সমান precedence-এর আর ∘ left-associative, ততক্ষণ সেটি pop করে output-এ যোগ করুন; তারপর ∘ push.
- “(” push হয়; “)” এলে “(” না পাওয়া পর্যন্ত সব pop করে output-এ, তারপর “(” ফেলে দিন।
- শেষে stack-এ যা থাকে, ওপর থেকে একে একে output-এ।
নিয়ম ২-এর মর্ম: stack-এর ওপরের operator যদি নতুনটির চেয়ে আগে হিসাব হওয়ার কথা, তবে তার দুই operand ইতিমধ্যে output-এ আছে, তাই এখনই তাকে বের করে দেওয়া নিরাপদ। 8 − 3 − 2-এ দ্বিতীয় − আসার সময় প্রথম −-কে বের করে দেয় (সমান precedence, left-associative), ফলে 8 3 − 2 −. Right-associative **-এর জন্য “সমান হলে বের করো” অংশটি বাদ যায়, তাই 2 3 2 ** **. Unary minus (একটি operand-এর বিয়োগ চিহ্ন) আলাদা টোকেন হিসেবে সামলাতে হয়; Python-এ -2 ** 2 = −4, কারণ ** unary minus-এর চেয়ে শক্ত করে বাঁধে। পূর্ণ কোড, বন্ধনী মেলানো আর ভুল ইনপুট সামলানো DSA কোর্সের পর্ব ১৩-এ।
কাজের বেলায় compiler infix থেকে সরাসরি গাছ বানায়, যার নাম abstract syntax tree (AST): বন্ধনী আর ফাঁকা জায়গার মতো “লেখার খুঁটিনাটি” বাদ দিয়ে শুধু গঠন। Python নিজেই আপনার কোড প্রথমে AST-তে রূপান্তর করে, তারপর bytecode বানায় (Python প্রোগ্রামিং কোর্সের পর্ব ০৫)। আর Python-এর ast module সেই গাছ আমাদের হাতে দেয়। তাই একটি সুন্দর যাচাই সম্ভব: আমাদের shunting-yard-এর output আর Python-এর নিজের AST-এর postorder কি এক?
import ast, random
PREC = {"+": 1, "-": 1, "*": 2, "/": 2} # all left-associative
def shunting_yard(text):
out, ops = [], []
for tk in text.replace("(", " ( ").replace(")", " ) ").split():
if tk.isdigit(): out.append(tk)
elif tk == "(": ops.append(tk)
elif tk == ")":
while ops[-1] != "(": out.append(ops.pop())
ops.pop() # throw away "("
else: # pop while top binds as tight
while ops and ops[-1] != "(" and PREC[ops[-1]] >= PREC[tk]:
out.append(ops.pop())
ops.append(tk)
return out + ops[::-1]
SYM = {ast.Add: "+", ast.Sub: "-", ast.Mult: "*", ast.Div: "/"}
def ast_postfix(n): # postorder walk of Python's AST
if isinstance(n, ast.Constant): return [str(n.value)]
return ast_postfix(n.left) + ast_postfix(n.right) + [SYM[type(n.op)]]
def rand_infix(d): # minimal brackets: (text, precedence)
if d == 0 or random.random() < 0.3: return str(random.randint(1, 9)), 3
op = random.choice("+-*/"); (a, pa), (b, pb) = rand_infix(d - 1), rand_infix(d - 1)
if pa < PREC[op]: a = f"({a})"
if pb <= PREC[op]: b = f"({b})" # right side: equal needs ()
return f"{a} {op} {b}", PREC[op]
s = "3 * 120 + 2 * 45 - 50"
print(" ".join(shunting_yard(s)))
print("8 - 3 - 2 ->", " ".join(shunting_yard("8 - 3 - 2")), "=", eval("8 - 3 - 2"))
random.seed(13); same = 0
for _ in range(1000):
text, _ = rand_infix(4)
tree = ast.parse(text, mode="eval").body
assert shunting_yard(text) == ast_postfix(tree); same += 1
print(same, "random infix strings: shunting-yard == postorder of Python's AST")
print("example:", text, "->", " ".join(shunting_yard(text)))আসল আউটপুট (ব্রাউজারের Python):
3 120 * 2 45 * + 50 -
8 - 3 - 2 -> 8 3 - 2 - = 3
1000 random infix strings: shunting-yard == postorder of Python's AST
example: 1 * (6 - (7 - 4) - 8) -> 1 6 7 4 - - 8 - *
প্রথম লাইন অর্ডারের হিসাবের postfix, §৪-এর গাছের postorder-এর সঙ্গে হুবহু এক। দ্বিতীয় লাইন left-associativity দেখায়: output 8 3 - 2 -, আর Python-এর নিজের মানও 3. তারপর ১০০০টি এলোমেলো infix string-এ (সবচেয়ে কম বন্ধনী দিয়ে লেখা) আমাদের ১৫ লাইনের shunting-yard আর Python-এর AST-এর postorder একই টোকেন-ক্রম দিয়েছে। rand_infix-এর বন্ধনী-নিয়ম লক্ষ করুন: বাঁ দিকে শুধু কম precedence হলে বন্ধনী, কিন্তু ডান দিকে সমান হলেও, কারণ 7 − (3 − 2) থেকে বন্ধনী সরালে গাছ বদলে যায়। শেষ উদাহরণে 6 − (7 − 4) − 8 = (6 − 3) − 8, তাই postfix-এ দুটি − পরপর আগে, তারপর 8.
৭ · Traversal থেকে গাছ ফেরত — কখন অনন্য, কখন নয়
বাজারঘরের সার্ভার ডেলিভারি-জোনের একটি binary tree শুধু দুটি তালিকা হিসেবে সংরক্ষণ করে। প্রশ্ন: কোন দুটি তালিকা রাখলে গাছটি নিশ্চিতভাবে ফিরে পাওয়া যায়? এই অংশে binary tree-র প্রতিটি vertex-এর লেবেল আলাদা ধরে নিচ্ছি; একই লেবেল একাধিকবার থাকলে কোনো জোড়াই যথেষ্ট নয় (যেমন সব লেবেল A হলে তিনটি তালিকাই শুধু A, A, A).
আলাদা লেবেলের দুটি binary tree-র preorder একই আর inorder-ও একই হলে গাছ দুটি একই।
প্রমাণ (vertex-সংখ্যা n-এর ওপর strong induction):
- ভিত্তি n = 0: দুটিই খালি গাছ। — base case
- n ≥ 1: preorder-এর প্রথম লেবেল r দুই গাছেরই root. — preorder-এ root আগে
- লেবেলগুলো আলাদা, তাই inorder-এ r ঠিক এক জায়গায়, ধরুন অবস্থান k + 1-এ। Inorder = (বাঁ subtree-র inorder), r, (ডান subtree-র inorder), তাই দুই গাছেই বাঁ subtree-তে ঠিক kটি vertex, আর সেগুলো inorder-এর প্রথম k লেবেল। — inorder-এর সংজ্ঞা
- Preorder = r, (বাঁ-র preorder), (ডান-র preorder), আর বাঁ-র preorder-এ kটি লেবেল; তাই দুই গাছেরই বাঁ subtree-র preorder হলো preorder-এর ২য় থেকে (k + 1)-তম লেবেল, ডান subtree-র বাকিগুলো। — দৈর্ঘ্য মেলানো
- দুই গাছের বাঁ subtree-র preorder আর inorder একই, আকার k < n; হাইপোথিসিস অনুযায়ী বাঁ subtree দুটি একই। ডান subtree-র জন্যও একই কথা। Root, বাঁ আর ডান এক বলে গাছ দুটি এক। ∎ — inductive hypothesis
প্রমাণটি নিজেই একটি algorithm: preorder-এর প্রথমটি root, inorder-এ তাকে খুঁজে বাঁ-ডান ভাগ, তারপর দুই ভাগে একই কাজ। নিচের stepper ৯ vertex-এর একটি গাছ ফেরত আনে।
1পুরো গাছ: root A2A-এর বাঁ: root B3B-এর ডান: root E4A-এর ডান: root C5C-এর ডান: root F6যাচাই: postorder
| preorder | |
| inorder | |
| ভাগ |
- Root. Preorder-এর প্রথম লেবেল A, তাই root A. Inorder-এ A পঞ্চম স্থানে: বাঁয়ে D B G E (৪টি), ডানে C H F I (৪টি)। তাই preorder-এর পরের ৪টি B D E G বাঁ subtree-র, শেষ ৪টি C F H I ডান subtree-র।
- বাঁ subtree. Preorder B D E G, inorder D B G E. Root B; inorder-এ B-এর বাঁয়ে শুধু D, ডানে G E. তাই D হলো B-এর বাঁ সন্তান (একটি পাতা)।
- B-এর ডান অংশ। Preorder E G, inorder G E. Root E; inorder-এ E-এর বাঁয়ে G, ডানে কিছু নেই। তাই G হলো E-এর বাঁ সন্তান, আর E-এর ডান সন্তান নেই।
- ডান subtree. Preorder C F H I, inorder C H F I. Root C; inorder-এ C প্রথমে, তাই C-এর বাঁ subtree খালি, আর H F I পুরোটাই ডানে।
- C-এর ডান অংশ। Preorder F H I, inorder H F I. Root F, বাঁয়ে H, ডানে I: দুটিই পাতা। আর কোনো তালিকা বাকি নেই।
- যাচাই। গড়া গাছ: A-এর বাঁয়ে B (B-এর বাঁয়ে D, ডানে E, E-এর বাঁয়ে G), ডানে C (C-এর ডানে F, F-এর দুই সন্তান H, I). এর postorder D G E B H I F C A; preorder আর inorder-ও দেওয়া তালিকার সঙ্গে মেলে (পরের Python ব্লকে মেলানো)।
Preorder + postorder কেন যথেষ্ট নয়
সবচেয়ে ছোট পাল্টা-উদাহরণ দুটি vertex-এর: root A আর একটি সন্তান B. B বাঁ সন্তান হোক বা ডান, দুই গাছেই preorder A B আর postorder B A. Preorder আর postorder দুটোই root-কে এক প্রান্তে রাখে, তাই কোনটি বাঁ আর কোনটি ডান, তা ভাগ করার মতো কোনো “মাঝের চিহ্ন” পাওয়া যায় না; একমাত্র সন্তানের দিক হারিয়ে যায়। প্রতিটি “একমাত্র-সন্তান” vertex এই দ্বিধা একবার করে আনে। Brute force-এ (n ≤ 7 পর্যন্ত সব গাছ) দেখা যায়, একই preorder আর postorder-ওয়ালা গাছ ঠিক 2sটি, যেখানে s = একমাত্র-সন্তানওয়ালা vertex-এর সংখ্যা।
উল্টো দিকে, full binary tree-এ (প্রতিটি internal vertex-এর ঠিক দুটি সন্তান) preorder আর postorder যথেষ্ট। প্রমাণের ধারণা: preorder-এর দ্বিতীয় লেবেল x হলো বাঁ সন্তান (n ≥ 3 হলে বাঁ সন্তান থাকতেই হয়)। Postorder-এ বাঁ subtree আগে আসে আর x তার শেষ লেবেল, তাই postorder-এ x-এর অবস্থান বাঁ subtree-র আকার বলে দেয়; বাকিটা উপপাদ্য ৭.১-এর মতো induction.
একটি traversal মেলে কয়টি গাছে
Preorder 1, 2, …, n দেওয়া থাকলে কয়টি binary tree এর সঙ্গে মেলে? প্রতিটি আকার (shape) নিন, আর vertex-গুলোকে preorder-এ 1, 2, …, n নাম দিন; তাতে প্রতিটি আকার থেকে ঠিক একটি গাছ পাওয়া যায়, আর আলাদা আকার আলাদা গাছ দেয়। তাই সংখ্যাটি n vertex-এর binary tree-র আকারের সংখ্যা, অর্থাৎ Catalan সংখ্যা Cn = C(2n, n)/(n + 1): 1, 2, 5, 14, 42, 132, … (গণনার প্রমাণ পর্ব ৩৭-এ)। এটি একটি bijection যুক্তি। Inorder দিলেও একই সংখ্যা। কিন্তু preorder আর inorder দুটি দিলে উপপাদ্য ৭.১ অনুযায়ী বড়জোর একটি।
import random
from collections import Counter
# binary tree: None or (label, left, right); labels are distinct
def pre(t): return [] if t is None else [t[0]] + pre(t[1]) + pre(t[2])
def ino(t): return [] if t is None else ino(t[1]) + [t[0]] + ino(t[2])
def post(t): return [] if t is None else post(t[1]) + post(t[2]) + [t[0]]
def rebuild(p, i): # preorder + inorder -> tree
if not p: return None
k = i.index(p[0]) # root splits the inorder list
return (p[0], rebuild(p[1:k + 1], i[:k]), rebuild(p[k + 1:], i[k + 1:]))
def shapes(n): # every binary tree shape with n nodes
if n == 0: return [None]
return [(None, l, r) for k in range(n)
for l in shapes(k) for r in shapes(n - 1 - k)]
def label_preorder(s, it): # name the nodes 1, 2, ... in preorder
if s is None: return None
v = next(it)
return (v, label_preorder(s[1], it), label_preorder(s[2], it))
t9 = rebuild(list("ABDEGCFHI"), list("DBGEACHFI"))
print("postorder:", "".join(post(t9)))
random.seed(7); ok = 0
for n in range(1, 8):
for s in shapes(n):
labels = random.sample(range(100), n)
t = label_preorder(s, iter(labels))
assert rebuild(pre(t), ino(t)) == t; ok += 1
print(ok, "trees (all shapes, n <= 7): rebuilt exactly from preorder + inorder")
for n in range(1, 7):
trees = [label_preorder(s, iter(range(1, n + 1))) for s in shapes(n)]
pp = Counter((tuple(pre(t)), tuple(post(t))) for t in trees)
print(f"n={n}: {len(trees)} trees share preorder 1..{n};",
f"(pre, post) pairs {len(pp)}, worst pair fits {max(pp.values())} trees")আসল আউটপুট (ব্রাউজারের Python):
postorder: DGEBHIFCA
625 trees (all shapes, n <= 7): rebuilt exactly from preorder + inorder
n=1: 1 trees share preorder 1..1; (pre, post) pairs 1, worst pair fits 1 trees
n=2: 2 trees share preorder 1..2; (pre, post) pairs 1, worst pair fits 2 trees
n=3: 5 trees share preorder 1..3; (pre, post) pairs 2, worst pair fits 4 trees
n=4: 14 trees share preorder 1..4; (pre, post) pairs 4, worst pair fits 8 trees
n=5: 42 trees share preorder 1..5; (pre, post) pairs 9, worst pair fits 16 trees
n=6: 132 trees share preorder 1..6; (pre, post) pairs 21, worst pair fits 32 trees
প্রথম লাইন stepper-এর গাছের postorder দেয়। তারপর ৭ vertex পর্যন্ত প্রতিটি আকারের (1 + 2 + 5 + 14 + 42 + 132 + 429 = 625টি) গাছে এলোমেলো আলাদা লেবেল বসিয়ে preorder আর inorder থেকে হুবহু একই গাছ ফেরত এসেছে: উপপাদ্য ৭.১-এর পরীক্ষা। শেষ ছয় লাইনে একই preorder-এর গাছ গুনে Catalan সংখ্যা 1, 2, 5, 14, 42, 132. আর সেই গাছগুলোর postorder দেখলে মাত্র কয়েক রকম জোড়া (1, 1, 2, 4, 9, 21) আসে, আর সবচেয়ে খারাপ জোড়ায় 2n−1টি গাছ: শিকলের মতো গাছ, যেখানে n − 1টি vertex-এর একমাত্র সন্তান, প্রত্যেকে বাঁ বা ডান। Preorder + postorder তাই সাধারণ binary tree-তে মোটেই যথেষ্ট নয়।
৮ · পরীক্ষার অঙ্ক আর একটি ভুল প্রমাণ
অঙ্ক ১: Infix থেকে prefix আর postfix
(7 − 2) × (4 + 6) ÷ 5-এর expression tree আঁকুন (×, ÷ left-associative), আর prefix, postfix ও মান লিখুন।
- × আর ÷ সমান precedence, বাঁ থেকে: ((7 − 2) × (4 + 6)) ÷ 5. Root ÷, বাঁ subtree × (যার বাঁয়ে 7 − 2, ডানে 4 + 6), ডান subtree পাতা 5.
- Prefix (preorder):
÷ × − 7 2 + 4 6 5. - Postfix (postorder):
7 2 − 4 6 + × 5 ÷. - মান: 5 × 10 ÷ 5 = 10 (scratch-এ Python-এর AST দিয়ে prefix, postfix আর মান মেলানো)।
অঙ্ক ২: Postfix আর prefix-এর মান
(ক) 6 2 3 + − 3 8 2 ÷ + × আর (খ) − × 2 + 3 5 ÷ 12 4-এর মান কত?
- (ক) Stack: [6, 2, 3] → + → [6, 5] → − → [1] → [1, 3, 8, 2] → ÷ → [1, 3, 4] → + → [1, 7] → × → [7]. মান 7.
- (খ) ডান থেকে পড়ে: [4, 12] → ÷ দেয় 12 ÷ 4 = 3 → [3, 5, 3] → + দেয় 3 + 5 = 8 → [3, 8, 2] → × দেয় 2 × 8 = 16 → [3, 16] → − দেয় 16 − 3 = 13. Infix-এ 2 × (3 + 5) − 12 ÷ 4 (দুটিই scratch-এ মেলানো)।
অঙ্ক ৩: দুই traversal থেকে তৃতীয়টি
একটি binary tree-র preorder P Q S T R U আর inorder S Q T P U R. Postorder কী?
- Root P; inorder-এ বাঁয়ে S Q T, ডানে U R. বাঁ অংশের preorder Q S T, ডান অংশের R U.
- বাঁ: root Q, বাঁয়ে S, ডানে T. ডান: root R, inorder U R, তাই U হলো R-এর বাঁ সন্তান।
- Postorder: S T Q, তারপর U R, শেষে P: S T Q U R P (scratch-এ rebuild দিয়ে মেলানো)।
অঙ্ক ৪: গোনা
(ক) ৫ vertex-এর কয়টি binary tree-র preorder 1 2 3 4 5? (খ) কয়টি binary tree-র preorder A B C আর postorder C B A? উত্তর: (ক) C5 = C(10, 5)/6 = 42. (খ) গাছ দুটির preorder আর postorder মিলাতে হলে A-এর একমাত্র সন্তান B, আর B-এর একমাত্র সন্তান C; প্রতিটি বাঁ বা ডান, তাই 2 × 2 = 4 (দুটিই scratch-এ সব গাছ গুনে মেলানো)।
প্রমাণের ভুল ধরুন: “preorder আর postorder গাছ চেনায়”
একজন লিখলেন: “n-এর ওপর induction. Preorder-এর প্রথম লেবেল root r, postorder-এর শেষ লেবেলও r. Preorder-এর দ্বিতীয় লেবেল x হলো বাঁ সন্তান। Postorder-এ x-এর আগে পর্যন্ত (x সহ) যা আছে তা বাঁ subtree, বাকিটা ডান subtree. দুই অংশেই হাইপোথিসিস খাটে, তাই গাছ অনন্য। ∎”
ভুল কোথায়: “preorder-এর দ্বিতীয় লেবেল বাঁ সন্তান” দাবিটি শুধু তখন সত্য, যখন root-এর বাঁ সন্তান আছে। Root-এর শুধু ডান সন্তান থাকলে দ্বিতীয় লেবেল ডান সন্তান, আর প্রমাণ ভুল করে তাকে বাঁ ধরে। পাল্টা-উদাহরণ: A-এর একমাত্র সন্তান B, বাঁয়ে বা ডানে; দুই গাছেই preorder A B, postorder B A. প্রমাণটি একটি গোপন ধারণার ওপর দাঁড়িয়ে: “প্রতিটি internal vertex-এর দুটি সন্তান”। ঠিক সেই শর্ত যোগ করলে (full binary tree) যুক্তিটি সঠিক হয়, §৭-এর মতো।
আরও গভীরে: n vertex-এর binary tree Cnটি, আর log2 Cn প্রায় 2n − (3/2)log2 n, তাই যেকোনো সংরক্ষণ-পদ্ধতিতে গাছের আকার রাখতে প্রায় 2n bit লাগে; “null চিহ্নসহ preorder” বা বন্ধনীর ক্রম দিয়ে প্রায় ঠিক এতটুকুতেই রাখা যায়, আর succinct data structure-এর গবেষণা (Jacobson-এর ১৯৮৯ সালের কাজ থেকে শুরু বলে উল্লেখ করা হয়) এমন সংক্ষিপ্ত রূপেই traversal-এর প্রশ্নের দ্রুত উত্তর দেয়। Brute force-এ (n ≤ 7) আলাদা (preorder, postorder) জোড়ার সংখ্যা 1, 1, 2, 4, 9, 21, 51 মেলে, যা Motzkin সংখ্যার ক্রম। আর recursion বা stack ছাড়াই, প্রতিটি vertex-এ অস্থায়ী একটি pointer বসিয়ে inorder চালানোর পদ্ধতি Morris traversal (১৯৭৯) নামে পরিচিত।
পরীক্ষা ও ইন্টারভিউতে: বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন প্রায়ই আসে:
- একটি infix থেকে prefix আর postfix লিখুন। আগে বন্ধনী আর precedence মেনে গাছ, তারপর preorder আর postorder.
- Postfix-এর মান stack দিয়ে। সংখ্যা push; operator এলে প্রথম pop ডান operand, দ্বিতীয়টি বাঁ।
- Preorder আর inorder থেকে postorder. Preorder-এর প্রথমটি root, inorder-এ তাকে খুঁজে বাঁ-ডান ভাগ।
- Preorder আর postorder কি যথেষ্ট? না; একমাত্র সন্তানের দিক হারায়। Full binary tree-তে যথেষ্ট।
- Traversal-এর সময়? Θ(n): প্রতিটি vertex একবার, আর সন্তান-সংখ্যার যোগফল n − 1.
৯ · AI-এর যুগে — traversal আর notation-এর উত্তর যাচাই
Traversal-এর প্রশ্নে AI সহকারী সংজ্ঞা সাধারণত ঠিক বলে, কিন্তু হাতে-কলমে ক্রম লিখতে গিয়ে একটি subtree বাদ দেয়, operand-এর ক্রম উল্টায়, আর “অনন্য” দাবিতে শর্ত ভুলে যায়। কয়েকটি নিয়মিত ভুল আর ধরার উপায়:
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
| Postfix হিসাবে বিয়োগ বা ভাগে operand উল্টে ফেলা (b − a) | 8 3 −-এর মান 5 কি না দেখুন; এক লাইনের stack কোডে মেলান |
| “Preorder আর postorder থেকে গাছ অনন্য” বলা | দুই vertex-এর পাল্টা-উদাহরণ: A-এর বাঁ বা ডান সন্তান B |
8 − 3 − 2-কে 8 − (3 − 2) পড়া, বা 2 ** 3 ** 2-কে 64 বলা | Associativity জিজ্ঞেস করুন; Python-এ eval চালিয়ে দেখুন |
| সাধারণ ordered tree-তে inorder-এর সংজ্ঞা নিয়ে অনিশ্চিত, অথবা লেবেল পুনরাবৃত্তি থাকলেও “অনন্য” বলা | কোন সংজ্ঞা ব্যবহার হচ্ছে লিখিয়ে নিন; সব লেবেল এক রেখে পাল্টা-উদাহরণ বানান |
| “Preorder মানে level order” বা ঠিকানা string হিসেবে সাজানো | ছোট গাছে চারটি ক্রম Python-এ ছাপুন; "2.10" আর "2.9" তুলনা করে দেখুন |
AI-এর কাছে traversal, notation বা গাছ-ফেরতের প্রশ্ন করলে এমন কাঠামো দিন, যাতে প্রতিটি ধাপ আলাদা করে যাচাই করা যায়:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
সবচেয়ে কাজের অভ্যাস: AI যে ক্রম বা মান দিক, সেটি ১০ লাইনের একটি traversal বা stack কোডে চালিয়ে মেলান। ছোট গাছে সব আকার brute force করেও দাবি পরীক্ষা করা যায়, যেমন §৭-এর ব্লকে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১একজন যুক্তিবিদ বন্ধনী বাদ দিতে চেয়েছিলেন। Polish notation কীভাবে logic থেকে Lisp পর্যন্ত গেল?
Jan Łukasiewicz (১৮৭৮–১৯৫৬) ছিলেন ওয়ারশ-র যুক্তিবিদ, যিনি বহু-মানের logic নিয়ে কাজের জন্যও পরিচিত। Logic-এর সূত্র লিখতে তখন বন্ধনী আর বিন্দুর জটিল নিয়ম চলত (যেমন Whitehead আর Russell-এর Principia Mathematica-র বিন্দু-চিহ্ন)। Łukasiewicz ১৯২০-এর দশকে একটি সরল রীতি চালু করেন: connective আগে, তারপর তার argument. যেমন “p আর q” লেখা হয় Kpq, “p হলে q” Cpq, “না p” Np. বন্ধনী একেবারেই লাগে না, কারণ প্রতিটি connective জানে তার কয়টি argument, আর §৪-এর মতো গোনার যুক্তিতে প্রতিটি সূত্র একভাবেই পড়া যায়।
পশ্চিমে রীতিটি “Polish notation” নামে ছড়ায়, কারণ তাঁর নাম উচ্চারণ অনেকের কাছে কঠিন ছিল বলে প্রচলিত গল্প আছে; এই গল্পটি যাচাই করা কঠিন, তাই নামের উৎস হিসেবে নিশ্চিতভাবে বলা যায় শুধু এটুকু যে রীতিটি পোল্যান্ডের যুক্তিবিদদের কাছ থেকে আসে।
প্রোগ্রামিং-এ সবচেয়ে বিখ্যাত prefix ভাষা Lisp, যা John McCarthy ১৯৫৮–১৯৬০ সালের দিকে নকশা করেন। Lisp-এ (+ (* 3 120) (* 2 45)) লেখা হয়: প্রতিটি বন্ধনীর ভেতরে আগে operator. এখানে বন্ধনী থাকে, কারণ Lisp-এর operator-এর argument-সংখ্যা নির্দিষ্ট নয় (+ যেকোনো সংখ্যক জিনিস যোগ করতে পারে)। তবু মূল ধারণা একই: লেখাটি সরাসরি গাছের preorder, তাই প্রোগ্রাম নিজেই একটি ডেটা-গাছ, যা অন্য প্রোগ্রাম সহজে পড়তে ও বদলাতে পারে। এই বৈশিষ্ট্য Lisp-এর macro ব্যবস্থার ভিত্তি।
গাণিতিক দিক থেকে মূল উপলব্ধি হলো, “একভাবেই পড়া যায়” একটি প্রমাণযোগ্য ধর্ম। প্রতিটি চিহ্নের arity (argument-সংখ্যা) জানা থাকলে, prefix লেখার প্রতিটি শুরু-অংশে “এখনো কয়টি argument দরকার” গুনেই সূত্রের সীমানা পাওয়া যায়। Infix-এ এই তথ্য নেই, তাই বন্ধনী বা precedence-এর চুক্তি লাগে।
মূল উপলব্ধি: Polish notation আসলে গাছের preorder; প্রতিটি operator-এর arity জানা থাকলে বন্ধনী ছাড়াই অনন্যভাবে পড়া যায়, আর একই ধারণা Lisp-এর মতো ভাষায় “কোডই গাছ” রূপে টিকে আছে।
প্র ০২Reverse Polish notation আর stack কেন কম্পিউটারের ইতিহাসে এত গুরুত্বপূর্ণ? Compiler আর virtual machine কোথায় তা ব্যবহার করে?
১৯৫০-এর দশকে কম্পিউটারের মেমরি ছিল খুব সীমিত, আর ফর্মুলা অনুবাদ করা ছিল বড় চ্যালেঞ্জ। Postfix-এর আকর্ষণ স্পষ্ট: হিসাব করতে একবার বাঁ থেকে ডানে পড়া আর একটি stack যথেষ্ট, কোনো বন্ধনী বা আগে-পরে দেখা লাগে না। Arthur Burks, Don Warren আর Jesse Wright ১৯৫৪ সালের দিকে এমন একটি “parenthesis-free” রূপ একটি যন্ত্র-নকশার প্রসঙ্গে প্রস্তাব করেন বলে উল্লেখ করা হয়। জার্মানিতে Friedrich L. Bauer আর Klaus Samelson ১৯৫০-এর দশকের শেষে ফর্মুলা অনুবাদে stack-এর ব্যবহার (“Kellerprinzip”, মাটির নিচের ঘরের নীতি) প্রকাশ করেন, যা পরে ALGOL-এর compiler-এ কাজে লাগে।
Edsger Dijkstra ১৯৬০-এর দশকের শুরুতে ALGOL 60-এর compiler-এর কাজে shunting-yard পদ্ধতি বর্ণনা করেন বলে উল্লেখ করা হয়: §৬-এর operator-stack. একই সময়ে বিভিন্ন যন্ত্রে stack-ভিত্তিক নির্দেশ-ব্যবস্থাও আসে। ১৯৬০ আর ১৯৭০-এর দশকে কিছু বৈজ্ঞানিক calculator RPN-এ চলত: ব্যবহারকারী সংখ্যা ঢুকিয়ে “Enter” চাপতেন, তারপর operator. বন্ধনীর বোতাম লাগত না, আর মধ্যবর্তী ফল stack-এ দেখা যেত।
আজও ধারণাটি সবখানে। CPython-এর bytecode একটি stack machine-এর নির্দেশ: 3 * 120 + 2 * 45 অনুবাদ হলে মোটামুটি “3 আর 120 load, গুণ, 2 আর 45 load, গুণ, যোগ” ধরনের ক্রম আসে, ঠিক postfix (ধ্রুবক হলে compiler আগেই হিসাব করে ফেলতে পারে)। Java-র virtual machine-এর নির্দেশও operand stack-ভিত্তিক। Forth আর PostScript-এর মতো ভাষা সরাসরি postfix-এ লেখা হয়। Compiler-এর পেছনের কাজটি সাধারণত দুই ধাপ: infix থেকে AST (গাছ), তারপর AST-এর postorder থেকে stack-নির্দেশ।
গাণিতিক দিক থেকে এর ভিত্তি §৫-এর lemma: post(E) চালালে stack-এ ঠিক val(E) যোগ হয় আর নিচের কিছু নষ্ট হয় না। এই “stack-এর নিচে হাত দেয় না” ধর্মের জন্যই function call, nested expression আর recursion একসঙ্গে একটি stack-এ নিরাপদে চলে।
মূল উপলব্ধি: Postfix আর stack মিলে ফর্মুলার হিসাবকে একবার পড়ার একটি যান্ত্রিক কাজে পরিণত করে; তাই compiler, virtual machine আর কিছু calculator এই ধারণার ওপর দাঁড়িয়ে, আর তার সঠিকতা structural induction-এর একটি সরল lemma.
প্র ০৩গ্রন্থাগারের Dewey পদ্ধতি থেকে ফাইলের path আর আইনের ধারা নম্বর: hierarchical ঠিকানা কেন এত কাজের, আর কোথায় ফাঁদ?
Melvil Dewey ১৮৭৬ সালে বই সাজানোর একটি দশমিক পদ্ধতি প্রকাশ করেন: জ্ঞানকে দশটি বড় শ্রেণিতে, প্রতিটিকে দশটি উপ-শ্রেণিতে, এভাবে ভাগ। প্রতিটি বইয়ের নম্বর আসলে একটি শ্রেণি-গাছে তার ঠিকানা, আর তাকে নম্বর অনুযায়ী বই সাজালে একই বিষয়ের বই পাশাপাশি আসে। সংখ্যাতাত্ত্বিকভাবে বলতে গেলে, তাক হলো শ্রেণি-গাছের preorder, ঠিক উপপাদ্য ৩.১-এর মতো।
একই কাঠামো সবখানে। আইনের ধারা “৩(২)(ক)”, বইয়ের অধ্যায় ২.৩.১, কম্পিউটারের ফাইলের path (যেমন /bazarghar/orders/2026/11), আর ওয়েবপাতার ভেতরের উপাদানগুলোর গাছ। Path হলো root থেকে নামার রাস্তা, শুধু সংখ্যার বদলে নাম। XML ডেটাবেস নিয়ে গবেষণায় Dewey-ধাঁচের লেবেল ব্যবহার হয়েছে বলে জানা যায়, কারণ দুটি লেবেল দেখেই বলা যায় একটি অন্যটির পূর্বপুরুষ কি না (prefix কি না), আর দুটি ঘরের সবচেয়ে কাছের সাধারণ পূর্বপুরুষ হলো দুই ঠিকানার সবচেয়ে লম্বা সাধারণ prefix.
ফাঁদ তিনটি। প্রথম, §১-এর সতর্কতা: ঠিকানাকে string হিসেবে সাজালে "2.10" আসে "2.9"-এর আগে; প্রতিটি অংশ সংখ্যা হিসেবে তুলনা করতে হয় (অনেক ফাইল-ব্যবস্থাপক এজন্য “natural sort” রাখে)। দ্বিতীয়, মাঝখানে নতুন ঘর ঢোকালে পরের সব ভাইবোন আর তাদের বংশধরের ঠিকানা বদলায়, তাই স্থায়ী id হিসেবে ঠিকানা রাখা বিপজ্জনক; কিছু পদ্ধতি ফাঁক রেখে বা ভগ্নাংশ-ধাঁচের লেবেল দিয়ে এটি এড়ায়। তৃতীয়, গাছ গভীর হলে ঠিকানা লম্বা হয়, তাই জায়গা লাগে height-এর সমানুপাতে।
বাজারঘরের জন্য ব্যবহারিক শিক্ষা: বিভাগের মেনু ঠিক ক্রমে দেখাতে ঠিকানাকে পূর্ণসংখ্যার tuple হিসেবে রেখে সাজানোই যথেষ্ট, আর “এই বিভাগের ভেতরের সব পণ্য” খুঁজতে prefix-মিল দেখলেই চলে।
মূল উপলব্ধি: Hierarchical ঠিকানা গাছকে একটি সারিতে নামিয়ে আনে, আর lexicographic order সেই সারিকে preorder বানায়; শর্ত হলো ঠিকানার প্রতিটি অংশ সংখ্যা হিসেবে তুলনা করা।
প্র ০৪Precedence-এর চুক্তি সব ভাষায় এক নয়। Compiler কেন AST বানায়, আর কোথায় মানুষ আর যন্ত্র ভিন্নভাবে পড়ে?
Infix-এর মান নির্ভর করে চুক্তির ওপর, আর চুক্তি ভাষা থেকে ভাষায় বদলায়। Python-এ -2 ** 2 হলো −4, কারণ ঘাত unary minus-এর চেয়ে শক্ত করে বাঁধে; গণিতের বইয়েও −22 সাধারণত −4 পড়া হয়। কিন্তু কিছু spreadsheet প্রোগ্রামে =-2^2 লিখলে 4 আসে বলে জানা যায়, কারণ সেখানে unary minus আগে বাঁধে। একই লেখা, দুটি আলাদা গাছ, দুটি আলাদা উত্তর। C ভাষায় পুরোনো একটি ফাঁদ: x & 1 == 0 লিখলে তুলনা == আগে হয়, তাই অর্থ দাঁড়ায় x & (1 == 0), প্রোগ্রামার যা চেয়েছিলেন তা নয়।
এজন্য compiler একবার infix পড়ে একটি গাছ (AST) বানিয়ে নেয়, আর পরের সব কাজ (type যাচাই, ধ্রুবক আগেই হিসাব, কোড তৈরি) সেই গাছে চলে। গাছে কোনো অস্পষ্টতা নেই: কোন operator-এর কোন দুটি operand, তা গঠনেই লেখা। Python-এ নিজেই দেখতে পারেন: ast.dump(ast.parse("-2 ** 2", mode="eval")) দেখায় যে root একটি UnaryOp, আর তার ভেতরে BinOp(2 ** 2).
এই পর্বের shunting-yard আর তার পরিবারের পদ্ধতিগুলো (operator-precedence parsing, recursive descent) সবাই একই প্রশ্নের উত্তর দেয়: precedence আর associativity-র একটি টেবিল দেওয়া থাকলে infix থেকে অনন্য গাছ। এর আরও সাধারণ তত্ত্ব হলো grammar আর parsing, যা পর্ব ৭৩ “Grammar আর formal language — BNF থেকে Chomsky-র শ্রেণি”-এ আসবে, আর পরে একটি Theory of Computation কোর্সে আরও গভীরে।
ব্যবহারিক শিক্ষা পরিষ্কার। বাজারঘরের ডিসকাউন্ট-সূত্রের মতো গুরুত্বপূর্ণ হিসাবে সন্দেহ থাকলে বন্ধনী দিন; বন্ধনী মানুষের জন্য, আর যন্ত্রের কাছে শুধু গাছটি নিশ্চিত করে। কোড রিভিউতেও “কোন operator আগে বাঁধে” নিয়ে তর্ক এড়াতে বন্ধনীই সবচেয়ে সস্তা সমাধান।
মূল উপলব্ধি: Infix লেখার অর্থ চুক্তি-নির্ভর, কিন্তু গাছের অর্থ নয়; তাই compiler আগে AST বানায়, আর মানুষের জন্য সবচেয়ে নিরাপদ পথ হলো সন্দেহের জায়গায় বন্ধনী দেওয়া।
প্র ০৫একটি binary tree ফাইলে লিখে আবার ফেরত আনতে কয়টি তালিকা লাগে? “Serialize/deserialize” প্রশ্নের পেছনের গণিত কী?
§৭ দেখিয়েছে, আলাদা লেবেল থাকলে preorder আর inorder মিলে গাছ চেনায়। কিন্তু বাস্তবে লেবেল পুনরাবৃত্তি হতে পারে (দুটি বিভাগের একই নাম), আর দুটি তালিকা রাখা মানে দ্বিগুণ জায়গা। তাই বেশি ব্যবহৃত পদ্ধতি হলো একটি তালিকায় খালি জায়গাও লেখা: “null চিহ্নসহ preorder”. যেমন A-এর বাঁয়ে B আর ডানে কিছু নেই, B পাতা: লেখা হয় A B # # #, যেখানে # মানে খালি subtree.
এই তালিকা অনন্য কেন? এটি আসলে §৪-এর prefix notation-এর মতো: প্রতিটি লেবেলকে “দুই argument-এর operator” আর # -কে “শূন্য argument-এর ধ্রুবক” ভাবুন। তখন উপপাদ্য ৪.২-এর গোনার যুক্তি (এখানে prefix রূপে) বলে, বৈধ তালিকা একভাবেই পড়া যায়, লেবেল পুনরাবৃত্তি থাকলেও। nটি vertex-এর গাছে #-এর সংখ্যা n + 1, কারণ “খালি জায়গা”গুলোকে পাতা ধরলে গাছটি full binary tree, আর পাতা = internal + 1. মোট দৈর্ঘ্য 2n + 1.
এর চেয়ে কম জায়গায় হয় কি? গণনা বলে, প্রায় না। n vertex-এর binary tree-র আকার Cnটি, তাই যেকোনো পদ্ধতিকে অন্তত Cnটি আলাদা bit-string দিতে হয়, অর্থাৎ অন্তত log2 Cn bit. n = 1000-এর জন্য তা প্রায় 1984 bit, আর 2n = 2000: null চিহ্নসহ preorder-এর আকারটুকু (লেবেল বাদে, প্রতিটি অবস্থানে “আছে/নেই” একটি bit) মোটামুটি সর্বনিম্নের কাছেই। এই যুক্তি পর্ব ৬৫-এর comparison-sort নিম্নসীমার মতোই “গোনা থেকে নিম্নসীমা”।
ইন্টারভিউতে এই প্রশ্ন প্রায়ই আসে “একটি binary tree serialize আর deserialize করুন” রূপে। ভালো উত্তর শুধু কোড নয়: কেন তালিকাটি অনন্যভাবে পড়া যায় (গোনার শর্ত), খালি গাছ কীভাবে লেখা হয়, আর লেবেল পুনরাবৃত্তি থাকলে কেন preorder + inorder ব্যর্থ হয়, তা বলতে পারা।
মূল উপলব্ধি: Null চিহ্নসহ একটি preorder তালিকাই গাছকে অনন্যভাবে রাখে, কারণ তা আসলে একটি prefix expression; আর Catalan গণনা দেখায় যে প্রায় 2n bit-এর কম জায়গায় আকার রাখা সম্ভব নয়।
অনুশীলন
আগে খাতায় করুন, তারপর উত্তর খুলে মেলান।
-
প্রমাণ: শুধু binary operator-ওয়ালা যেকোনো expression tree E-এর জন্য প্রমাণ করুন: post(E)-এর শেষ টোকেন বাদ দিলে বাকি অংশের যেকোনো অশূন্য শুরু-অংশে operand-এর সংখ্যা operator-এর সংখ্যার চেয়ে অন্তত ১ বেশি, আর E একটি পাতা না হলে post(E)-এর শেষ টোকেন বাদ দেওয়া অংশে এই ফারাক ঠিক ২।
- সংজ্ঞা দিন f(w) = operand-সংখ্যা − operator-সংখ্যা. উপপাদ্য ৪.২(খ)-এর প্রথম অংশ (structural induction) দেয়: post(E)-এর প্রতিটি অশূন্য শুরু-অংশে f ≥ 1, আর পুরো post(E)-এ f = 1. — উপপাদ্য ৪.২
- শেষ টোকেন বাদ দেওয়া অংশের যেকোনো অশূন্য শুরু-অংশ post(E)-এরও শুরু-অংশ, তাই সেখানে f ≥ 1. — শুরু-অংশের শুরু-অংশ
- E = (∘, E1, E2) হলে post(E) = post(E1) post(E2) ∘. শেষ টোকেন ∘ বাদ দিলে থাকে post(E1) post(E2), যার f = 1 + 1 = 2. — হাইপোথিসিস, দুই subtree
- এটি stack-এর ভাষায়: শেষ operator আসার ঠিক আগে stack-এ ঠিক দুটি মান থাকে, তার দুই operand. ∎
scratch-এ ২০০০টি এলোমেলো গাছে দুই দাবিই মেলানো।
-
রূপান্তর আর হিসাব: 8 − 3 − 2 × (6 ÷ 3)-এর expression tree আঁকুন (+, −, ×, ÷ left-associative, × আর ÷-এর precedence বেশি)। তারপর (ক) prefix, (খ) postfix, (গ) মান, আর (ঘ) গাছের level order লিখুন।
গাছ: (8 − 3) − (2 × (6 ÷ 3)). Root −, বাঁয়ে − (8, 3), ডানে × (2, ÷ (6, 3)).
(ক)
− − 8 3 × 2 ÷ 6 3. (খ)8 3 − 2 6 3 ÷ × −. (গ) 5 − 2 × 2 = 1. (ঘ) level 0: −; level 1: −, ×; level 2: 8, 3, 2, ÷; level 3: 6, 3. তাই− − × 8 3 2 ÷ 6 3. (ক) থেকে (গ) scratch-এ Python-এর AST দিয়ে মেলানো, (ঘ) গাছ থেকে BFS চালিয়ে। -
আউটপুট অনুমান: কোড না চালিয়ে বলুন কী ছাপা হবে, আর কোন উত্তরগুলো ভুল ও কেন। তারপর যেকোনো Python-এ চালিয়ে মেলান।
def run(s): st = [] for tk in s.split(): if tk in "+-": a = st.pop(); b = st.pop() st.append(a + b if tk == "+" else a - b) else: st.append(int(tk)) return st print(run("8 3 - 2 -"), run("8 3 + 2 +"), run("5 1 2 + -"))[7] [13] [-2]কোডটি প্রথম pop-কে
a(বাঁ operand) ধরে, অথচ প্রথম pop আসলে ডান operand. যোগে এতে কিছু যায় আসে না, তাই মাঝের [13] ঠিক (8 + 3 + 2). কিন্তু "8 3 - 2 -"-এ প্রথম − দেয় 3 − 8 = −5, তারপর 2 − (−5) = 7; সঠিক মান (8 − 3) − 2 = 3. আর "5 1 2 + -"-এ 1 + 2 = 3, তারপর 3 − 5 = −2; সঠিক মান 5 − 3 = 2. ঠিক করতেb = st.pop(); a = st.pop()লিখুন। Lemma ৫.১-এর প্রমাণের ধাপ ৫ ঠিক এই ক্রমের কথা বলে (CPython 3.14-এ চালিয়ে মেলানো)। -
গণনা আর পাল্টা-উদাহরণ: (ক) ৪ vertex-এর কয়টি binary tree-র preorder 1 2 3 4? (খ) Preorder A B C আর postorder C B A, এমন সব binary tree আঁকুন। (গ) এদের মধ্যে inorder যোগ করলে কী হয়?
(ক) প্রতিটি আকারকে preorder-এ 1, 2, 3, 4 নাম দিলে একটি গাছ, তাই C4 = C(8, 4)/5 = 14. (খ) Postorder-এর শেষে A, preorder-এর শুরুতে A, তাই root A. Root-এর দুই সন্তান থাকলে preorder-এ B-এর পরে ডান subtree-র root আসত আর postorder-এ B আগে শেষ হতো; মিলিয়ে দেখলে A-এর একমাত্র সন্তান B, আর B-এর একমাত্র সন্তান C. প্রতিটি “একমাত্র সন্তান” বাঁ বা ডান, তাই 4টি গাছ: বাঁ-বাঁ, বাঁ-ডান, ডান-বাঁ, ডান-ডান। (গ) চারটির inorder যথাক্রমে C B A, B C A, A C B, A B C: সব আলাদা, তাই inorder যোগ করলে ঠিক একটি গাছ থাকে, উপপাদ্য ৭.১-এর মতো (চারটিই scratch-এ সব গাছ গুনে মেলানো)।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমআলাদা লেবেলের binary tree-কে preorder আর inorder থেকে ফেরত আনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Preorder-এর প্রথম লেবেলটিকে root ধরুন
- Inorder-এ root খুঁজে তার বাঁয়ের kটি লেবেলকে বাঁ subtree ধরুন
- Preorder-এর পরের kটি লেবেল বাঁ subtree-র preorder, বাকিগুলো ডান subtree-র
- বাঁ আর ডান অংশে একই পদ্ধতি আবার প্রয়োগ করুন
- গড়া গাছের postorder বা preorder বের করে দেওয়া তালিকার সঙ্গে মিলিয়ে যাচাই করুন
Postfix expression 4 2 5 × + 1 −-এর মান কত?
- 13
- 29
- −13
- 10
একটি ordered rooted tree-র root A, তার সন্তান ক্রমে B আর C; B-এর সন্তান ক্রমে D আর E; C একটি পাতা। এর postorder কোনটি?
- A B D E C
- A B C D E
- D E B C A
- D B E A C
আলাদা লেবেলের যেকোনো binary tree অনন্যভাবে ফেরত পেতে কোন দুটি traversal সবসময় যথেষ্ট?
- Preorder আর postorder
- Preorder আর inorder
- Preorder আর preorder-এর উল্টো ক্রম
- Level order আর preorder
+ আর − সমান precedence-এর আর left-associative হলে, shunting-yard পদ্ধতিতে infix a − b + c-এর postfix কোনটি?
- a b c + −
- − + a b c
- a b + − c
- a b − c +
n vertex-এর একটি ordered rooted tree-তে recursive preorder traversal-এর সময় (প্রতিটি vertex-এ কাজ সন্তান-সংখ্যার রৈখিক ফাংশন হলে) কত?
- Θ(n²)
- Θ(log n)
- Θ(n)
- Θ(n · h), h = height
৪টি vertex-এর কয়টি আলাদা binary tree-র preorder হলো 1 2 3 4?
- 1
- 14
- 24
- 8
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- চার traversal: preorder (root আগে), inorder (প্রথম subtree, root, বাকিরা; binary tree-তে বাঁ, root, ডান), postorder (root শেষে), level order (queue দিয়ে স্তরে স্তরে); সবই Θ(n).
- ঠিকানা: universal address-এর lexicographic order = preorder; ঠিকানার অংশগুলো সংখ্যা হিসেবে তুলনা করুন, string হিসেবে নয়।
- Notation: expression tree-র preorder = prefix (Polish), postorder = postfix (RPN), inorder = infix; prefix আর postfix বন্ধনী ছাড়াই অনন্যভাবে পড়া যায়।
- Stack: postfix-এ সংখ্যা push, operator এলে প্রথম pop ডান, দ্বিতীয় pop বাঁ operand; সঠিকতা “যেকোনো শুরুর stack”-এর ওপর structural induction দিয়ে।
- গাছ ফেরত: আলাদা লেবেলে preorder + inorder অনন্য গাছ দেয়; preorder + postorder দেয় না (full binary tree ছাড়া); একটি preorder-এর সঙ্গে Cnটি গাছ মেলে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৬৭ · Spanning tree — DFS/BFS, minimum spanning tree আর Kirchhoff-এর গণনাপরবর্তী পর্বGraph-এর ভেতরের tree, MST-এর প্রমাণ আর spanning tree-র গণনা।
- পর্ব ১২ · Recursive সংজ্ঞা আর structural inductionপুনরালোচনাFull binary tree, structural recursion আর unique readability.
- পর্ব ৩৭ · গণনার কৌশল — lattice path, bijection আর double countingপুনরালোচনাBinary tree গোনার Catalan সংখ্যা আর তার প্রমাণ।
- ডিসক্রিট ম্যাথমেটিক্স · কোর্স হোম৮৩টি পর্বপুরো কোর্সের মানচিত্র আর ১৪টি মডিউল।
- DSA · পর্ব ২৬ · Tree — পরিভাষা, উপস্থাপন আর traversalকোডে প্রয়োগRecursion ছাড়া traversal, level order আর গাছের ক্লাসিক সমস্যা।
- DSA · পর্ব ১৩ · Stack — শেষে ঢোকে, আগে বের হয়কোডে প্রয়োগShunting-yard আর postfix হিসাবের পূর্ণ কোড।