পাঠ ০৭ · ৫৬-এর মধ্যে · মডিউল ২

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

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

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

  • NFA-এর ফরমাল ৫-টাপল সংজ্ঞা এবং L06-এর DFA থেকে এর নির্ভুল পার্থক্য
  • নন্ডিটারমিনিজমের অর্থ এবং "কোনো একটি পাথ সফল হলেই যথেষ্ট" — অ্যাকসেপ্টেন্স রুল
  • কেন NFA ডিজাইন করা প্রায়ই সহজ — একটি concrete উদাহরণসহ
  • ব্যাকট্র্যাকিং ছাড়া নন্ডিটারমিনিজম সিমুলেট করার স্ট্যান্ডার্ড কৌশল — সব সম্ভাব্য স্টেটের একটি সেট একসাথে ট্র্যাক করা

১ · NFA-এর ফরমাল সংজ্ঞা

NFANondeterministic Finite Automatonএকটি ৫-টাপল অটোমাটা যেখানে ট্রানজিশন ফাংশন প্রতিটি স্টেট/সিম্বলের জন্য একটি সেট অফ স্টেট রিটার্ন করে। ও DFA-এর গঠন হুবহু একই: একটি ৫-টাপল $M = (Q, \Sigma, \delta, q_0, F)$, যেখানে Q ফাইনাইট স্টেটের সেট, Σ ইনপুট আলফাবেট, $q_0 \in Q$ স্টার্ট স্টেট, এবং $F \subseteq Q$ অ্যাকসেপ্ট স্টেটের সেট — L06-এর সাথে হুবহু মিলে যায়। একমাত্র পার্থক্য, কিন্তু একটি গুরুত্বপূর্ণ পার্থক্য, ট্রানজিশন ফাংশনে:

$$\delta: Q \times \Sigma \to \mathcal{P}(Q)$$

অর্থাৎ $\delta(q, a)$ একটি একক স্টেট রিটার্ন করে না — এটি Q-এর একটি সাবসেট রিটার্ন করে (Q-এর পাওয়ার সেট $\mathcal{P}(Q)$-এর একটি সদস্য) — এই সাবসেটটি খালিও হতে পারে (কোনো বৈধ পরবর্তী স্টেট নেই), অথবা একাধিক স্টেট থাকতে পারে (একাধিক সম্ভাব্য পরবর্তী স্টেট)। DFA-তে $\delta$ ছিল একটি টোটাল ফাংশন যা প্রতিটি জোড়ার জন্য ঠিক একটি স্টেট দেয় — এখানে সেই কড়াকড়ি নেই।

একাধিক পরবর্তী স্টেট
$\delta(q,a)$ একাধিক স্টেট রিটার্ন করতে পারে — মেশিন যেন সব সম্ভাবনা একসাথে "চেষ্টা" করছে।
শূন্য পরবর্তী স্টেট
$\delta(q,a) = \emptyset$ হতে পারে — সেই সিম্বলের জন্য কোনো বৈধ পরবর্তী স্টেট নেই, সেই পাথ সেখানেই "মারা যায়"।
DFA-এর সাথে মিল
বাকি সবকিছু — Q, Σ, q₀, F — হুবহু L06-এর DFA-এর সংজ্ঞার মতোই।

২ · অ্যাকসেপ্টেন্স রুল — "exists" কোয়ান্টিফায়ার

একটি NFA স্ট্রিং w অ্যাকসেপ্ট করে যদি অন্তত একটি সম্ভাব্য চয়েস-সিকোয়েন্স (নন্ডিটারমিনিস্টিক পাথ) থাকে যা $q_0$ থেকে শুরু করে w পড়ে শেষে F-এর কোনো স্টেটে পৌঁছায় — এমনকি যদি অন্যান্য সম্ভাব্য পাথ ব্যর্থ হয় (কোনো অ্যাকসেপ্ট স্টেটে না পৌঁছায়, বা কোনো ডেড-এন্ডে থেমে যায়), তাও w অ্যাকসেপ্টেড বলে গণ্য হয় — যতক্ষণ কোনো একটি পাথ সফল হয়। এটি সব সম্ভাব্য গণনা-পাথের উপর একটি "exists" (∃) কোয়ান্টিফায়ার — DFA-এর একক, নির্ধারিত পাথের সরাসরি বিপরীত।

q0 (start) '0'→q0 '0'→q1 q0 q1 '1'→q0 '1'→q2 q0 · reject q2 · ACCEPT ✓
ইনপুট "01"-এ NFA-এর সম্ভাব্য শাখা-প্রশাখা — একটি পাথ q2 (অ্যাকসেপ্ট স্টেট)-এ পৌঁছায়, তাই সম্পূর্ণ স্ট্রিং "01" অ্যাকসেপ্টেড, অন্য পাথ ব্যর্থ হলেও সমস্যা নেই।
লক্ষ্য করুন উপরের ডায়াগ্রামে q1 থেকে '0' পড়ার সময় কোনো শাখা আঁকা হয়নি — কারণ আমাদের উদাহরণ NFA-তে $\delta(q_1, 0)$ শুধু $\{q_1\}$ (নিজের কাছেই থাকে), যা এই নির্দিষ্ট ইনপুট "01"-এর ট্রেসে ব্যবহৃত হয়নি। এটিই দেখায় $\delta(q,a) = \emptyset$ হলে সেই পাথ কীভাবে সেখানেই থেমে যায় — কোনো এরর নয়, শুধু একটি ব্যর্থ শাখা।

৩ · কেন NFA ডিজাইন করা প্রায়ই সহজ

একটি concrete উদাহরণ: ভাষা $L = \{w \in \{0,1\}^* : w\text{-এর কোথাও "01" সাবস্ট্রিং আছে}\}$। একটি NFA দিয়ে এটি স্বাভাবিকভাবেই ডিজাইন করা যায় — মেশিন "গেস" করে কখন সাবস্ট্রিংটি শুরু হতে পারে (নন্ডিটারমিনিস্টিকভাবে হয় বর্তমান স্টেটেই থেকে যায়, অথবা সাবস্ট্রিং-খোঁজার শাখায় চলে যায়) — একটি DFA দিয়ে সমতুল্য ভাষা রিকগনাইজ করা সম্ভব (L08 প্রমাণ করবে সব NFA-এর সমতুল্য DFA থাকে), কিন্তু DFA-কে প্রতিটি স্টেটে "সর্বশেষ কী দেখেছি" এক্সপ্লিসিটভাবে মনে রাখতে হয় — NFA-তে এই বুককিপিং নন্ডিটারমিনিজমের মধ্যেই "লুকানো" থাকে, ফলে ডিজাইন প্রায়ই ছোট ও সহজ হয়। এটি সরাসরি L11-এর regex→NFA কনস্ট্রাকশনের পূর্বাভাস — সেই কনস্ট্রাকশনও স্বাভাবিকভাবে নন্ডিটারমিনিস্টিক।

NFA vs DFA — সংক্ষেপে

গঠন হুবহু একই ৫-টাপল, শুধু $\delta$-এর কোডোমেইন আলাদা: DFA-তে $\delta: Q\times\Sigma\to Q$ (ঠিক একটি, সবসময়-সংজ্ঞায়িত পরবর্তী স্টেট), NFA-তে $\delta: Q\times\Sigma\to\mathcal{P}(Q)$ (শূন্য, একটি, বা একাধিক)। অ্যাকসেপ্টেন্সও আলাদা: DFA-এর একটিই পাথ থাকে বলে অ্যাকসেপ্টেন্স মানে "সেই একমাত্র পাথ F-এ শেষ হয়" — NFA-তে একাধিক সম্ভাব্য পাথ থাকতে পারে বলে অ্যাকসেপ্টেন্স মানে "অন্তত একটি পাথ F-এ শেষ হয়"।

৪ · একটি সত্যিকারের NFA সিমুলেটর

নন্ডিটারমিনিজম সিমুলেট করার স্ট্যান্ডার্ড, সঠিক কৌশল হলো আক্ষরিক অর্থে "ব্যাকট্র্যাক" করা নয় — বরং প্রতিটি মুহূর্তে সব সম্ভাব্য বর্তমান স্টেটের একটি সেট একসাথে ট্র্যাক করা। প্রতিটি ইনপুট সিম্বলে, নতুন সেট হলো বর্তমান সেটের প্রতিটি স্টেট থেকে সেই সিম্বলে পৌঁছানো সব স্টেটের ইউনিয়ন — শেষে যদি এই সেট F-এর সাথে ইন্টারসেক্ট করে (কোনো কমন স্টেট থাকে), তাহলে অ্যাকসেপ্ট।

Python
class NFA:
    def __init__(self, states, alphabet, transition, start, accept_states):
        self.states = states
        self.alphabet = alphabet
        self.transition = transition   # dict: (state, symbol) -> set(states)
        self.start = start
        self.accept_states = accept_states

    def accepts(self, string):
        current = {self.start}
        for ch in string:
            nxt = set()
            for q in current:
                nxt |= self.transition.get((q, ch), set())
            current = nxt
            if not current:
                break
        return bool(current & self.accept_states)


states = {"q0", "q1", "q2"}
alphabet = {"0", "1"}
transition = {
    ("q0", "0"): {"q0", "q1"},
    ("q0", "1"): {"q0"},
    ("q1", "0"): {"q1"},
    ("q1", "1"): {"q2"},
    ("q2", "0"): {"q2"},
    ("q2", "1"): {"q2"},
}
nfa = NFA(states, alphabet, transition, "q0", {"q2"})

test_strings = ["", "0", "1", "01", "10", "110", "1010", "000", "0011", "101"]
for s in test_strings:
    contains01 = "01" in s
    result = nfa.accepts(s)
    status = "OK" if result == contains01 else "MISMATCH"
    print(f"{s!r:8s} contains01={contains01!s:5s} nfa={result!s:5s} {status}")

    
লক্ষ্য করুন $q_1$-এর স্টেট $q_0$-কে "সাবসিউম" করে — q1-এ থাকা মানে "সাম্প্রতিক '0' দেখেছি" — তাই q1-এ '0' পড়লে শুধু q1-এই থাকি (q0-এ ফিরি না), কারণ q1 নিজেই q0-এর চেয়ে বেশি তথ্য রাখে। কোড সেলটি এই ১০টি টেস্ট স্ট্রিং-এর প্রতিটিতে NFA-এর ফলাফল Python-এর নিজস্ব "01" in s চেক-এর সাথে ক্রস-চেক করে — সব ক্ষেত্রে "OK" আসার কথা।
মূল কথা · Key takeaway

NFA একই ৫-টাপল কাঠামো ব্যবহার করে, কিন্তু $\delta$ একাধিক (বা শূন্য) পরবর্তী স্টেট রিটার্ন করতে দেয়, এবং অ্যাকসেপ্টেন্স মানে "অন্তত একটি পাথ সফল হওয়া"। এটি ডিজাইন সহজ করে তোলে, এবং L08-এ আমরা প্রমাণ করব এই সহজবোধ্যতা কোনো অতিরিক্ত গণনাশক্তি আনে না — প্রতিটি NFA-এর একটি সমতুল্য DFA থাকে।

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

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

প্র ০১ NFA-এর $\delta$ কেন $\mathcal{P}(Q)$-তে ম্যাপ করে, DFA-এর মতো সরাসরি Q-তে নয়?

কারণ একটি স্টেট/সিম্বল জোড়ার জন্য একাধিক সম্ভাব্য পরবর্তী স্টেট থাকতে পারে (অথবা কোনোটিই না) — একটি একক স্টেট রিটার্ন করার মতো ফাংশন এই সম্ভাবনাগুলো প্রকাশ করতে পারবে না। $\mathcal{P}(Q)$ (Q-এর পাওয়ার সেট) হলো ঠিক সেই সেট যাতে "স্টেটদের যেকোনো সাবসেট" প্রকাশ করা যায় — খালি সাবসেট থেকে পুরো Q পর্যন্ত — তাই এটিই সঠিক কোডোমেইন।

প্র ০২ যদি একটি ইনপুটের জন্য ১০টি সম্ভাব্য পাথের মধ্যে ৯টি reject করে, ১টি accept করে — NFA কী সিদ্ধান্ত নেয়?

Accept। NFA-এর অ্যাকসেপ্টেন্স নিয়ম হলো "exists" — অন্তত একটি পাথ সফল হলেই যথেষ্ট, বাকি পাথ কী করল তা গুরুত্বপূর্ণ নয়। এটিই উপরের কোড সেলের সেট-ট্র্যাকিং পদ্ধতিতে ধরা পড়ে — শেষ সেটে যদি অ্যাকসেপ্ট স্টেট থাকে, তাহলে সেই স্টেটে পৌঁছানো অন্তত একটি পাথ ছিল, তাই স্ট্রিং accepted।

প্র ০৩ "01 সাবস্ট্রিং" ভাষাটি কি DFA দিয়েও রিকগনাইজ করা সম্ভব?

হ্যাঁ, সম্পূর্ণভাবে সম্ভব — একটি DFA-ও এই ভাষা রিকগনাইজ করতে পারে (একটি স্টেট "এখনও 01 দেখিনি", একটি স্টেট "সবেমাত্র 0 দেখেছি", একটি স্টেট "01 দেখে ফেলেছি, বাকি সব ইগনোর")। পার্থক্যটা এক্সপ্রেসিভ পাওয়ারে নয় — L08 প্রমাণ করবে NFA ও DFA-এর গণনাশক্তি ঠিক সমান — পার্থক্যটা শুধু ডিজাইনের স্বাভাবিকতায়।

অনুশীলন

  1. চিন্তা করুন: উপরের NFA-তে $\delta(q_2, 0)$ ও $\delta(q_2, 1)$ উভয়ই $\{q_2\}$ (নিজের কাছে লুপ)। এর মানে কী — একবার "01" পাওয়া গেলে বাকি ইনপুটে কী প্রভাব পড়ে?

    একবার মেশিন q2-এ পৌঁছালে (অর্থাৎ "01" ইতিমধ্যে পাওয়া গেছে), q2-এর সেলফ-লুপ নিশ্চিত করে বাকি যেকোনো ইনপুট সিম্বলেও মেশিন q2-এই থেকে যায় — যেহেতু q2 নিজেই অ্যাকসেপ্ট স্টেট, তাই একবার "01" পাওয়া গেলে বাকি স্ট্রিং যাই হোক না কেন, স্ট্রিংটি অ্যাকসেপ্টেড থাকে — যা সঠিক, কারণ ভাষার সংজ্ঞা "কোথাও 01 আছে", পুরো স্ট্রিং 01 দিয়ে শেষ হতে হবে এমন নয়।

  2. পরীক্ষা করুন: কোড সেলে test_strings-এ নিজের একটি স্ট্রিং (যেমন "00110") যোগ করে Run চেপে দেখুন NFA-এর ফলাফল আপনার হাতে-গোনা "01 আছে কি না"-এর সাথে মেলে কি না।

    "00110"-এ "01" সাবস্ট্রিং আছে (ইনডেক্স ১-২ তে "01")। ট্রেস: q0 →('0')→ {q0,q1} →('0')→ {q0,q1} →('1')→ {q0,q2} →('1')→ {q0,q2} →('0')→ {q0,q1,q2} — শেষ সেটে q2 আছে, তাই accept — প্রত্যাশিত ফলাফলের সাথেই মিলে যায়।

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

আগের পাঠ
DFA — ফরমাল ডেফিনিশন ও ল্যাঙ্গুয়েজ অ্যাকসেপ্টেন্স