পাঠ ২৪ · ৫৬-এর মধ্যে · মডিউল ৫
Home / Courses / Formal Language & Automata Theory / Theory of Computation / পুশডাউন অটোমাটা

PDA ও CFG ইকুইভ্যালেন্স

PDA & CFG equivalence
৯ মিনিট পড়া মাঝারি · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • 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 করা হবে।

Python
# 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-এর ট্রানজিশনে সরাসরি প্রতিফলিত হয়েছে।
মূল কথা · Key takeaway

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 সমতা যাচাই।

অনুশীলন

  1. ট্রেস করুন: হাতে-কলমে 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।

  2. পরীক্ষা করুন: কোড সেলে MAX_LEN-এর মান ৭-এ বাড়িয়ে আবার চালান — mismatches তালিকা কি এখনও খালি থাকে? কতগুলো স্ট্রিং টেস্ট হয়?

    হ্যাঁ, mismatches খালিই থাকা উচিত — সমতা থিওরেম যেকোনো দৈর্ঘ্যের জন্যই সত্য, শুধু একটি নির্দিষ্ট বাউন্ডের জন্য নয়। দৈর্ঘ্য ৭ পর্যন্ত $\{(,)\}$-এর উপর মোট $2^0+2^1+\dots+2^7 = 255$টি স্ট্রিং টেস্ট হবে (আগের ১২৭-এর প্রায় দ্বিগুণ)।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
L23 · PDA অ্যাকসেপ্টেন্স — ফাইনাল স্টেট বনাম এম্পটি স্ট্যাক