NFA — ফরমাল ডেফিনিশন
এই পাঠে যা শিখবেন
- 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$ হতে পারে — সেই সিম্বলের জন্য কোনো বৈধ পরবর্তী স্টেট নেই, সেই পাথ সেখানেই "মারা যায়"।
বাকি সবকিছু — Q, Σ, q₀, F — হুবহু L06-এর DFA-এর সংজ্ঞার মতোই।
২ · অ্যাকসেপ্টেন্স রুল — "exists" কোয়ান্টিফায়ার
একটি NFA স্ট্রিং w অ্যাকসেপ্ট করে যদি অন্তত একটি সম্ভাব্য চয়েস-সিকোয়েন্স (নন্ডিটারমিনিস্টিক পাথ) থাকে যা $q_0$ থেকে শুরু করে w পড়ে শেষে F-এর কোনো স্টেটে পৌঁছায় — এমনকি যদি অন্যান্য সম্ভাব্য পাথ ব্যর্থ হয় (কোনো অ্যাকসেপ্ট স্টেটে না পৌঁছায়, বা কোনো ডেড-এন্ডে থেমে যায়), তাও w অ্যাকসেপ্টেড বলে গণ্য হয় — যতক্ষণ কোনো একটি পাথ সফল হয়। এটি সব সম্ভাব্য গণনা-পাথের উপর একটি "exists" (∃) কোয়ান্টিফায়ার — DFA-এর একক, নির্ধারিত পাথের সরাসরি বিপরীত।
৩ · কেন NFA ডিজাইন করা প্রায়ই সহজ
একটি concrete উদাহরণ: ভাষা $L = \{w \in \{0,1\}^* : w\text{-এর কোথাও "01" সাবস্ট্রিং আছে}\}$। একটি NFA দিয়ে এটি স্বাভাবিকভাবেই ডিজাইন করা যায় — মেশিন "গেস" করে কখন সাবস্ট্রিংটি শুরু হতে পারে (নন্ডিটারমিনিস্টিকভাবে হয় বর্তমান স্টেটেই থেকে যায়, অথবা সাবস্ট্রিং-খোঁজার শাখায় চলে যায়) — একটি DFA দিয়ে সমতুল্য ভাষা রিকগনাইজ করা সম্ভব (L08 প্রমাণ করবে সব NFA-এর সমতুল্য DFA থাকে), কিন্তু DFA-কে প্রতিটি স্টেটে "সর্বশেষ কী দেখেছি" এক্সপ্লিসিটভাবে মনে রাখতে হয় — NFA-তে এই বুককিপিং নন্ডিটারমিনিজমের মধ্যেই "লুকানো" থাকে, ফলে ডিজাইন প্রায়ই ছোট ও সহজ হয়। এটি সরাসরি L11-এর regex→NFA কনস্ট্রাকশনের পূর্বাভাস — সেই কনস্ট্রাকশনও স্বাভাবিকভাবে নন্ডিটারমিনিস্টিক।
গঠন হুবহু একই ৫-টাপল, শুধু $\delta$-এর কোডোমেইন আলাদা: DFA-তে $\delta: Q\times\Sigma\to Q$ (ঠিক একটি, সবসময়-সংজ্ঞায়িত পরবর্তী স্টেট), NFA-তে $\delta: Q\times\Sigma\to\mathcal{P}(Q)$ (শূন্য, একটি, বা একাধিক)। অ্যাকসেপ্টেন্সও আলাদা: DFA-এর একটিই পাথ থাকে বলে অ্যাকসেপ্টেন্স মানে "সেই একমাত্র পাথ F-এ শেষ হয়" — NFA-তে একাধিক সম্ভাব্য পাথ থাকতে পারে বলে অ্যাকসেপ্টেন্স মানে "অন্তত একটি পাথ F-এ শেষ হয়"।
৪ · একটি সত্যিকারের NFA সিমুলেটর
নন্ডিটারমিনিজম সিমুলেট করার স্ট্যান্ডার্ড, সঠিক কৌশল হলো আক্ষরিক অর্থে "ব্যাকট্র্যাক" করা নয় — বরং প্রতিটি মুহূর্তে সব সম্ভাব্য বর্তমান স্টেটের একটি সেট একসাথে ট্র্যাক করা। প্রতিটি ইনপুট সিম্বলে, নতুন সেট হলো বর্তমান সেটের প্রতিটি স্টেট থেকে সেই সিম্বলে পৌঁছানো সব স্টেটের ইউনিয়ন — শেষে যদি এই সেট F-এর সাথে ইন্টারসেক্ট করে (কোনো কমন স্টেট থাকে), তাহলে অ্যাকসেপ্ট।
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}")
"01" in s চেক-এর সাথে ক্রস-চেক করে —
সব ক্ষেত্রে "OK" আসার কথা।
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-এর গণনাশক্তি ঠিক সমান — পার্থক্যটা শুধু ডিজাইনের স্বাভাবিকতায়।
অনুশীলন
-
চিন্তা করুন: উপরের NFA-তে $\delta(q_2, 0)$ ও $\delta(q_2, 1)$ উভয়ই $\{q_2\}$ (নিজের কাছে
লুপ)। এর মানে কী — একবার "01" পাওয়া গেলে বাকি ইনপুটে কী প্রভাব পড়ে?
একবার মেশিন q2-এ পৌঁছালে (অর্থাৎ "01" ইতিমধ্যে পাওয়া গেছে), q2-এর সেলফ-লুপ নিশ্চিত করে বাকি যেকোনো ইনপুট সিম্বলেও মেশিন q2-এই থেকে যায় — যেহেতু q2 নিজেই অ্যাকসেপ্ট স্টেট, তাই একবার "01" পাওয়া গেলে বাকি স্ট্রিং যাই হোক না কেন, স্ট্রিংটি অ্যাকসেপ্টেড থাকে — যা সঠিক, কারণ ভাষার সংজ্ঞা "কোথাও 01 আছে", পুরো স্ট্রিং 01 দিয়ে শেষ হতে হবে এমন নয়।
-
পরীক্ষা করুন: কোড সেলে
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-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ — NFA থেকে DFA ইকুইভ্যালেন্স L08 এই পাঠের NFA-কে সাবসেট কনস্ট্রাকশন দিয়ে একটি সমতুল্য DFA-তে রূপান্তর করে দেখানো হবে — এবং ফলাফল কোড দিয়ে যাচাই করা হবে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ DFA থেকে টুরিং মেশিন পর্যন্ত ধাপে ধাপে বাড়তে থাকা গণনাশক্তির সম্পূর্ণ মানচিত্র।
- Discrete Mathematics কোর্স সহোদর কোর্স পাওয়ার সেট $\mathcal{P}(Q)$-এর সংজ্ঞা ও সেট থিওরির ভিত্তি সেই কোর্সে বিস্তারিত আছে।
- Programming Languages & Compiler Design কোর্স সঙ্গী কোর্স NFA ব্যবহার করে বাস্তবে লেক্সার বানানোর প্র্যাকটিক্যাল দিক সেই কোর্সে দেখুন — এই পাঠ তার গাণিতিক ভিত্তি দেয়।