PDA — ফরমাল ডেফিনিশন
এই পাঠে যা শিখবেন
- PDA কেন NFA-এর একটি "স্ট্যাক-সহ" সম্প্রসারণ, এবং স্ট্যাক ঠিক কীভাবে গণনাশক্তি বাড়ায়
- PDA-এর ফরমাল ৬-টাপল সংজ্ঞা এবং ট্রানজিশন ফাংশন $\delta$-এর সঠিক গঠন
- "কনফিগারেশন" ধারণা — একটি PDA-এর কম্পিউটেশনের সম্পূর্ণ স্ন্যাপশট
- Python দিয়ে $0^n1^n$ ভাষার একটি সত্যিকারের, সম্পূর্ণ কার্যকর PDA সিমুলেশন
১ · PDA কেন NFA-এর চেয়ে বেশি শক্তিশালী
M2-M3-এ দেখা DFA/NFAফিক্সড, ফাইনাইট সংখ্যক স্টেট দিয়ে গঠিত মেশিন — কোনো অতিরিক্ত মেমরি নেই-এর একটি মৌলিক সীমাবদ্ধতা আছে — এদের কাছে শুধু ফাইনাইট সংখ্যক স্টেট আছে, তাই এরা কোনো কিছু "গুনতে" পারে না যদি সেই গণনার সম্ভাব্য মান অসীম হয়। এই কারণেই L14-এ প্রমাণিত হয়েছিল $L = \{0^n1^n : n \geq 0\}$ রেগুলার নয় — কোনো DFA-ই "কতগুলো 0 পড়েছি" মনে রাখতে পারে না যখন $n$ যেকোনো মান নিতে পারে।
পুশডাউন অটোমাটাPushdown Automaton (PDA)একটি NFA যাতে একটি আনবাউন্ডেড স্ট্যাক (LIFO মেমরি) যোগ করা হয়েছে — কনটেক্সট-ফ্রি ল্যাঙ্গুয়েজ (M4) স্বীকৃতির জন্য এটিই স্ট্যান্ডার্ড মেশিন।
ঠিক এই সীমাবদ্ধতা দূর করে — একটি স্ট্যাকStack — LIFO (Last-In-First-Out) ডেটা স্ট্রাকচার, ../dsa/-এর Stack ADT-এর সাথে সরাসরি সম্পর্কিত যোগ করে দিলে মেশিনটি এখন অসীম পর্যন্ত "গুনতে" পারে (প্রতিটি 0-এর জন্য একটি মার্কার push করে) — এটিই ঠিক সেই বাড়তি ক্ষমতা যা রেগুলার ল্যাঙ্গুয়েজ থেকে কনটেক্সট-ফ্রি ল্যাঙ্গুয়েজে (M4-M6) উন্নীত করে। L26-এ CFL-এর জন্যও একটি পাম্পিং লেমা দেখা যাবে, ঠিক এই কারণেই।
২ · ফরমাল সংজ্ঞা — ৬-টাপল
একটি PDA ফরমালি একটি ৬-টাপল:
$$M = (Q, \Sigma, \Gamma, \delta, q_0, F)$$
যেখানে —
- $Q$ — ফাইনাইট সংখ্যক স্টেটের সেট (DFA/NFA-এর মতোই)।
- $\Sigma$ — ইনপুট আলফাবেট।
- $\Gamma$ — স্ট্যাক আলফাবেট (যেসব সিম্বল push/pop করা যায় — $\Sigma$ থেকে সম্পূর্ণ আলাদা হতে পারে, প্রায়ই একটি বাড়তি "বটম মার্কার" সিম্বলও থাকে)।
- $q_0 \in Q$ — শুরু স্টেট, এবং $F \subseteq Q$ — accepting স্টেটসমূহ।
- $\delta$ — ট্রানজিশন ফাংশন, DFA/NFA-এর চেয়ে বেশি জটিল:
$$\delta: Q \times (\Sigma \cup \{\varepsilon\}) \times \Gamma \to \mathcal{P}(Q \times \Gamma^*)$$
অর্থাৎ প্রতিটি ট্রানজিশন একসাথে তিনটি জিনিসের উপর নির্ভর করে — বর্তমান স্টেট, একটি ইনপুট সিম্বল (অথবা ε দিয়ে কিছুই না পড়ে), এবং স্ট্যাকের বর্তমান টপ সিম্বল (যা বাধ্যতামূলকভাবে pop হয়) — এবং ফলাফল হিসেবে একটি নতুন স্টেট আর একটি স্ট্রিং $\Gamma^*$ দেয় যা push করা হবে (popped সিম্বলের জায়গায়)। শূন্য সিম্বল push করলে সেটা নিছক pop-ই থেকে যায়; একাধিক সিম্বল push করলে স্ট্যাকে একসাথে কয়েকটি সিম্বল যোগ হয়।
PDA-এর সাধারণ (general) সংজ্ঞা নন-ডিটারমিনিস্টিক — একই (স্টেট, ইনপুট সিম্বল, স্ট্যাক-টপ) কম্বিনেশনের জন্য একাধিক সম্ভাব্য (নতুন স্টেট, push-স্ট্রিং) জোড়া থাকতে পারে, ফাংশনের রেঞ্জ $\mathcal{P}(Q \times \Gamma^*)$ (একটি সেট) হওয়াতেই এটা স্পষ্ট। M2-এর NFA=DFA (L08) থেকে ভিন্ন — L25-এ দেখা যাবে, PDA-এর ক্ষেত্রে নন-ডিটারমিনিজম সত্যিকারের বাড়তি ক্ষমতা দেয়, শুধু সুবিধা নয়।
৩ · কনফিগারেশন — কম্পিউটেশনের একটি স্ন্যাপশট
একটি কনফিগারেশনConfigurationPDA-এর কম্পিউটেশনের একটি সম্পূর্ণ instantaneous স্ন্যাপশট — (বর্তমান স্টেট, বাকি থাকা ইনপুট, বর্তমান স্ট্যাক কনটেন্ট) হলো একটি ট্রিপল — (বর্তমান স্টেট, বাকি থাকা ইনপুট, বর্তমান স্ট্যাক কনটেন্ট)। একটি সম্পূর্ণ কম্পিউটেশন হলো এমন কনফিগারেশনের একটি সিকোয়েন্স, প্রতিটি ধাপ $\delta$-এর কোনো একটি বৈধ ট্রানজিশন অনুসরণ করে পরবর্তী কনফিগারেশনে যায়। যেহেতু নন-ডিটারমিনিজম আছে, তাই একটি ইনপুটের জন্য একাধিক সমান্তরাল কনফিগারেশন একই সাথে সম্ভব হতে পারে — একটি স্ট্রিং accept হয় যদি অন্তত একটি কম্পিউটেশন পাথ পুরো ইনপুট consume করে একটি accept স্টেটে পৌঁছায় (L07-এর NFA "সব সম্ভাব্য স্টেট একসাথে ট্র্যাক করা" কৌশলের সাথে সরাসরি সম্পর্কিত, এখন স্ট্যাক-সহ)।
৪ · Python-এ একটি সত্যিকারের PDA
নিচের কোড সেলে একটি real PDA ক্লাস — একটি genuine Python list-কে স্ট্যাক হিসেবে ব্যবহার করে (push = append, pop = pop — সরাসরি ../dsa/-এর Stack ADT অপারেশন) — এবং accepts মেথড সব সম্ভাব্য সমান্তরাল কনফিগারেশন ট্র্যাক করে (একটি সেট, L07-এর NFA কৌশলের সাধারণীকরণ, এখন স্ট্যাক-কনটেন্টসহ)। এই PDA-টি $L = \{0^n1^n : n \geq 0\}$ accept করার জন্য বানানো — প্রতিটি 0-এর জন্য একটি $X$ push করে, প্রতিটি 1-এর জন্য একটি $X$ pop করে, এবং ইনপুট শেষ হওয়ার সময় স্ট্যাকে শুধু বটম-মার্কার $Z_0$ বাকি থাকলে (অর্থাৎ সব $X$ ঠিক জোড়ায় জোড়ায় মিলে গেছে) accept করে।
# একটি সত্যিকারের PDA -- L = { 0^n 1^n : n >= 0 }, ফাইনাল-স্টেট অ্যাকসেপ্টেন্স
class PDA:
def __init__(self, states, input_alphabet, stack_alphabet, delta, start_state, start_stack, accept_states):
self.states = states
self.input_alphabet = input_alphabet
self.stack_alphabet = stack_alphabet
self.delta = delta # dict: (state, symbol_or_None, stack_top) -> [(new_state, push_tuple), ...]
self.start_state = start_state
self.start_stack = start_stack # bottom-of-stack marker
self.accept_states = accept_states
def _moves(self, state, remaining_input, stack):
"""genuine Python list স্ট্যাক থেকে top pop করে সব সম্ভাব্য (state, input, stack) মুভ বের করে"""
moves = []
if not stack:
return moves
top = stack[-1] # স্ট্যাকের টপ = list-এর শেষ উপাদান
for (new_state, push) in self.delta.get((state, None, top), []): # epsilon মুভ
new_stack = list(stack[:-1]) # top pop
new_stack.extend(reversed(push)) # push -- বাম দিকের সিম্বল টপে থাকবে
moves.append((new_state, remaining_input, tuple(new_stack)))
if remaining_input:
a = remaining_input[0]
for (new_state, push) in self.delta.get((state, a, top), []): # সিম্বল পড়ে মুভ
new_stack = list(stack[:-1])
new_stack.extend(reversed(push))
moves.append((new_state, remaining_input[1:], tuple(new_stack)))
return moves
def accepts(self, string):
"""সব সম্ভাব্য সমান্তরাল কনফিগারেশন ট্র্যাক করে -- অন্তত একটি accept স্টেটে পৌঁছালেই accept"""
start_config = (self.start_state, string, (self.start_stack,))
seen = {start_config}
frontier = [start_config]
while frontier:
state, remaining, stack = frontier.pop()
if remaining == "" and state in self.accept_states:
return True
for nxt in self._moves(state, remaining, stack):
if nxt not in seen:
seen.add(nxt)
frontier.append(nxt)
return False
states = {"q0", "q1", "qf"}
input_alphabet = {"0", "1"}
stack_alphabet = {"Z0", "X"}
delta = {
("q0", "0", "Z0"): [("q0", ("X", "Z0"))], # push X, Z0 নিচে থাকবে
("q0", "0", "X"): [("q0", ("X", "X"))], # push X, আরেকটা X-এর উপর
("q0", "1", "X"): [("q1", ())], # pop X (প্রথম 1 দেখলে)
("q1", "1", "X"): [("q1", ())], # প্রতিটি পরবর্তী 1-এ pop X
("q0", None, "Z0"): [("qf", ("Z0",))], # খালি স্ট্রিং হলে সরাসরি accept
("q1", None, "Z0"): [("qf", ("Z0",))], # সব X pop হয়ে গেলে accept
}
pda = PDA(states, input_alphabet, stack_alphabet, delta, "q0", "Z0", {"qf"})
accept_tests = ["0011", "000111", ""]
reject_tests = ["10", "0011000", "0111"]
print("ACCEPT হওয়া উচিত:")
for s in accept_tests:
print(f" {s!r:10s} -> {pda.accepts(s)}")
print("REJECT হওয়া উচিত:")
for s in reject_tests:
print(f" {s!r:10s} -> {pda.accepts(s)}")
_moves মেথডে দুই ধরনের ট্রানজিশন একসাথে চেক করা হচ্ছে — epsilon মুভ (ইনপুট না পড়েই, delta[(state, None, top)]) এবং সিম্বল-পড়া মুভ (delta[(state, a, top)]) — একটি একক কনফিগারেশন থেকে দুটোই সম্ভব হতে পারে, যা accepts-এর BFS-স্টাইল frontier-এ স্বাভাবিকভাবেই ধরা পড়ে। এটাই ঠিক PDA-এর নন-ডিটারমিনিজমের বাস্তব বাস্তবায়ন — L25-এ এই একই PDA ক্লাস পুনরায় ব্যবহার করে দেখানো হবে কখন এই নন-ডিটারমিনিজম genuine (সত্যিকারের একাধিক পথ) আর কখন শুধু কাঠামোগত।
PDA = NFA + একটি genuine unbounded স্ট্যাক — এই সাধারণ সংযোজনই DFA/NFA-এর ফাইনাইট-মেমরি সীমাবদ্ধতা ভেঙে দেয়, এবং কনটেক্সট-ফ্রি ল্যাঙ্গুয়েজ (M4) স্বীকৃতির জন্য ঠিক যথেষ্ট ক্ষমতা দেয় — না কম, না বেশি (M4-M6 জুড়ে এই সমতা প্রমাণিত হবে)।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একটি DFA কেন কখনোই $0^n1^n$ ভাষা accept করতে পারে না, কিন্তু একটি PDA পারে?
একটি DFA-এর কাছে শুধু ফাইনাইট সংখ্যক স্টেট আছে — তাই "কতগুলো 0 দেখেছি" এই তথ্যটি শুধু ফাইনাইট সংখ্যক উপায়ে এনকোড করা যায়, আর $n$ যেকোনো (potentially অসীম) মান নিতে পারে বলে কোনো ফিক্সড সংখ্যক স্টেট যথেষ্ট নয় (L14-এ পাম্পিং লেমা দিয়ে এটি ফরমালি প্রমাণিত)। একটি PDA-এর স্ট্যাক সেই সীমাবদ্ধতা দূর করে — স্ট্যাকে যত ইচ্ছা তত সিম্বল push করা যায়, তাই এটি $n$-এর যেকোনো মানের জন্য 0-এর সংখ্যা যথাযথভাবে "গুনে" রাখতে পারে, তারপর 1 পড়ার সময় সেই একই সংখ্যক সিম্বল pop করে মিলিয়ে দেখতে পারে।
প্র ০২ PDA-এর ট্রানজিশন ফাংশন $\delta$ কেন স্ট্যাকের টপ সিম্বলের উপরও নির্ভর করে, শুধু স্টেট আর ইনপুটের উপর নয়?
কারণ PDA-এর "সিদ্ধান্ত" শুধু বর্তমান স্টেট আর ইনপুট সিম্বলের উপর নয়, স্ট্যাকে এখন পর্যন্ত কী জমা আছে তার উপরও নির্ভর করে — এটাই স্ট্যাকের পুরো পয়েন্ট। উদাহরণস্বরূপ, উপরের $0^n1^n$ PDA-তে, একটি '1' পড়ার সময় ট্রানজিশনটি বৈধ হওয়ার জন্য স্ট্যাকের টপে অবশ্যই একটি $X$ থাকতে হবে (মানে এখনো মেলানোর জন্য একটি 0 বাকি আছে) — টপে যদি $Z_0$ (বটম মার্কার) থাকে, তাহলে এই ট্রানজিশনটি প্রযোজ্যই না, স্বাভাবিকভাবেই সেই পথ reject হয়ে যায়।
প্র ০৩
উপরের PDA-এর delta-তে কেন দুটো আলাদা স্টেট q0 আর q1 লাগলো — একটি স্টেটেই কি সব করা যেত না?
দুটো ফেজ আলাদা রাখা দরকার — q0-তে শুধু 0 পড়ে push করা হয়, q1-এ শুধু 1 পড়ে pop করা হয়। যদি একটি স্টেটে দুটোই মেশানো হতো, তাহলে "0111" (2টি অতিরিক্ত 1, যেখানে 0 কম)-এর মতো ভুল-ক্রমের স্ট্রিং এমনভাবে প্রসেস হতে পারতো যা ভুলভাবে accept করে ফেলতে পারে। দুটো স্টেট ব্যবহার করে নিশ্চিত করা হচ্ছে — একবার '1' পড়া শুরু হলে, আর কোনো '0' গ্রহণযোগ্য নয় (q1-এ কোনো '0'-ট্রানজিশন নেই), তাই "0011000"-এর মতো ভুল-ক্রমের স্ট্রিং স্বাভাবিকভাবেই আটকে যায় (dead configuration) এবং reject হয়।
অনুশীলন
-
ট্রেস করুন: হাতে-কলমে উপরের PDA-তে "000111" স্ট্রিং-এর কনফিগারেশন সিকোয়েন্স লিখুন (প্রতিটি ধাপে স্টেট, বাকি ইনপুট, স্ট্যাক) — মোট কতগুলো ধাপ লাগে?
মোট ৬টি সিম্বল (৩টি 0, ৩টি 1) পড়তে ৬টি সিম্বল-consuming ট্রানজিশন লাগে, প্লাস শেষে একটি epsilon ট্রানজিশন accept স্টেটে যেতে — সব মিলিয়ে ৭টি কনফিগারেশন-পরিবর্তন। ট্রেস: (q0,"000111",[Z0]) → (q0,"00111",[Z0,X]) → (q0,"0111",[Z0,X,X]) → (q0,"111",[Z0,X,X,X]) → (q1,"11",[Z0,X,X]) → (q1,"1",[Z0,X]) → (q1,"",[Z0]) → (qf,"",[Z0]) — শেষ কনফিগারেশনে বাকি ইনপুট খালি এবং স্টেট
qf∈ accept states, তাই accept। -
পরীক্ষা করুন: উপরের কোড সেলে
accept_tests-এ"00001111"যোগ করে Run চাপুন — আপনার প্রত্যাশা কী, এবং কেন?"00001111"-তে ৪টি 0 আর ৪টি 1 আছে ($n=4$), তাই এটি $0^n1^n$ ভাষার সদস্য এবং PDA-টি এটিacceptকরার কথা — চারটি $X$ push হবে, তারপর চারটি $X$ ঠিক pop হয়ে স্ট্যাক আবার $[Z_0]$-এ ফিরে আসবে ইনপুট শেষ হওয়ার সাথে সাথেই।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ L23-এ PDA অ্যাকসেপ্টেন্সের দুটো ভিন্ন কনভেনশন (ফাইনাল স্টেট বনাম এম্পটি স্ট্যাক) এবং তাদের সমতা দেখুন।
- Data Structures & Algorithms কোর্স সহোদর কোর্স Stack ADT (push/pop/LIFO) প্র্যাকটিক্যালি কীভাবে ব্যবহৃত হয় সেই কোর্সে দেখুন — এই পাঠের PDA ঠিক সেই একই স্ট্যাক দিয়ে একটি ফরমাল রিকগনাইজার বানায়।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git ও Theory of Computation — সব এক জায়গায়।