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

PDA — ফরমাল ডেফিনিশন

PDA — formal definition
৮ মিনিট পড়া মাঝারি · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • 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 স্বভাবতই নন-ডিটারমিনিস্টিক

PDA-এর সাধারণ (general) সংজ্ঞা নন-ডিটারমিনিস্টিক — একই (স্টেট, ইনপুট সিম্বল, স্ট্যাক-টপ) কম্বিনেশনের জন্য একাধিক সম্ভাব্য (নতুন স্টেট, push-স্ট্রিং) জোড়া থাকতে পারে, ফাংশনের রেঞ্জ $\mathcal{P}(Q \times \Gamma^*)$ (একটি সেট) হওয়াতেই এটা স্পষ্ট। M2-এর NFA=DFA (L08) থেকে ভিন্ন — L25-এ দেখা যাবে, PDA-এর ক্ষেত্রে নন-ডিটারমিনিজম সত্যিকারের বাড়তি ক্ষমতা দেয়, শুধু সুবিধা নয়।

৩ · কনফিগারেশন — কম্পিউটেশনের একটি স্ন্যাপশট

একটি কনফিগারেশনConfigurationPDA-এর কম্পিউটেশনের একটি সম্পূর্ণ instantaneous স্ন্যাপশট — (বর্তমান স্টেট, বাকি থাকা ইনপুট, বর্তমান স্ট্যাক কনটেন্ট) হলো একটি ট্রিপল — (বর্তমান স্টেট, বাকি থাকা ইনপুট, বর্তমান স্ট্যাক কনটেন্ট)। একটি সম্পূর্ণ কম্পিউটেশন হলো এমন কনফিগারেশনের একটি সিকোয়েন্স, প্রতিটি ধাপ $\delta$-এর কোনো একটি বৈধ ট্রানজিশন অনুসরণ করে পরবর্তী কনফিগারেশনে যায়। যেহেতু নন-ডিটারমিনিজম আছে, তাই একটি ইনপুটের জন্য একাধিক সমান্তরাল কনফিগারেশন একই সাথে সম্ভব হতে পারে — একটি স্ট্রিং accept হয় যদি অন্তত একটি কম্পিউটেশন পাথ পুরো ইনপুট consume করে একটি accept স্টেটে পৌঁছায় (L07-এর NFA "সব সম্ভাব্য স্টেট একসাথে ট্র্যাক করা" কৌশলের সাথে সরাসরি সম্পর্কিত, এখন স্ট্যাক-সহ)।

stack: [Z0] শুরু [Z0,X] '0' push X [Z0,X,X] '0' push X [Z0,X] '1' pop X [Z0] '1' pop X accept! stack=[Z0], input শেষ
"0011" ইনপুটে PDA-এর কনফিগারেশন সিকোয়েন্স — প্রতিটি '0' একটি X push করে, প্রতিটি '1' একটি X pop করে; স্ট্যাক ঠিক তখনই [Z0]-এ ফিরে আসে যখন 0-এর সংখ্যা == 1-এর সংখ্যা।

৪ · 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 করে।

Python
# একটি সত্যিকারের 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 (সত্যিকারের একাধিক পথ) আর কখন শুধু কাঠামোগত।
মূল কথা · Key takeaway

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 হয়।

অনুশীলন

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

  2. পরীক্ষা করুন: উপরের কোড সেলে 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 — সব এক জায়গায়।
আগের পাঠ
L21 · গ্রেইবাখ নর্মাল ফর্ম