NFA থেকে DFA ইকুইভ্যালেন্স — সাবসেট কনস্ট্রাকশন
এই পাঠে যা শিখবেন
- NFA=DFA ইকুইভ্যালেন্স থিওরেমের নির্ভুল বিবৃতি এবং এর গুরুত্ব
- সাবসেট কনস্ট্রাকশনের ফরমাল সংজ্ঞা — $Q_D$, $\delta_D$, $F_D$ কীভাবে গঠিত হয়
- সঠিকতার প্রমাণের কেন্দ্রীয় ইনভ্যারিয়েন্ট — কেন এই কনস্ট্রাকশন কাজ করে
- একটি সত্যিকারের ইমপ্লিমেন্টেশন এবং ব্যাচ-টেস্টিং দিয়ে থিওরেমের একটি concrete, code-verified নিশ্চিতকরণ
১ · থিওরেম — প্রতিটি NFA-এর একটি সমতুল্য DFA আছে
L07-এ আমরা দেখেছি NFA ডিজাইন করা প্রায়ই সহজ। এখন প্রশ্ন: এই সহজবোধ্যতা কি অতিরিক্ত গণনাশক্তি নিয়ে আসে? উত্তর: না। থিওরেম: প্রতিটি NFA $N$-এর জন্য একটি DFA $D$ আছে যেন $L(D) = L(N)$ (হুবহু একই ভাষা অ্যাকসেপ্ট করে) — অর্থাৎ নন্ডিটারমিনিজম শুধু বর্ণনার সুবিধা দেয়, কোনো নতুন রিকগনাইজিং ক্ষমতা নয়। এই থিওরেমের কনস্ট্রাকটিভ প্রমাণকে বলা হয় সাবসেট কনস্ট্রাকশন (বা পাওয়ারসেট কনস্ট্রাকশন)।
একটি টেকনিক্যাল নোট: এই একই অ্যালগরিদম ../programming-languages-compilers/-এর M4/L18-এ
লেক্সার বানানোর প্র্যাকটিক্যাল কাজে ব্যবহৃত হয়েছিল — এখানে আমাদের লক্ষ্য লেক্সার বানানো নয়, বরং
অ্যালগরিদমটির সঠিকতা প্রমাণ করা।
২ · সাবসেট কনস্ট্রাকশনের ফরমাল সংজ্ঞা
NFA $N = (Q_N, \Sigma, \delta_N, q_0, F_N)$ দেওয়া থাকলে, DFA $D = (Q_D, \Sigma, \delta_D, \{q_0\}, F_D)$ নিম্নরূপে গঠন করা হয়:
- $$Q_D = \mathcal{P}(Q_N)$$ — প্রতিটি DFA স্টেট হলো NFA স্টেটদের একটি সেট (তাত্ত্বিকভাবে $\mathcal{P}(Q_N)$-এর সব সদস্য সম্ভাব্য, বাস্তবে শুধু স্টার্ট থেকে পৌঁছানো যায় এমন সাবসেটগুলোই তৈরি হয় — নিচের কোডে এটি BFS দিয়ে করা হয়েছে)।
- $$\delta_D(S, a) = \bigcup_{q \in S} \delta_N(q, a)$$ — বর্তমান সাবসেট $S$-এর প্রতিটি স্টেট থেকে $a$ দিয়ে পৌঁছানো যায় এমন সব NFA-স্টেটের ইউনিয়ন।
- $$F_D = \{S \in Q_D : S \cap F_N \neq \emptyset\}$$ — একটি DFA স্টেট (একটি সাবসেট) অ্যাকসেপ্টিং হয় যদি তাতে অন্তত একটি NFA অ্যাকসেপ্ট স্টেট থাকে।
৩ · সঠিকতার প্রমাণ — কেন্দ্রীয় ইনভ্যারিয়েন্ট
এই কনস্ট্রাকশন সঠিক তা প্রমাণ করতে (L03-এর স্ট্রাকচারাল ইনডাকশনের মাধ্যমে, স্ট্রিং-এর দৈর্ঘ্যের উপর), আমরা একটি মূল ইনভ্যারিয়েন্ট প্রতিষ্ঠা করি:
স্ট্রিং $w$ প্রসেস করার পর, কনস্ট্রাক্ট করা DFA-এর বর্তমান স্টেট (একটি সেট $S$) ঠিক তাই — সেই সব NFA-স্টেটের সেট যেখানে NFA $q_0$ থেকে $w$ পড়ে পৌঁছাতে পারে (সব সম্ভাব্য নন্ডিটারমিনিস্টিক পাথ মিলিয়ে)।
এই ইনভ্যারিয়েন্ট একবার প্রতিষ্ঠিত হলে, সরাসরি ফলাফল দেয়: DFA স্ট্রিং $w$ অ্যাকসেপ্ট করে ঠিক তখনই যখন $S \cap F_N \neq \emptyset$ — যার মানে "NFA-এর কোনো একটি পাথ $w$ পড়ে একটি অ্যাকসেপ্ট স্টেটে পৌঁছেছে" — যা হুবহু L07-এর NFA অ্যাকসেপ্টেন্স-এর সংজ্ঞা। তাই $L(D) = L(N)$।
৪ · কোড: সাবসেট কনস্ট্রাকশন ও থিওরেমের ব্যাচ-টেস্ট যাচাই
নিচে L07-এর "01 সাবস্ট্রিং" NFA-এর উপর সাবসেট কনস্ট্রাকশন চালানো হয়েছে — একটি BFS বর্তমান সাবসেট থেকে শুরু করে ধাপে ধাপে সব পৌঁছানো-যায়-এমন সাবসেট আবিষ্কার করে (তাত্ত্বিক $2^3=8$টি সম্ভাব্য সাবসেটের বদলে বাস্তবে মাত্র কয়েকটি রিচেবল হয়)। এরপর — শুধু অ্যালগরিদম চালানো নয়, থিওরেমের একটি সত্যিকারের সঠিকতা-যাচাই হিসেবে — দৈর্ঘ্য ০ থেকে ৬ পর্যন্ত সব সম্ভাব্য বাইনারি স্ট্রিং (মোট ১২৭টি) NFA ও নতুন DFA উভয়ের বিরুদ্ধে টেস্ট করে assert করা হয়েছে যে প্রতিটিতে ফলাফল হুবহু মেলে।
from collections import deque
from itertools import product
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)
class DFA:
def __init__(self, states, alphabet, transition, start, accept_states):
self.states = states
self.alphabet = alphabet
self.transition = transition # dict: (state, symbol) -> state
self.start = start
self.accept_states = accept_states
def accepts(self, string):
state = self.start
for ch in string:
state = self.transition[(state, ch)]
return state in self.accept_states
def subset_construction(nfa):
start_set = frozenset({nfa.start})
dfa_states = {start_set}
dfa_transition = {}
queue = deque([start_set])
while queue:
current = queue.popleft()
for a in nfa.alphabet:
nxt = frozenset().union(*(nfa.transition.get((q, a), set()) for q in current)) if current else frozenset()
dfa_transition[(current, a)] = nxt
if nxt not in dfa_states:
dfa_states.add(nxt)
queue.append(nxt)
accept_states = {S for S in dfa_states if S & nfa.accept_states}
return DFA(dfa_states, nfa.alphabet, dfa_transition, start_set, 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"})
dfa = subset_construction(nfa)
print(f"NFA states: {len(nfa.states)} | subset-construction DFA states: {len(dfa.states)}")
mismatches = 0
total = 0
for length in range(0, 7):
for combo in product("01", repeat=length):
s = "".join(combo)
total += 1
n_res = nfa.accepts(s)
d_res = dfa.accepts(s)
if n_res != d_res:
mismatches += 1
print("MISMATCH on", repr(s), n_res, d_res)
print(f"মোট টেস্ট স্ট্রিং: {total}, mismatch: {mismatches}")
assert mismatches == 0
print("সব স্ট্রিং-এ NFA ও DFA-এর ফলাফল অভিন্ন -- subset construction সঠিকভাবে কাজ করছে।")
mismatches == 0 — একটি concrete, code-verified প্রমাণ
যে এই নির্দিষ্ট NFA ও তার সাবসেট-কনস্ট্রাক্টেড DFA হুবহু একই ভাষা রিকগনাইজ করে।
সাবসেট কনস্ট্রাকশন প্রমাণ করে NFA ও DFA-এর গণনাশক্তি ঠিক সমান — নন্ডিটারমিনিজম শুধু বর্ণনার সুবিধা, নতুন ক্ষমতা নয়। এই মূল ইনভ্যারিয়েন্ট ("DFA-এর সেট-স্টেট = সব সম্ভাব্য NFA-স্টেটের সেট") ও এর ব্যাচ-টেস্ট যাচাইয়ের প্যাটার্ন M3-এর ক্লোজার প্রপার্টি ও L11-এর Kleene's theorem-এও পুনরায় ব্যবহৃত হবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একটি n-স্টেট NFA থেকে DFA-তে সর্বোচ্চ কতটি স্টেট হতে পারে, এবং বাস্তবে সাধারণত কতটি হয়?
তাত্ত্বিক সর্বোচ্চ $2^n$ (Q_N-এর পাওয়ার সেটের আকার) — যেহেতু প্রতিটি DFA স্টেট একটি সাবসেট। কিন্তু বাস্তবে, উপরের উদাহরণে ৩-স্টেট NFA থেকে মাত্র ৪টি রিচেবল সাবসেট এসেছে ($2^3=8$-এর বদলে) — কারণ সব সাবসেট স্টার্ট স্টেট থেকে পৌঁছানো যায় এমন নয়। BFS-ভিত্তিক কনস্ট্রাকশন শুধু রিচেবল সাবসেটগুলোই তৈরি করে, যা প্র্যাকটিসে সাধারণত তাত্ত্বিক সর্বোচ্চের চেয়ে অনেক কম।
প্র ০২ সাবসেট কনস্ট্রাকশনের নতুন DFA কি সবসময় "টোটাল" (L06-এর প্রয়োজনীয়তা) হয়?
হ্যাঁ, স্বয়ংক্রিয়ভাবেই — কারণ $\delta_D(S,a) = \bigcup_{q\in S}\delta_N(q,a)$ সংজ্ঞা অনুযায়ী প্রতিটি সাবসেট $S$ ও প্রতিটি সিম্বল $a$-এর জন্য একটি (সম্ভাব্য খালি) নতুন সাবসেট রিটার্ন করে — খালি সেট $\emptyset$-ও একটি বৈধ DFA স্টেট (একটি "ডেড স্টেট", সব ইনপুটে নিজের কাছেই ফিরে আসে, কখনো অ্যাকসেপ্ট স্টেটে পৌঁছায় না)। তাই $\delta_D$ সবসময় প্রতিটি জোড়ার জন্য সংজ্ঞায়িত — L06-এর টোটালিটি রিকোয়ারমেন্ট স্বয়ংক্রিয়ভাবে পূরণ হয়।
প্র ০৩ কোড সেলের ব্যাচ-টেস্ট (১২৭টি স্ট্রিং) কি থিওরেমের একটি সম্পূর্ণ, সাধারণ প্রমাণ?
না — এটি একটি নির্দিষ্ট NFA-এর উপর থিওরেমের একটি concrete, code-verified নিশ্চিতকরণ, সাধারণ প্রমাণ নয়। সাধারণ প্রমাণ (উপরের সেকশন ৩-এ দেওয়া ইনভ্যারিয়েন্ট, স্ট্রিং-দৈর্ঘ্যের উপর স্ট্রাকচারাল ইনডাকশন দিয়ে) সব সম্ভাব্য স্ট্রিং, সব সম্ভাব্য NFA-এর জন্য কাজ করে দেখায় — কোড টেস্ট শুধু একটি finite sample-এ (এখানে দৈর্ঘ্য ৬ পর্যন্ত সব স্ট্রিং) hypothesis-টি ভুল প্রমাণ করার চেষ্টা করে ব্যর্থ হয়েছে, যা সাধারণ প্রমাণের একটি শক্তিশালী concrete সাপোর্ট, কিন্তু প্রতিস্থাপন নয়।
অনুশীলন
-
চিন্তা করুন: যদি NFA-এর $\delta_N(q,a)$ প্রতিটি জোড়ার জন্য ঠিক একটি স্টেট রিটার্ন করে
(অর্থাৎ NFA আসলে ইতিমধ্যে একটি DFA), সাবসেট কনস্ট্রাকশনের ফলাফল কী হবে?
রিচেবল সাবসেটগুলো সবসময় সিঙ্গলটন সেট ($\{q\}$ আকারের) হবে — কারণ প্রতিটি ট্রানজিশন ঠিক একটি স্টেট দেয়, তাই ইউনিয়ন কখনো একাধিক স্টেট একত্র করে না। ফলে DFA স্টেট সংখ্যা মূল NFA-এর স্টেট সংখ্যার সমান (বা কম, যদি কিছু স্টেট আনরিচেবল হয়) — সাবসেট কনস্ট্রাকশন কার্যত মূল DFA-টিরই একটি রিলেবেলড কপি ফেরত দেয়, যা প্রত্যাশিত: যেহেতু ইনপুট NFA-টি ইতিমধ্যেই ডিটারমিনিস্টিক।
-
পরীক্ষা করুন: কোড সেলে
for length in range(0, 7):লাইনটিrange(0, 9)-এ পরিবর্তন করে Run চাপুন — mismatches এখনও ০ থাকে কি না দেখুন।হ্যাঁ, mismatches এখনও ০ থাকবে (এখন টেস্ট স্ট্রিং সংখ্যা $2^0+2^1+\dots+2^8 = 511$-এ বেড়ে যাবে) — এটি প্রত্যাশিত, কারণ ইনভ্যারিয়েন্ট (সেকশন ৩) স্ট্রিং-দৈর্ঘ্যের উপর নির্ভর করে না, সব দৈর্ঘ্যের স্ট্রিং-এর জন্যই সমানভাবে প্রযোজ্য — বেশি স্ট্রিং টেস্ট করলে শুধু আমাদের কনফিডেন্স বাড়ে, থিওরেমের সত্যতা বদলায় না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ — এপসিলন-NFA ও এপসিলন-ক্লোজার L09 ε-ট্রানজিশন যোগ করে NFA-কে আরও প্রসারিত করা হবে — এবং সেগুলো সরিয়ে আবার এই পাঠের কৌশলে DFA-তে ফেরত যাওয়া যাবে।
- আগের পাঠ — NFA ফরমাল ডেফিনিশন L07 এই পাঠের সাবসেট কনস্ট্রাকশনের ইনপুট NFA-টি L07-এ প্রথম সংজ্ঞায়িত হয়েছিল।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ DFA থেকে টুরিং মেশিন পর্যন্ত ধাপে ধাপে বাড়তে থাকা গণনাশক্তির সম্পূর্ণ মানচিত্র।
- Programming Languages & Compiler Design কোর্স সঙ্গী কোর্স এই একই সাবসেট কনস্ট্রাকশন অ্যালগরিদম সেই কোর্সের M4/L18-এ বাস্তবে লেক্সার বানাতে ব্যবহৃত হয়েছে।