PDA অ্যাকসেপ্টেন্স — ফাইনাল স্টেট বনাম এম্পটি স্ট্যাক
এই পাঠে যা শিখবেন
- ফাইনাল-স্টেট অ্যাকসেপ্টেন্স কনভেনশন — L06/L07-এর DFA/NFA কনভেনশনের সরাসরি সম্প্রসারণ
- এম্পটি-স্ট্যাক অ্যাকসেপ্টেন্স কনভেনশন — PDA-নির্দিষ্ট, DFA/NFA-এ যার কোনো analog নেই
- এই দুই কনভেনশনের সমতা প্রমাণের ধারণা — উভয় দিকের কনভার্শন কীভাবে কাজ করে
- Python-এ একটি সত্যিকারের কনভার্শন অ্যালগরিদম প্রয়োগ করে সমতা কনক্রিটলি verify করা
১ · দুটো ভিন্ন অ্যাকসেপ্টেন্স কনভেনশন
L22-এ PDA-এর ফরমাল সংজ্ঞায় $F \subseteq Q$ (accept স্টেটসমূহ) ছিল, ঠিক DFA/NFA-এর মতোই। কিন্তু PDA-এর একটি বাড়তি জিনিস আছে — স্ট্যাক — আর সেই স্ট্যাকের চূড়ান্ত অবস্থাও একটি স্বাভাবিক, বিকল্প অ্যাকসেপ্টেন্স শর্ত হতে পারে। তাই দুটো ভিন্ন, উভয়ই স্ট্যান্ডার্ড কনভেনশন প্রচলিত আছে:
PDA স্ট্রিং $w$ accept করে যদি কোনো কম্পিউটেশন পাথ পুরো ইনপুট consume করে একটি accept স্টেটে ($F$-এর মধ্যে) থামে — স্ট্যাকের চূড়ান্ত কনটেন্ট সম্পূর্ণ irrelevant।
PDA স্ট্রিং $w$ accept করে যদি কোনো কম্পিউটেশন পাথ পুরো ইনপুট consume করে স্ট্যাক সম্পূর্ণ খালি করে ফেলে — বর্তমান স্টেট কী তা সম্পূর্ণ irrelevant (তাই এই কনভেনশনে প্রায়ই $F$ পুরোপুরি বাদ দেওয়া হয়, বা খালি রাখা হয়)।
L22-এর $0^n1^n$ PDA-টি ফাইনাল-স্টেট কনভেনশনে তৈরি ছিল — সেখানে qf-এ পৌঁছানোই ছিল accept শর্ত। এই পাঠে সেই একই ভাষার জন্য একটি এম্পটি-স্ট্যাক PDA বানানো হবে — কোনো accept স্টেটেরই দরকার পড়বে না।
২ · সমতা থিওরেম
এই দুই কনভেনশন সম্পূর্ণ ভিন্ন দেখতে হলেও, এগুলো একই শ্রেণির ভাষা সংজ্ঞায়িত করে — যদিও একটি নির্দিষ্ট PDA দুই কনভেনশনে ভিন্ন আচরণ করতে পারে, প্রতিটি ফাইনাল-স্টেট PDA-এর জন্য একটি সমতুল্য এম্পটি-স্ট্যাক PDA বানানো সম্ভব, এবং উল্টোটাও।
ফাইনাল স্টেট → এম্পটি স্ট্যাক: একটি নতুন "cleanup" স্টেট যোগ করুন — যখনই মূল PDA একটি accept স্টেটে পৌঁছায়, নতুন স্টেটে গিয়ে ε-ট্রানজিশন দিয়ে স্ট্যাকের সব সিম্বল একে একে pop করে খালি করে ফেলা হয়, তারপর accept।
এম্পটি স্ট্যাক → ফাইনাল স্টেট (এই পাঠের কোড যা বাস্তবায়ন করবে): একটি নতুন বটম-মার্কার স্ট্যাক-সিম্বল $X_0$ (মূল $\Gamma$-তে নেই এমন) এবং দুটো নতুন স্টেট $q_0'$ (নতুন শুরু) ও $q_f$ (নতুন accept) যোগ করুন —
- $q_0'$ থেকে ε-ট্রানজিশনে মূল PDA-এর শুরু-স্ট্যাক-সিম্বল push করে (নিচে $X_0$ রেখে) মূল শুরু স্টেটে যাওয়া হয়।
- মূল PDA-এর যেকোনো স্টেট $q$-তে, যদি স্ট্যাকের টপে $X_0$ দেখা যায় (অর্থাৎ মূল স্ট্যাক ইতিমধ্যে খালি হয়ে গেছে), তাহলে ε-ট্রানজিশনে $q_f$-এ চলে যাওয়া হয় — এটাই accept শর্ত।
$$\delta'(q_0', \varepsilon, X_0) = \{(q_0, Z_0 X_0)\} \qquad \delta'(q, \varepsilon, X_0) = \{(q_f, X_0)\} \text{ প্রতিটি } q \in Q \text{-এর জন্য}$$
মূল PDA-তে স্ট্যাক খালি হওয়া মানেই নতুন PDA-তে $X_0$ উন্মুক্ত হয়ে যাওয়া (কারণ $X_0$ সবসময় সবচেয়ে নিচে বসে আছে, মূল স্ট্যাক-কনটেন্টের নিচে) — আর সেই মুহূর্তেই (যেকোনো স্টেট থেকে) $q_f$-এ যাওয়ার সুযোগ তৈরি হয়। তাই "মূল PDA স্ট্যাক খালি করেছে" ঠিক তখনই সত্য যখন "নতুন PDA $q_f$-এ পৌঁছাতে পারে" — দুটো শর্ত সমতুল্য।
৩ · Python-এ সমতা verify করা
নিচের কোড সেলে L22-এর PDA ক্লাস সম্প্রসারণ করে দুটো আলাদা মেথড — accepts_by_final_state ও accepts_by_empty_stack — যোগ করা হয়েছে, তারপর $0^n1^n$-এর জন্য একটি এম্পটি-স্ট্যাক PDA বানিয়ে (কোনো accept স্টেট ছাড়াই) সেটিকে convert_to_final_state_acceptance দিয়ে কনভার্ট করা হয়েছে — এবং উভয় PDA একই ব্যাচ টেস্ট স্ট্রিং-এ ঠিক একই ফলাফল দেয় কিনা তা সরাসরি চেক করা হচ্ছে।
# L23 -- final-state বনাম empty-stack PDA অ্যাকসেপ্টেন্স, সমতা কনক্রিটলি verify করা
class PDA:
def __init__(self, states, input_alphabet, stack_alphabet, delta, start_state, start_stack, accept_states=None):
self.states = states
self.input_alphabet = input_alphabet
self.stack_alphabet = stack_alphabet
self.delta = delta
self.start_state = start_state
self.start_stack = start_stack
self.accept_states = accept_states or set()
def _moves(self, state, remaining_input, stack):
moves = []
if not stack:
return moves
top = stack[-1]
for (new_state, push) in self.delta.get((state, None, top), []):
new_stack = list(stack[:-1])
new_stack.extend(reversed(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 _reachable_configs(self, string):
start_config = (self.start_state, string, (self.start_stack,))
seen = {start_config}
frontier = [start_config]
while frontier:
state, remaining, stack = frontier.pop()
yield (state, remaining, stack)
for nxt in self._moves(state, remaining, stack):
if nxt not in seen:
seen.add(nxt)
frontier.append(nxt)
def accepts_by_final_state(self, string):
return any(remaining == "" and state in self.accept_states
for (state, remaining, stack) in self._reachable_configs(string))
def accepts_by_empty_stack(self, string):
return any(remaining == "" and len(stack) == 0
for (state, remaining, stack) in self._reachable_configs(string))
# --- এম্পটি-স্ট্যাক PDA for L = {0^n 1^n}, কোনো accept state দরকার নেই ---
es_delta = {
("q0", "0", "Z0"): [("q0", ("X", "Z0"))],
("q0", "0", "X"): [("q0", ("X", "X"))],
("q0", "1", "X"): [("q1", ())],
("q1", "1", "X"): [("q1", ())],
("q0", None, "Z0"): [("q0", ())], # খালি স্ট্রিং: সরাসরি বটম-মার্কার pop
("q1", None, "Z0"): [("q1", ())], # সব X pop হয়ে গেলে বটম-মার্কারও pop -> স্ট্যাক খালি
}
es_pda = PDA({"q0", "q1"}, {"0", "1"}, {"Z0", "X"}, es_delta, "q0", "Z0", accept_states=set())
def convert_to_final_state_acceptance(empty_stack_pda):
"""স্ট্যান্ডার্ড কনস্ট্রাকশন: empty-stack PDA -> সমতুল্য final-state PDA।
নতুন বটম-মার্কার X0 এবং নতুন স্টেট q0', qf যোগ করা হয়।"""
new_start, new_accept, X0 = "q0__NEW", "qf__NEW", "X0__NEW"
new_states = set(empty_stack_pda.states) | {new_start, new_accept}
new_stack_alphabet = set(empty_stack_pda.stack_alphabet) | {X0}
new_delta = {k: list(v) for k, v in empty_stack_pda.delta.items()}
new_delta[(new_start, None, X0)] = [(empty_stack_pda.start_state,
(empty_stack_pda.start_stack, X0))]
for q in empty_stack_pda.states:
new_delta.setdefault((q, None, X0), []).append((new_accept, (X0,)))
return PDA(new_states, empty_stack_pda.input_alphabet, new_stack_alphabet, new_delta,
new_start, X0, accept_states={new_accept})
fs_pda = convert_to_final_state_acceptance(es_pda)
print("স্ট্রিং | এম্পটি-স্ট্যাক | কনভার্টেড ফাইনাল-স্টেট | মিলছে?")
print("-" * 60)
all_match = True
for s in ["0011", "000111", "", "10", "0011000", "0111"]:
a = es_pda.accepts_by_empty_stack(s)
b = fs_pda.accepts_by_final_state(s)
all_match = all_match and (a == b)
print(f"{s!r:12s} | {a!s:14s} | {b!s:22s} | {a == b}")
print("\nসবগুলো মিলেছে (সমতা নিশ্চিত):", all_match)
convert_to_final_state_acceptance-এ new_delta.setdefault((q, None, X0), []).append(...) লাইনটি — এটি মূল PDA-এর প্রতিটি স্টেট $q$-এর জন্য একটি ε-ট্রানজিশন যোগ করছে, কারণ মূল স্ট্যাক ঠিক কোন স্টেটে খালি হবে তা আমরা আগে থেকে জানি না (নন-ডিটারমিনিস্টিকভাবে বিভিন্ন পাথে বিভিন্ন সময়ে খালি হতে পারে) — তাই প্রতিটি স্টেট থেকেই এই "escape hatch" থাকা প্রয়োজন।
ফাইনাল স্টেট আর এম্পটি স্ট্যাক — দুটো ভিন্ন দেখতে অ্যাকসেপ্টেন্স নিয়ম, কিন্তু উভয়ই একই শ্রেণির ভাষা (কনটেক্সট-ফ্রি ল্যাঙ্গুয়েজ) সংজ্ঞায়িত করে, কারণ যেকোনো একটি কনভেনশনের PDA-কে অন্য কনভেনশনের একটি সমতুল্য PDA-তে যান্ত্রিকভাবে কনভার্ট করা যায় — L24-এ CFG-PDA সমতা প্রমাণের সময় এম্পটি-স্ট্যাক কনভেনশনই বেশি সুবিধাজনক হবে, তাই এই দুই কনভেনশন হাতে-কলমে জানা এখন কাজে লাগবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ এম্পটি-স্ট্যাক কনভেনশনে কেন সাধারণত কোনো accept স্টেট সেট $F$-এর দরকার পড়ে না?
কারণ এম্পটি-স্ট্যাক কনভেনশনে অ্যাকসেপ্টেন্সের শর্তই হলো "স্ট্যাক সম্পূর্ণ খালি" — এটি সম্পূর্ণভাবে স্ট্যাকের অবস্থার উপর নির্ভরশীল, বর্তমান স্টেট কী তার উপর নয়। তাই $F$ সেটটি সংজ্ঞায় থাকলেও অ্যাকসেপ্টেন্সের সিদ্ধান্তে এর কোনো ভূমিকা থাকে না — উপরের কোডে es_pda-এর accept_states=set() (খালি সেট) দেওয়া হয়েছে, কারণ accepts_by_empty_stack মেথড কখনোই accept_states চেক করে না।
প্র ০২ যদি একটি PDA-এর ট্রানজিশন এমনভাবে ডিজাইন করা হয় যে স্ট্যাক মাঝপথে ভুলবশত খালি হয়ে যেতে পারে (ইনপুট এখনো বাকি থাকতেই), তাহলে কী সমস্যা হতে পারে?
মাঝপথে স্ট্যাক খালি হয়ে গেলে _moves-এ if not stack: return moves শর্ত সত্য হয়ে যায় — অর্থাৎ সেই কনফিগারেশন থেকে আর কোনো ট্রানজিশনই সম্ভব নয় (কারণ প্রতিটি ট্রানজিশনের একটি স্ট্যাক-টপ প্রয়োজন, এমনকি ε-ট্রানজিশনেও)। তাই যদি ইনপুট তখনো বাকি থাকে, সেই কম্পিউটেশন পাথ একটি dead end-এ চলে যায় এবং সেই পাথ দিয়ে স্ট্রিং কখনোই accept হতে পারবে না — এই কারণেই একটি বটম-মার্কার সিম্বল (যেমন $Z_0$) রাখা এত গুরুত্বপূর্ণ, যাতে স্ট্যাক অপ্রত্যাশিতভাবে "উধাও" না হয়ে যায়।
প্র ০৩
কনভার্টেড ফাইনাল-স্টেট PDA-তে new_delta[(new_start, None, X0)]-এ কেন (empty_stack_pda.start_stack, X0) — অর্থাৎ দুটো সিম্বল — push করা হচ্ছে, শুধু একটি নয়?
কারণ নতুন PDA-তে $X_0$ সবসময় স্ট্যাকের একেবারে নিচে "স্থায়ী বটম মার্কার" হিসেবে থাকতে হবে (যাতে পরে মূল স্ট্যাক খালি হলে সেটা চেনা যায়), অথচ মূল PDA-এর নিজস্ব বটম মার্কার ($Z_0$)-ও দরকার যাতে মূল PDA স্বাভাবিকভাবে কাজ করতে পারে। তাই দুটো একসাথে push করা হয় — $Z_0$ উপরে (মূল PDA-এর কাজের জন্য), আর $X_0$ তার নিচে (কনভার্শনের "sentinel" হিসেবে, যা কখনো pop হয় না, শুধু উন্মুক্ত হয়)।
অনুশীলন
-
চিন্তা করুন: ফাইনাল-স্টেট → এম্পটি-স্ট্যাক দিকের কনভার্শনে (এই পাঠে শুধু বর্ণনা করা হয়েছে, কোড করা হয়নি) "cleanup" স্টেটে কেন ε-ট্রানজিশন ব্যবহার করা প্রয়োজন, ইনপুট-consuming ট্রানজিশন নয়?
কারণ cleanup ধাপে কোনো নতুন ইনপুট সিম্বল পড়া হচ্ছে না — মূল PDA ইতিমধ্যে পুরো ইনপুট consume করে ফেলেছে (accept স্টেটে পৌঁছে গেছে), এখন শুধু স্ট্যাকের অবশিষ্ট সিম্বলগুলো পরিষ্কার করা হচ্ছে। যদি ইনপুট-consuming ট্রানজিশন ব্যবহার করা হতো, তাহলে এটি ভুলভাবে আরও ইনপুট সিম্বল দাবি করতো, যা ইতিমধ্যে সম্পূর্ণ-consumed স্ট্রিং-এর জন্য ভুল আচরণ হতো।
-
পরীক্ষা করুন: উপরের কোড সেলে টেস্ট তালিকায়
"00001111"($n=4$) যোগ করে চালান —es_pdaএবংfs_pdaকি একই ফলাফল দেয়?হ্যাঁ —
"00001111"$0^n1^n$ ভাষার সদস্য ($n=4$), তাই উভয় PDA-ইTrueফেরত দেবে। এম্পটি-স্ট্যাক PDA-তে স্ট্যাক ঠিক ইনপুট শেষ হওয়ার সাথে সাথে সম্পূর্ণ খালি হয়ে যায়, আর কনভার্টেড ফাইনাল-স্টেট PDA-তে ঠিক তখনই $X_0$ উন্মুক্ত হয়েqf__NEW-এ যাওয়ার সুযোগ তৈরি হয় — দুই কনভেনশনের সমতা আবারও নিশ্চিত হয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ L24-এ এম্পটি-স্ট্যাক কনভেনশন ব্যবহার করেই CFG থেকে PDA বানানোর ক্লাসিক কনস্ট্রাকশন দেখুন।
- L22 · PDA — ফরমাল ডেফিনিশন আগের পাঠ 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 — সব এক জায়গায়।