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

ডিটারমিনিস্টিক বনাম নন-ডিটারমিনিস্টিক PDA

Deterministic vs nondeterministic PDA
৭ মিনিট পড়া মাঝারি · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • DPDA-এর সঠিক টেকনিক্যাল সংজ্ঞা — কোন শর্তে একটি PDA "ডিটারমিনিস্টিক"
  • কেন DPDA-PDA সমতা M2-এর NFA-DFA সমতার মতো নয় — এই পার্থক্য কেন গুরুত্বপূর্ণ
  • $\{ww^R\}$ ক্লাসিক উদাহরণ — কেন এটি কনটেক্সট-ফ্রি কিন্তু ডিটারমিনিস্টিক-কনটেক্সট-ফ্রি নয়
  • Python-এ কনফিগারেশন-কাউন্ট ট্র্যাক করে জেনুইন নন-ডিটারমিনিজম বনাম জেনুইন ডিটারমিনিজম প্রদর্শন

১ · DPDA-এর সংজ্ঞা

ডিটারমিনিস্টিক PDA (DPDA)Deterministic PDAএকটি PDA যেখানে প্রতিটি কনফিগারেশনে সর্বোচ্চ একটিই সম্ভাব্য পরবর্তী মুভ থাকে — কোনো প্রকৃত "চয়েস" নেই। হলো এমন একটি PDA (L22) যেখানে, প্রতিটি কনফিগারেশনের জন্য, সর্বোচ্চ একটিই সম্ভাব্য পরবর্তী মুভ থাকে। ফরমালি দুটো শর্ত একসাথে পূরণ হতে হয়:

  • কোনো স্টেটে, একই স্ট্যাক-টপ সিম্বলের জন্য একইসাথে একটি ε-ট্রানজিশন এবং একটি সিম্বল-পড়া ট্রানজিশন — দুটোই উপলব্ধ থাকতে পারবে না।
  • প্রতিটি প্রকৃত (স্টেট, ইনপুট সিম্বল, স্ট্যাক-টপ) কম্বিনেশনের জন্য সর্বোচ্চ একটি ট্রানজিশন গন্তব্য থাকতে হবে।

L22/L23-এর $0^n1^n$ PDA-টি খেয়াল করলে দেখা যাবে এটি প্রকৃতপক্ষে ইতিমধ্যেই ডিটারমিনিস্টিক ছিল — প্রতিটি স্টেটে ε-মুভ আর সিম্বল-মুভ সবসময় ভিন্ন স্ট্যাক-টপ সিম্বলের উপর নির্ভরশীল ছিল, কখনো একসাথে নয়।

গুরুত্বপূর্ণ পার্থক্য — M2-এর NFA=DFA-এর সাথে তুলনা করবেন না

M2/L08-এ প্রমাণিত হয়েছিল প্রতিটি NFA-এর জন্যই একটি সমতুল্য DFA বানানো যায় — নন-ডিটারমিনিজম শুধু বর্ণনার সুবিধা দেয়, কোনো বাড়তি ক্ষমতা নয়। PDA-এর ক্ষেত্রে এই একই যুক্তি খাটে না — এটাই এই পাঠের সবচেয়ে গুরুত্বপূর্ণ, প্রায়ই বিস্ময়কর ফলাফল। DPDA সাধারণ PDA-এর তুলনায় strictly কম শক্তিশালী। কেন? কারণ স্ট্যাক-ভিত্তিক মেমরির কারণে একটি DPDA "backtrack" করে ভিন্ন একটি চয়েস আবার চেষ্টা করতে পারে না (একবার স্ট্যাক থেকে কিছু pop হয়ে গেলে সেই তথ্য হারিয়ে যায়) — যেখানে একটি DFA-তে সসীম স্টেট থাকায় "সব সম্ভাব্য NFA-পাথ একসাথে ট্র্যাক করা" (subset construction) সবসময় সম্ভব হয়।

২ · ক্লাসিক উদাহরণ — $\{ww^R\}$ প্যালিনড্রোম

$L = \{ww^R : w \in \{0,1\}^*\}$ (জোড়-দৈর্ঘ্যের প্যালিনড্রোম, যেমন "0110", "1001") — এই ভাষাটি কনটেক্সট-ফ্রি, এবং একটি নন-ডিটারমিনিস্টিক PDA দিয়ে স্বীকৃত হয়: প্রথমার্ধের প্রতিটি সিম্বল push করতে থাকে, আর নন-ডিটারমিনিস্টিকভাবে "গেস" করে কোথায় মাঝপথ — সেই মুহূর্তে matching ফেজে চলে যায়, বাকি ইনপুটকে স্ট্যাকের সাথে মিলিয়ে pop করতে থাকে।

কিন্তু এই একই ভাষা কোনো DPDA দিয়ে স্বীকৃত হতে পারে না — informally, কারণ একটি ডিটারমিনিস্টিক মেশিন সঠিকভাবে "গেস" করতে পারে না ঠিক কোথায় মাঝপথ, এবং সাধারণভাবে মাঝপথ শনাক্ত করার কোনো ডিটারমিনিস্টিক উপায় নেই (ইনপুট আগে থেকে না দেখেই)। এটি প্রমাণ করে যে DCFL (ডিটারমিনিস্টিক কনটেক্সট-ফ্রি ল্যাঙ্গুয়েজ) — DPDA দ্বারা স্বীকৃত শ্রেণি — কনটেক্সট-ফ্রি ল্যাঙ্গুয়েজের একটি প্রকৃত সাবসেট।

বাস্তব প্রাসঙ্গিকতা — DCFL কেন গুরুত্বপূর্ণ

DCFL শ্রেণি নিছক তাত্ত্বিক কৌতূহল নয় — ../programming-languages-compilers/-এর M5-এর LL/LR পার্সিং টেকনিকগুলো ঠিক এই DCFL-এর জন্যই ডিজাইন করা, কারণ সেগুলো efficient, ডিটারমিনিস্টিক পার্সিং করতে চায় (backtracking ছাড়া, এক-পাসে)। একটি প্রোগ্রামিং ল্যাঙ্গুয়েজের গ্রামার যদি DCFL না হয়, তাহলে সাধারণ LL/LR পার্সার দিয়ে efficiently পার্স করা কঠিন হয়ে পড়ে — এই কারণেই বাস্তব কম্পাইলার ডিজাইনাররা ইচ্ছাকৃতভাবে DCFL-বান্ধব গ্রামার বেছে নেন।

৩ · Python-এ জেনুইন নন-ডিটারমিনিজম বনাম জেনুইন ডিটারমিনিজম

নিচের কোড সেলে L22-এর PDA ক্লাস পুনরায় ব্যবহার করে দুটো PDA বানানো হয়েছে, এবং একটি trace মেথড প্রতিটি ইনপুট সিম্বল consume করার পর কতগুলো distinct, epsilon-settled কনফিগারেশন একসাথে "জীবিত" আছে তা গণনা করে —

  • ডিটারমিনিস্টিক $0^n1^n$ PDA — এখন তিনটি স্টেট দিয়ে পুনর্গঠিত, যাতে কোনো স্টেটেই ε-মুভ আর সিম্বল-মুভ একই স্ট্যাক-টপে সংঘর্ষে না আসে (প্রকৃত DPDA)।
  • নন-ডিটারমিনিস্টিক $\{ww^R\}$ PDA — যেকোনো মুহূর্তে "আরও পুশ করবো" আর "এখনই মাঝপথ ধরে নেবো" — দুটো মুভই একসাথে সক্রিয় থাকতে পারে, প্রকৃত ফর্ক তৈরি করে।
Python
# L25 -- ডিটারমিনিস্টিক বনাম নন-ডিটারমিনিস্টিক PDA, কনফিগারেশন-কাউন্ট দিয়ে verify

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
        self.start_state = start_state
        self.start_stack = start_stack
        self.accept_states = accept_states

    def _eps_moves(self, state, stack):
        if not stack:
            return []
        top = stack[-1]
        moves = []
        for (ns, push) in self.delta.get((state, None, top), []):
            new_stack = list(stack[:-1]); new_stack.extend(reversed(push))
            moves.append((ns, tuple(new_stack)))
        return moves

    def _sym_moves(self, state, stack, a):
        if not stack:
            return []
        top = stack[-1]
        moves = []
        for (ns, push) in self.delta.get((state, a, top), []):
            new_stack = list(stack[:-1]); new_stack.extend(reversed(push))
            moves.append((ns, tuple(new_stack)))
        return moves

    def _eps_close(self, configs):
        frontier, closure = list(configs), set(configs)
        while frontier:
            state, stack = frontier.pop()
            for cfg in self._eps_moves(state, stack):
                if cfg not in closure:
                    closure.add(cfg); frontier.append(cfg)
        return closure

    def _has_symbol_move(self, state, top):
        return any((state, a, top) in self.delta for a in self.input_alphabet)

    def _settled(self, configs):
        """একটি কনফিগারেশন 'transient' (উপেক্ষাযোগ্য) যদি তার একমাত্র সম্ভাব্য মুভ হয় একটি
        ε-মুভ (কোনো প্রতিদ্বন্দ্বী সিম্বল-মুভ ছাড়া) -- সেটি কোনো প্রকৃত 'চয়েস' নয়, শুধু
        যান্ত্রিকভাবে পরের কনফিগারেশনে গড়িয়ে যাওয়া। ε-মুভ এবং সিম্বল-মুভ একসাথে থাকলে
        সেটাই প্রকৃত ফর্ক -- তখন কনফিগারেশনটি গণনায় থেকে যায়।"""
        settled = set()
        for (s, st) in configs:
            top = st[-1] if st else None
            has_eps = top is not None and (s, None, top) in self.delta
            has_sym = top is not None and self._has_symbol_move(s, top)
            if not (has_eps and not has_sym):
                settled.add((s, st))
        return settled

    def trace(self, string):
        """প্রতিটি ইনপুট সিম্বল consume করার পর কতগুলো distinct সমান্তরাল কনফিগারেশন
        জীবিত আছে তা রিটার্ন করে -- (non)determinism-এর কনক্রিট প্রমাণ।"""
        configs = self._eps_close({(self.start_state, (self.start_stack,))})
        sizes = [len(self._settled(configs))]
        for a in string:
            nxt = set()
            for (state, stack) in configs:
                nxt.update(self._sym_moves(state, stack, a))
            configs = self._eps_close(nxt)
            sizes.append(len(self._settled(configs)))
        accepted = any(s in self.accept_states for (s, st) in configs)
        return accepted, sizes


def is_deterministic_pda(pda):
    """স্ট্রাকচারাল DPDA চেক: কোনো (state, stack-top)-এ ε-মুভ ও সিম্বল-মুভ একসাথে নয়,
    এবং কোনো (state, symbol, stack-top)-এর একাধিক গন্তব্য নয়।"""
    by_state_top = {}
    for (state, sym, top), dests in pda.delta.items():
        if len(dests) > 1:
            return False
        by_state_top.setdefault((state, top), []).append(sym)
    return all(not (None in syms and len(syms) > 1) for syms in by_state_top.values())


# --- ডিটারমিনিস্টিক PDA: L = {0^n 1^n} -- কোনো কনফিগারেশনেই একের বেশি মুভ নেই ---
det_delta = {
    ("q0", "0", "Z0"): [("qread0", ("X", "Z0"))],
    ("qread0", "0", "X"): [("qread0", ("X", "X"))],
    ("qread0", "1", "X"): [("qread1", ())],
    ("qread1", "1", "X"): [("qread1", ())],
    ("qread1", None, "Z0"): [("qacc", ("Z0",))],
}
det_pda = PDA({"q0", "qread0", "qread1", "qacc"}, {"0", "1"}, {"Z0", "X"},
              det_delta, "q0", "Z0", {"q0", "qacc"})

print("=== ডিটারমিনিস্টিক 0^n1^n PDA ===")
for s in ["0011", "000111", "", "10", "0011000", "0111"]:
    acc, sizes = det_pda.trace(s)
    print(f"  {s!r:10s} accept={acc!s:5s} প্রতি-ধাপে-কনফিগ={sizes} max={max(sizes)}")
print("is_deterministic_pda(det_pda):", is_deterministic_pda(det_pda))

# --- নন-ডিটারমিনিস্টিক PDA: L = {w w^R} -- মাঝপথ গেস করে (epsilon fork) ---
pal_delta = {
    ("push", "0", "Z0"): [("push", ("0", "Z0"))], ("push", "1", "Z0"): [("push", ("1", "Z0"))],
    ("push", "0", "0"): [("push", ("0", "0"))],   ("push", "0", "1"): [("push", ("0", "1"))],
    ("push", "1", "0"): [("push", ("1", "0"))],   ("push", "1", "1"): [("push", ("1", "1"))],
    ("push", None, "Z0"): [("pop", ("Z0",))],     # নন-ডিটারমিনিস্টিক গেস: এটাই মাঝপথ
    ("push", None, "0"): [("pop", ("0",))],
    ("push", None, "1"): [("pop", ("1",))],
    ("pop", "0", "0"): [("pop", ())],  ("pop", "1", "1"): [("pop", ())],
    ("pop", None, "Z0"): [("acc", ("Z0",))],
}
pal_pda = PDA({"push", "pop", "acc"}, {"0", "1"}, {"Z0", "0", "1"},
              pal_delta, "push", "Z0", {"acc"})

print("\n=== নন-ডিটারমিনিস্টিক w-w^R প্যালিনড্রোম PDA ===")
for s in ["0110", "1001", "", "0101", "0010", "111"]:
    acc, sizes = pal_pda.trace(s)
    print(f"  {s!r:10s} accept={acc!s:5s} প্রতি-ধাপে-কনফিগ={sizes} max={max(sizes)}")
print("is_deterministic_pda(pal_pda):", is_deterministic_pda(pal_pda))

    
কোড চালিয়ে দেখুন — $0^n1^n$ PDA-এর প্রতি-ধাপে-কনফিগ সবসময় [1, 1, 1, ...] (কখনো ১-এর বেশি না), আর is_deterministic_pda True ফেরত দেয়। প্যালিনড্রোম PDA-তে এই সংখ্যা ২-৩-৪ পর্যন্ত ওঠে (একাধিক "মাঝপথ-গেস" একসাথে সক্রিয় থাকে), আর is_deterministic_pda False ফেরত দেয় — ("push", None, "0") স্টেট-টপ কম্বিনেশনে ε-মুভ এবং ("push", "0", "0")-এর মতো সিম্বল-মুভ একসাথে সংজ্ঞায়িত থাকার কারণে।
মূল কথা · Key takeaway

M2-তে নন-ডিটারমিনিজম ছিল নিছক সুবিধার — NFA=DFA। PDA-তে নন-ডিটারমিনিজম প্রকৃত অতিরিক্ত ক্ষমতা দেয় — DPDA কঠোরভাবে সাধারণ PDA-এর চেয়ে দুর্বল, আর $\{ww^R\}$ এর একটি কনক্রিট, প্রমাণিত উদাহরণ। এই পার্থক্য শুধু তাত্ত্বিক নয় — বাস্তব কম্পাইলার ডিজাইনে (LL/LR পার্সিং) DCFL-এর সীমাবদ্ধতার সরাসরি প্রভাব আছে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ $\{ww^R\}$-এর জন্য একটি DPDA বানানোর চেষ্টা করলে ঠিক কোথায় আটকে যাবে?

মাঝপথ শনাক্ত করার জায়গায়। একটি ডিটারমিনিস্টিক মেশিনকে অবশ্যই একটি নির্দিষ্ট, পূর্ব-নির্ধারিত নিয়মে সিদ্ধান্ত নিতে হবে কখন push করা বন্ধ করে pop শুরু করবে — কিন্তু ইনপুটের কোনো নির্দিষ্ট মার্কার নেই যা মাঝপথ নির্দেশ করে (যেমন "0110"-তে ২য় আর ৩য় সিম্বলের মাঝে থামতে হবে, অথচ ইনপুটে এমন কোনো সংকেত নেই)। নন-ডিটারমিনিস্টিক PDA এটি "গেস করে সবগুলো সম্ভাবনা সমান্তরালে চেষ্টা করে" সমাধান করে — কিন্তু একটি DPDA-এর একবারে একটিই পথ অনুসরণ করার ক্ষমতা আছে, তাই ভুল গেস করলে backtrack করার কোনো উপায় নেই।

প্র ০২ বিজোড়-দৈর্ঘ্যের প্যালিনড্রোম (মাঝখানে একটি বিশেষ মার্কার সিম্বলসহ, যেমন $wcw^R$) কি ডিটারমিনিস্টিকভাবে স্বীকৃত হতে পারে?

হ্যাঁ! এটাই আকর্ষণীয় পয়েন্ট — যদি মাঝপথে একটি স্পষ্ট মার্কার সিম্বল $c$ (যা $\{0,1\}$-এ নেই) থাকে, তাহলে PDA ঠিক জানে কখন push থেকে pop-এ পরিবর্তন করতে হবে ($c$ দেখলেই, কোনো "গেস" ছাড়াই) — তাই $\{wcw^R\}$ একটি DCFL, কিন্তু মার্কার-ছাড়া $\{ww^R\}$ নয়। এটি স্পষ্টভাবে দেখায় সমস্যাটা "প্যালিনড্রোম" ধারণায় নয়, বরং মাঝপথ শনাক্তযোগ্যতায়।

প্র ০৩ উপরের কোডে is_deterministic_pda ফাংশনটি শুধু delta-এর গঠন দেখে সিদ্ধান্ত নেয়, কোনো স্ট্রিং সিমুলেট না করেই — এটি কি নির্ভরযোগ্য?

হ্যাঁ, এবং এটাই সঠিক পদ্ধতি — DPDA-এর সংজ্ঞা নিজেই একটি স্ট্রাকচারাল শর্ত (ট্রানজিশন ফাংশনের গঠনের উপর), কোনো নির্দিষ্ট ইনপুটের আচরণের উপর নয়। একবার delta-তে কোনো (state,top) জোড়ায় ε-মুভ ও সিম্বল-মুভ একসাথে পাওয়া গেলে, PDA-টি ডিটারমিনিস্টিক নয় — তা যেকোনো নির্দিষ্ট ইনপুট স্ট্রিং-এ সেই সংঘর্ষ আদৌ ট্রিগার হোক বা না হোক। রানটাইম trace-ভিত্তিক কনফিগ-কাউন্ট শুধু এই স্ট্রাকচারাল সত্যের একটি কনক্রিট, পর্যবেক্ষণযোগ্য পরিণতি দেখায়।

অনুশীলন

  1. চিন্তা করুন: $L = \{0^i1^j : i \neq j\}$ — এই ভাষাটি কি DCFL হতে পারে? (ইঙ্গিত: PDA-কে "$i=j$ কিনা" প্রথমে সম্পূর্ণ নিশ্চিত হতে হয় কি না ভাবুন।)

    হ্যাঁ, এটি একটি DCFL — একটি ডিটারমিনিস্টিক PDA প্রথমে সব 0 push করতে পারে, তারপর 1 পড়ার সময় pop করতে থাকে; যদি 1 শেষ হওয়ার আগেই স্ট্যাক (শুধু বটম-মার্কার বাদে) খালি হয়ে যায় ($j > i$) অথবা 1 শেষ হয়ে যাওয়ার পরও স্ট্যাকে এখনো সিম্বল থাকে ($i > j$), উভয় ক্ষেত্রেই ডিটারমিনিস্টিকভাবে শনাক্ত করা যায় — কোনো "গেস"-এর দরকার নেই, কারণ প্রতিটি পদক্ষেপ শুধু বর্তমান ইনপুট সিম্বল আর স্ট্যাক-টপের উপর ভিত্তি করেই একদম নির্দিষ্টভাবে ঠিক করা যায়।

  2. পরীক্ষা করুন: উপরের কোড সেলে pal_pda.trace("00") চালিয়ে দেখুন — দৈর্ঘ্য-২ স্ট্রিং-এ কনফিগ-কাউন্ট কেমন আচরণ করে? "00" কি প্যালিনড্রোম হিসেবে accept হয়?

    "00" একটি বৈধ $ww^R$ (যেখানে $w$="0"), তাই accept=True হওয়ার কথা। কনফিগ-কাউন্ট শুরুতে একাধিক (মাঝপথ-গেস-সহ) থেকে ধীরে ধীরে সংকুচিত হবে, যতক্ষণ না সঠিক গেস-পাথটি (মাঝপথ ঠিক ১ম আর ২য় সিম্বলের মাঝে) acc স্টেটে পৌঁছায় — এটিই নন-ডিটারমিনিজমের বাস্তব সুবিধা: ভুল গেসগুলো স্বয়ংক্রিয়ভাবে dead-end হয়ে যায়, সঠিকটি টিকে থাকে।

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

আগের পাঠ
L24 · PDA ও CFG ইকুইভ্যালেন্স