এপসিলন-NFA ও এপসিলন-ক্লোজার
এই পাঠে যা শিখবেন
- ε-NFA-এর ফরমাল সংজ্ঞা এবং ε-ট্রানজিশন কীভাবে কাজ করে
- এপসিলন-ক্লোজারের ফিক্সড-পয়েন্ট গণনা — ধাপে ধাপে হাতে-ট্রেসসহ
- ε-ট্রানজিশন সরিয়ে সমতুল্য প্লেইন NFA বানানোর সূত্র
- একটি সত্যিকারের ইমপ্লিমেন্টেশন যা দুটি সাব-মেশিন জোড়া লাগিয়ে রূপান্তরের সঠিকতা যাচাই করে
১ · ε-NFA-এর ফরমাল সংজ্ঞা
একটি এপসিলন-NFAEpsilon-NFAε-ট্রানজিশনসহ একটি NFA — ইনপুট সিম্বল না পড়েই স্টেট বদলানো যায়। L07-এর NFA-এর একটি সম্প্রসারণ — ট্রানজিশন ফাংশন এখন ইনপুট আলফাবেটের পাশাপাশি একটি বিশেষ "ε" (এপসিলন) সিম্বলও গ্রহণ করে:
$$\delta: Q \times (\Sigma \cup \{\varepsilon\}) \to \mathcal{P}(Q)$$
$\delta(q, \varepsilon)$ বলে দেয় q থেকে কোনো ইনপুট সিম্বল না পড়েই কোন কোন স্টেটে যাওয়া যায় — এই "ফ্রি" মুভ মেশিনকে যেকোনো সময় নিতে পারে, ইনপুট পয়েন্টার একই জায়গায় থেকে যায়। এটি বিশেষভাবে কার্যকর যখন ছোট ছোট অটোমাটাকে একসাথে জোড়া লাগাতে হয় — যেমন দুটি ভাষার ইউনিয়নের জন্য একটি নতুন কমন স্টার্ট স্টেট থেকে উভয় সাব-মেশিনে ε-ট্রানজিশন দেওয়া (সরাসরি L11-এর Kleene's theorem কনস্ট্রাকশনের পূর্বাভাস)।
২ · এপসিলন-ক্লোজার
$\text{ECLOSE}(q)$ হলো q থেকে শুধু ε-ট্রানজিশন ব্যবহার করে (শূন্য বা একাধিকবার) পৌঁছানো
যায় এমন সব স্টেটের সেট — একটি স্ট্যান্ডার্ড গ্রাফ-রিচেবিলিটি গণনা (../dsa/-এর
BFS/DFS-ধাঁচের একই ধারণা): $\{q\}$ থেকে শুরু করে, বর্তমান সেট থেকে আরও একটি ε-ট্রানজিশনে পৌঁছানো যায় এমন
যেকোনো স্টেট যোগ করা হয়, যতক্ষণ না নতুন কোনো স্টেট যোগ হচ্ছে (ফিক্সড পয়েন্ট)।
৩ · ε-ট্রানজিশন সরানো — সমতুল্য প্লেইন NFA
L08-এর সাবসেট কনস্ট্রাকশনের সরাসরি সম্প্রসারণ হিসেবে, একটি ε-NFA-কে একটি সমতুল্য প্লেইন NFA-তে রূপান্তর করা যায় (তারপর L08 প্রয়োগ করে DFA-তে) — প্রতিটি "আসল" ট্রানজিশনের আগে ও পরে ε-ক্লোজার নিয়ে:
$$\delta'(q, a) = \text{ECLOSE}\Big(\bigcup_{p \in \text{ECLOSE}(q)} \delta(p, a)\Big)$$
নতুন অ্যাকসেপ্ট স্টেট সেট: $q$ অ্যাকসেপ্টিং হয় যদি $\text{ECLOSE}(q)$-এর মধ্যে মূল ε-NFA-এর কোনো অ্যাকসেপ্ট স্টেট থাকে (যেহেতু q থেকে বিনামূল্যে সেখানে পৌঁছানো যায়)।
৪ · কোড: এপসিলন-ক্লোজার, রূপান্তর, ও যাচাই
নিচের কোডে $L=\{"a","b"\}$ রিকগনাইজ করার একটি ε-NFA বানানো হয়েছে (উপরের ডায়াগ্রাম অনুযায়ী), এপসিলন-ক্লোজার কয়েকটি স্টেটের জন্য হাতে-যাচাইযোগ্য assert দিয়ে নিশ্চিত করা হয়েছে, তারপর ε-ট্রানজিশন সরিয়ে একটি প্লেইন NFA বানানো হয়েছে, এবং শেষে — দৈর্ঘ্য ০ থেকে ৩ পর্যন্ত সব স্ট্রিং-এ — মূল ε-NFA ও রূপান্তরিত NFA উভয়ের ফলাফল প্রত্যাশিত ভাষার সাথে মিলিয়ে assert করা হয়েছে।
from itertools import product
EPSILON = None # sentinel representing an epsilon-transition
class EpsilonNFA:
def __init__(self, states, alphabet, transition, start, accept_states):
self.states = states
self.alphabet = alphabet
self.transition = transition # dict: (state, symbol_or_EPSILON) -> set(states)
self.start = start
self.accept_states = accept_states
def accepts(self, string):
current = epsilon_closure(self, self.start)
for ch in string:
raw = set()
for q in current:
raw |= self.transition.get((q, ch), set())
nxt = set()
for s in raw:
nxt |= epsilon_closure(self, s)
current = nxt
if not current:
break
return bool(current & self.accept_states)
def epsilon_closure(enfa, state):
closure = {state}
frontier = [state]
while frontier:
q = frontier.pop()
for nxt in enfa.transition.get((q, EPSILON), set()):
if nxt not in closure:
closure.add(nxt)
frontier.append(nxt)
return frozenset(closure)
class NFA:
def __init__(self, states, alphabet, transition, start, accept_states):
self.states = states
self.alphabet = alphabet
self.transition = transition
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)
def remove_epsilon_transitions(enfa):
new_transition = {}
for q in enfa.states:
closure_q = epsilon_closure(enfa, q)
for a in enfa.alphabet:
raw = set()
for p in closure_q:
raw |= enfa.transition.get((p, a), set())
dest = set()
for r in raw:
dest |= epsilon_closure(enfa, r)
if dest:
new_transition[(q, a)] = dest
new_accept = {q for q in enfa.states if epsilon_closure(enfa, q) & enfa.accept_states}
return NFA(enfa.states, enfa.alphabet, new_transition, enfa.start, new_accept)
# sub-machine A: accepts exactly "a" (s0 --a--> s1)
# sub-machine B: accepts exactly "b" (t0 --b--> t1)
# joined at a new start q0 via epsilon-transitions to s0 and t0
states = {"q0", "s0", "s1", "t0", "t1"}
alphabet = {"a", "b"}
transition = {
("q0", EPSILON): {"s0", "t0"},
("s0", "a"): {"s1"},
("t0", "b"): {"t1"},
}
enfa = EpsilonNFA(states, alphabet, transition, "q0", {"s1", "t1"})
# hand-verify epsilon-closures
print("ECLOSE(q0) =", sorted(epsilon_closure(enfa, "q0")))
print("ECLOSE(s0) =", sorted(epsilon_closure(enfa, "s0")))
print("ECLOSE(s1) =", sorted(epsilon_closure(enfa, "s1")))
assert epsilon_closure(enfa, "q0") == frozenset({"q0", "s0", "t0"})
assert epsilon_closure(enfa, "s0") == frozenset({"s0"})
plain_nfa = remove_epsilon_transitions(enfa)
print("plain NFA transitions:")
for k, v in sorted(plain_nfa.transition.items(), key=lambda kv: (kv[0][0], kv[0][1] or "")):
print(" ", k, "->", sorted(v))
print("plain NFA accept states:", sorted(plain_nfa.accept_states))
mismatches = 0
total = 0
for length in range(0, 4):
for combo in product("ab", repeat=length):
s = "".join(combo)
total += 1
r1 = enfa.accepts(s)
r2 = plain_nfa.accepts(s)
expected = s in ("a", "b")
if not (r1 == r2 == expected):
mismatches += 1
print("MISMATCH", repr(s), r1, r2, expected)
print(f"total={total} mismatches={mismatches}")
assert mismatches == 0
print("epsilon-NFA ও epsilon-transition-remove করা NFA উভয়ই অভিন্ন ফলাফল দেয়।")
ε-ট্রানজিশন ছোট অটোমাটা জোড়া লাগানোকে স্বাভাবিক করে তোলে, এবং এপসিলন-ক্লোজার + L08-এর সাবসেট কনস্ট্রাকশনের সংমিশ্রণে প্রতিটি ε-NFA-কে একটি সমতুল্য DFA-তে রূপান্তর করা যায় — অর্থাৎ ε-ট্রানজিশন যোগ করেও কোনো নতুন গণনাশক্তি আসে না, শুধু বর্ণনার (এবং কনস্ট্রাকশনের) সুবিধা বাড়ে। L11-এ ঠিক এই "জোড়া লাগানোর" কৌশল ব্যবহার করে regex থেকে সরাসরি NFA বানানো হবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ এপসিলন-ক্লোজার সবসময় নিজের স্টেট q-কে অন্তর্ভুক্ত করে কেন?
কারণ q থেকে "শূন্যটি" ε-ট্রানজিশন নিয়ে (অর্থাৎ কিছুই না করে) q-তেই থাকা যায় — এবং সংজ্ঞা অনুযায়ী
ECLOSE(q) হলো "শূন্য বা তার বেশি" ε-ট্রানজিশন দিয়ে পৌঁছানো যায় এমন স্টেট। "শূন্য" কেসটি সবসময় সত্য,
তাই q নিজেই সবসময় ECLOSE(q)-এর সদস্য — কোডে এটিই প্রতিফলিত হয়েছে closure = {state} দিয়ে
শুরু করার মাধ্যমে।
প্র ০২ $\delta'(q,a)$-এর সূত্রে ECLOSE কেন দুইবার প্রয়োগ করা হয় — একবার আগে, একবার পরে?
প্রথমটি (আগে) নিশ্চিত করে q থেকে ε-ট্রানজিশন দিয়ে পৌঁছানো যায় এমন সব স্টেট থেকেও 'a' পড়ার সুযোগ বিবেচনা করা হচ্ছে (q নিজে থেকে 'a' না গেলেও, q-এর ε-প্রতিবেশী কেউ যেতে পারে)। দ্বিতীয়টি (পরে) নিশ্চিত করে 'a' পড়ার পর যে স্টেটে পৌঁছানো গেল, সেখান থেকেও যদি আরও ε-ট্রানজিশন সম্ভব হয়, সেগুলোও অন্তর্ভুক্ত হয়। দুটো বাদ দিলে কিছু বৈধ ট্রানজিশন মিস হয়ে যেত।
প্র ০৩ যদি কোনো ε-ট্রানজিশন একটি অ্যাকসেপ্ট স্টেটের দিকে যায়, নতুন প্লেইন NFA-এর অ্যাকসেপ্ট স্টেট সেটে এর প্রভাব কী?
তাহলে সেই ε-ট্রানজিশনের উৎস স্টেট q-ও নতুন NFA-তে অ্যাকসেপ্টিং হয়ে যাবে — কারণ
new_accept = {q : ECLOSE(q) ∩ accept_states ≠ ∅} শর্তটি q-এর ε-ক্লোজারে অ্যাকসেপ্ট স্টেট
থাকলেই q-কে অ্যাকসেপ্টিং ধরে। এটি যৌক্তিক: q-তে থেকেই (বিনামূল্যে ε-ট্রানজিশন নিয়ে) একটি অ্যাকসেপ্ট
স্টেটে পৌঁছানো সম্ভব, তাই q থেকে ইনপুট শেষ হলেও স্ট্রিং অ্যাকসেপ্টেড হওয়া উচিত।
অনুশীলন
-
চিন্তা করুন: কোড সেলের ε-NFA-তে $\text{ECLOSE}(t_1)$ কী হবে? হাতে ট্রেস করে verify করুন
কোড আউটপুটের সাথে মেলে কি না।
$\text{ECLOSE}(t_1) = \{t_1\}$ — কারণ t1 থেকে কোনো ε-ট্রানজিশন সংজ্ঞায়িত নেই (transition ডিকশনারিতে
(t1, EPSILON)কী নেই), তাই t1-এর ε-ক্লোজারে শুধু t1 নিজেই থাকে, ঠিক s0/s1-এর মতোই। -
পরীক্ষা করুন: কোড সেলে
for length in range(0, 4):লাইনে max length বাড়িয়েrange(0, 5)করে Run চাপুন — mismatches এখনও ০ থাকে কি না, এবং কোন নতুন স্ট্রিংগুলো টেস্ট হলো দেখুন।mismatches এখনও ০ থাকবে। নতুন যোগ হওয়া দৈর্ঘ্য-৪ স্ট্রিংগুলো (যেমন "aabb", "baba" ইত্যাদি) কোনোটিই "a" বা "b" নয়, তাই উভয় মেশিনই সেগুলো reject করবে — যা প্রত্যাশিত
expected = s in ("a","b")-এর সাথে মিলে যায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ — রেগুলার এক্সপ্রেশন ফরমাল ডেফিনিশন L10 regex-এর রিকার্সিভ সংজ্ঞা — L11-এ ঠিক এই পাঠের ε-জোড়া-লাগানো কৌশল ব্যবহার করে regex থেকে NFA বানানো হবে।
- আগের পাঠ — সাবসেট কনস্ট্রাকশন L08 এই পাঠের remove_epsilon_transitions-এর ফলাফলকে DFA-তে রূপান্তর করতে হলে সরাসরি L08-এর সাবসেট কনস্ট্রাকশন প্রয়োগ করা যায়।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ DFA থেকে টুরিং মেশিন পর্যন্ত ধাপে ধাপে বাড়তে থাকা গণনাশক্তির সম্পূর্ণ মানচিত্র।
- Data Structures & Algorithms কোর্স সম্পর্কিত কোর্স এপসিলন-ক্লোজারের ফিক্সড-পয়েন্ট/BFS রিচেবিলিটি গণনা সেই কোর্সের গ্রাফ ট্রাভার্সাল অ্যালগরিদমের একই মূল ধারণা।