PDA ও CFG ইকুইভ্যালেন্স
এই পাঠে যা শিখবেন
- CFG-PDA ইকুইভ্যালেন্স থিওরেমের সঠিক বিবৃতি এবং এটি কেন M4-M5-এর কেন্দ্রীয় ফলাফল
- CFG → PDA কনস্ট্রাকশন — leftmost derivation স্ট্যাকের উপর সিমুলেট করার ধারণা
- PDA → CFG দিকের সংক্ষিপ্ত পরিচিতি (আরও জটিল, শুধু উল্লেখ)
- Python-এ একটি সত্যিকারের
cfg_to_pdaফাংশন, বহু স্ট্রিং-এ CFG-এর সাথে তুলনা করে সমতা কনক্রিটলি verify করা
১ · কেন্দ্রীয় থিওরেম
M4 (L17-21) কনটেক্সট-ফ্রি গ্রামার (CFG) নিয়ে কাজ করেছে — রুল-ভিত্তিক একটি ফরম্যালিজম। M5-এ এতক্ষণ PDA নিয়ে কাজ হয়েছে — একটি সম্পূর্ণ ভিন্ন, স্ট্যাক-মেশিন-ভিত্তিক ফরম্যালিজম। দেখতে সম্পূর্ণ আলাদা হলেও, এই দুটোর গণনাশক্তি হুবহু সমান:
$$L \text{ কনটেক্সট-ফ্রি} \iff \text{কোনো PDA } M \text{ আছে যেন } L(M) = L$$
এটি ঠিক M2/L08-এর "NFA-DFA সমতা" আর M2/L11-এর "Kleene's থিওরেম"-এর মতোই একটি দুই-দিকের সমতা — একটি ফরম্যালিজমে বর্ণনাযোগ্য যেকোনো ভাষা অন্য ফরম্যালিজমেও বর্ণনাযোগ্য।
২ · CFG → PDA কনস্ট্রাকশন
এই দিকের প্রমাণ কনস্ট্রাক্টিভ এবং সত্যিই মার্জিত — মূল ধারণা হলো, PDA-এর স্ট্যাক সরাসরি একটি sentential formCFG-এর derivation প্রক্রিয়ায় "আংশিকভাবে" তৈরি হওয়া স্ট্রিং — কিছু অংশ এখনো ভ্যারিয়েবল, কিছু অংশ টার্মিনাল হয়ে গেছে (L17-এর derivation ধারণার সাথে সরাসরি সম্পর্কিত) ধরে রাখে, আর leftmost derivation সরাসরি স্ট্যাকের উপর সিমুলেট করে —
- শুরুতে স্ট্যাকে শুধু স্টার্ট সিম্বল $S$ push করা হয়।
- যদি স্ট্যাকের টপে একটি ভ্যারিয়েবল $A$ থাকে, তাহলে নন-ডিটারমিনিস্টিকভাবে কোনো একটি rule $A \to \alpha$ বেছে নিয়ে $A$-কে $\alpha$ দিয়ে replace করা হয় (pop $A$, push $\alpha$ — বিপরীত ক্রমে, যাতে $\alpha$-এর leftmost সিম্বলটি টপে থাকে)।
- যদি স্ট্যাকের টপে একটি টার্মিনাল থাকে, তাহলে সেটি অবশ্যই পরবর্তী ইনপুট সিম্বলের সাথে মিলতে হবে (দুটোই consume হয়ে যায়)।
- ইনপুট শেষ হয়ে গেলে এবং স্ট্যাক সম্পূর্ণ খালি হয়ে গেলে (L23-এর এম্পটি-স্ট্যাক কনভেনশন) accept।
এই কনস্ট্রাকশনে PDA-এর কোনো "accept স্টেট"-এর প্রয়োজনই পড়ে না — মাত্র একটিই স্টেট যথেষ্ট (নিচের কোডে "q")। অ্যাকসেপ্টেন্সের স্বাভাবিক সংকেত হলো স্ট্যাক খালি হওয়া — অর্থাৎ পুরো sentential form সম্পূর্ণভাবে টার্মিনালে derive হয়ে গেছে এবং ইনপুটের সাথে হুবহু মিলে গেছে। এই কারণেই L23-এর এম্পটি-স্ট্যাক কনভেনশন এখানে এতটা স্বাভাবিকভাবে খাপ খায়।
৩ · PDA → CFG দিক (সংক্ষেপে)
উল্টো দিকের প্রমাণ (একটি এম্পটি-স্ট্যাক PDA থেকে একটি সমতুল্য CFG বানানো) অনেক বেশি জটিল — এটি এমন ভ্যারিয়েবল ব্যবহার করে যা এনকোড করে "PDA স্টেট $p$ থেকে শুরু করে, স্ট্যাক-টপ সিম্বল $X$ পুরোপুরি pop হয়ে স্টেট $q$-এ পৌঁছানো সম্ভব কি না" — প্রতিটি এমন $(p, X, q)$ ট্রিপলের জন্য একটি ভ্যারিয়েবল। এই কনস্ট্রাকশনের সম্পূর্ণ বাস্তবায়ন এই পাঠের আওতার বাইরে — থিওরেমটি সত্য এই তথ্যই এখানে যথেষ্ট, প্রমাণের বিস্তারিত ছাড়া।
৪ · L17-এর গ্রামারে প্রয়োগ ও Python-এ verify করা
L17-এ দেখা ব্যালেন্সড-বন্ধনী গ্রামার — $S \to (S)S \mid \varepsilon$ — ব্যবহার করে নিচের কোড সেলে cfg_to_pda ফাংশনটি প্রয়োগ করা হবে। তারপর দৈর্ঘ্য ৬ পর্যন্ত $\{(, )\}$-এর উপর প্রতিটি সম্ভাব্য স্ট্রিং (শুধু ব্যালেন্সড নয়, ভুল/অ্যানব্যালেন্সডসহ সবগুলো) — CFG-এর জেনারেশন ফাংশন আর কনস্ট্রাক্টেড PDA — দুটোতেই টেস্ট করে ফলাফল হুবহু মেলে কিনা তা কনক্রিটলি verify করা হবে।
# L24 -- CFG -> PDA কনস্ট্রাকশন, L17-স্টাইল CFG জেনারেশনের বিপরীতে সমতা verify করা
import itertools
# ---- L17-স্টাইল Grammar (ব্যালেন্সড বন্ধনী: S -> (S)S | epsilon) ----
grammar = {
"S": [["(", "S", ")", "S"], []], # [] মানে S -> epsilon
}
start_symbol = "S"
def generate_language_up_to_length(grammar, start, max_length):
"""leftmost-derivation sentential form-এর উপর BFS, দৈর্ঘ্য-বাউন্ড পর্যন্ত টার্মিনাল স্ট্রিং জমা করে"""
variables = set(grammar.keys())
language = set()
seen = {(start,)}
frontier = [(start,)]
while frontier:
new_frontier = []
for form in frontier:
idx = next((i for i, sym in enumerate(form) if sym in variables), None)
if idx is None:
s = "".join(form)
if len(s) <= max_length:
language.add(s)
continue
var = form[idx]
for rhs in grammar[var]:
new_form = form[:idx] + tuple(rhs) + form[idx + 1:]
terminal_len = sum(1 for sym in new_form if sym not in variables)
if terminal_len > max_length:
continue
if new_form not in seen:
seen.add(new_form)
new_frontier.append(new_form)
frontier = new_frontier
return language
lang = generate_language_up_to_length(grammar, start_symbol, 6)
print("CFG-জেনারেটেড ব্যালেন্সড-বন্ধনী স্ট্রিং (দৈর্ঘ্য <=6):", sorted(lang, key=lambda s: (len(s), s)))
class PDA:
def __init__(self, states, input_alphabet, stack_alphabet, delta, start_state, start_stack):
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
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 accepts_by_empty_stack(self, string):
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 len(stack) == 0:
return True
for nxt in self._moves(state, remaining, stack):
if nxt not in seen:
seen.add(nxt)
frontier.append(nxt)
return False
def cfg_to_pda(grammar, start_symbol, terminal_alphabet):
"""CFG -> PDA: একটিমাত্র স্টেট, স্ট্যাক নিজেই বর্তমান sentential form ধরে রাখে --
ভ্যারিয়েবল নন-ডিটারমিনিস্টিকভাবে rule অনুযায়ী expand হয়, টার্মিনাল ইনপুটের সাথে সরাসরি মেলে।"""
variables = set(grammar.keys())
stack_alphabet = variables | set(terminal_alphabet)
delta = {}
for var, rules in grammar.items():
delta[("q", None, var)] = [("q", tuple(rhs)) for rhs in rules] # rhs push, leftmost টপে
for a in terminal_alphabet:
delta[("q", a, a)] = [("q", ())] # ইনপুট সিম্বল == স্ট্যাক-টপ টার্মিনাল হলে দুটোই pop/consume
return PDA({"q"}, set(terminal_alphabet), stack_alphabet, delta, "q", start_symbol)
pda = cfg_to_pda(grammar, start_symbol, {"(", ")"})
# দৈর্ঘ্য 6 পর্যন্ত {(,)}-এর উপর সব সম্ভাব্য স্ট্রিং (member + non-member সবগুলো) টেস্ট করা হচ্ছে
MAX_LEN = 6
mismatches = []
tested = 0
for length in range(0, MAX_LEN + 1):
for combo in itertools.product("()", repeat=length):
s = "".join(combo)
tested += 1
cfg_member = s in lang
pda_member = pda.accepts_by_empty_stack(s)
if cfg_member != pda_member:
mismatches.append((s, cfg_member, pda_member))
print(f"\nমোট {tested}টি স্ট্রিং টেস্ট করা হয়েছে (দৈর্ঘ্য {MAX_LEN} পর্যন্ত সবগুলো)।")
print("অমিল পাওয়া গেছে:", mismatches)
print("CFG-PDA সমতা নিশ্চিত হলো:", len(mismatches) == 0)
cfg_to_pda-এ delta[("q", None, var)]-এর মান একটি তালিকা — গ্রামারের $A$-এর জন্য একাধিক rule থাকলে (যেমন $S \to (S)S$ এবং $S \to \varepsilon$ দুটোই), উভয়ই এই তালিকায় ঢুকে যায়, আর _moves স্বয়ংক্রিয়ভাবে উভয় সম্ভাবনাই নন-ডিটারমিনিস্টিকভাবে explore করে — ঠিক যেভাবে একটি CFG-এর derivation-এ একাধিক rule-চয়েস সম্ভব হয়, সেই একই নন-ডিটারমিনিজম এখানে PDA-এর ট্রানজিশনে সরাসরি প্রতিফলিত হয়েছে।
CFG আর PDA — একটি গ্রামার-রুল-ভিত্তিক, আরেকটি স্ট্যাক-মেশিন-ভিত্তিক — সম্পূর্ণ ভিন্ন দেখতে হলেও, CFG-এর derivation প্রক্রিয়াকে সরাসরি PDA-এর স্ট্যাক-অপারেশনে অনুবাদ করা যায় বলেই এই দুটো একই শ্রেণির ভাষা (কনটেক্সট-ফ্রি ল্যাঙ্গুয়েজ) সংজ্ঞায়িত করে — M2-এর NFA-DFA সমতার ঠিক এক ধাপ উপরের এই ফলাফলটিই M4-M5-এর কেন্দ্রীয় সেতুবন্ধন।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
উপরের cfg_to_pda-এ তৈরি PDA-তে মাত্র একটিই স্টেট ("q") — তাহলে সব "মেমরি" কোথায় রাখা হচ্ছে?
স্ট্যাকে — এটাই এই কনস্ট্রাকশনের মূল কৌশল। সাধারণত স্টেট দিয়ে "কোথায় আছি" তথ্য ট্র্যাক করা হয়, কিন্তু এখানে বর্তমান sentential form (কোন অংশ derive হয়েছে, কোন অংশ এখনো ভ্যারিয়েবল) সম্পূর্ণভাবে স্ট্যাকের কনটেন্টে এনকোড করা আছে — তাই স্টেটের কোনো বাড়তি ভূমিকা লাগে না। এটি PDA-এর স্ট্যাক কতটা শক্তিশালী মেমরি, তার একটি চমৎকার উদাহরণ।
প্র ০২ এই কনস্ট্রাকশন যদি $S \to \varepsilon$ rule ছাড়া শুধু $S \to (S)S$ দিয়ে করা হতো, তাহলে PDA-এর জন্য কী সমস্যা হতো?
তাহলে $S$ কখনো টার্মিনালে "শেষ" হতে পারতো না — প্রতিবার $S$ expand করলে আরেকটি নতুন $S$ তৈরি হয়ে যেত (recursion-এর কোনো base case ছাড়া), এবং derivation কখনো শেষ হতো না। PDA-এর ক্ষেত্রে এর মানে হলো স্ট্যাক থেকে $S$ কখনো সম্পূর্ণভাবে বাদ যেত না — অসীম push চলতেই থাকতো, কোনো স্ট্রিং-ই (এমনকি "" ও না) কখনো accept হতো না। $\varepsilon$-rule-টিই derivation-এর "base case" — L03-এর induction-এর base case-এর সাথে সরাসরি সম্পর্কিত।
প্র ০৩ কোড সেলে ১২৭টি স্ট্রিং টেস্ট করা হয়েছে, কিন্তু এতে শুধু ৯টি প্রকৃত ব্যালেন্সড-বন্ধনী স্ট্রিং আছে (দৈর্ঘ্য ৬ পর্যন্ত) — বাকি ১১৮টি কেন টেস্ট করা দরকার?
কারণ সমতা প্রমাণ করতে হলে শুধু "সদস্য স্ট্রিংগুলো সঠিকভাবে accept হচ্ছে" তা যথেষ্ট নয় — "অ-সদস্য স্ট্রিংগুলো সঠিকভাবে reject হচ্ছে" তাও নিশ্চিত করতে হয় (নাহলে PDA-টি ভুলভাবে অতিরিক্ত স্ট্রিং accept করে ফেলতে পারতো, একটি "false positive")। দৈর্ঘ্য ৬ পর্যন্ত $\{(,)\}$-এর উপর প্রতিটি সম্ভাব্য স্ট্রিং টেস্ট করে (যেমন ")(", "(((" ইত্যাদি ভুল/অ্যানব্যালেন্সড স্ট্রিংসহ) নিশ্চিত করা হচ্ছে PDA আর CFG সম্পূর্ণ সেটেই একমত — এটিই একটি প্রকৃত exhaustive সমতা যাচাই।
অনুশীলন
-
ট্রেস করুন: হাতে-কলমে
cfg_to_pda-এর PDA-তে ইনপুট"()"-এর জন্য স্ট্যাকের পরিবর্তনগুলো লিখুন — শুরু থেকে accept পর্যন্ত।স্ট্যাক:
[S]→ ($S \to (S)S$ rule নিয়ে)[(,S,),S]("(" টপে) → ইনপুট "(" মেলে, pop:[S,),S]→ ($S \to \varepsilon$ rule নিয়ে, টপ $S$)[),S]→ ইনপুট ")" মেলে, pop:[S]→ ($S \to \varepsilon$)[]— স্ট্যাক খালি, ইনপুট শেষ, accept। -
পরীক্ষা করুন: কোড সেলে
MAX_LEN-এর মান ৭-এ বাড়িয়ে আবার চালান —mismatchesতালিকা কি এখনও খালি থাকে? কতগুলো স্ট্রিং টেস্ট হয়?হ্যাঁ,
mismatchesখালিই থাকা উচিত — সমতা থিওরেম যেকোনো দৈর্ঘ্যের জন্যই সত্য, শুধু একটি নির্দিষ্ট বাউন্ডের জন্য নয়। দৈর্ঘ্য ৭ পর্যন্ত $\{(,)\}$-এর উপর মোট $2^0+2^1+\dots+2^7 = 255$টি স্ট্রিং টেস্ট হবে (আগের ১২৭-এর প্রায় দ্বিগুণ)।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ L25-এ ডিটারমিনিস্টিক বনাম নন-ডিটারমিনিস্টিক PDA-এর গুরুত্বপূর্ণ পার্থক্য দেখুন — যা M2-এর NFA-DFA সমতা থেকে সম্পূর্ণ ভিন্ন একটি ফলাফল।
- L23 · 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 — সব এক জায়গায়।