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

NFA থেকে DFA ইকুইভ্যালেন্স — সাবসেট কনস্ট্রাকশন

NFA to DFA equivalence — subset construction
৯ মিনিট পড়া মাঝারি · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • 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)$।

NFA N Q_N স্টেট, δ_N (L07) সাবসেট কনস্ট্রাকশন প্রতিটি DFA স্টেট = NFA স্টেটদের সেট δ_D(S,a)=⋃δ_N(q,a), q∈S DFA D L(D) = L(N)
সাবসেট কনস্ট্রাকশন — DFA-এর প্রতিটি স্টেট আসলে NFA-স্টেটদের একটি সাবসেট, এবং শুধু স্টার্ট থেকে পৌঁছানো যায় এমন সাবসেটগুলোই তৈরি হয় (তাত্ত্বিক $2^{|Q_N|}$-এর চেয়ে সাধারণত অনেক কম)।

৪ · কোড: সাবসেট কনস্ট্রাকশন ও থিওরেমের ব্যাচ-টেস্ট যাচাই

নিচে L07-এর "01 সাবস্ট্রিং" NFA-এর উপর সাবসেট কনস্ট্রাকশন চালানো হয়েছে — একটি BFS বর্তমান সাবসেট থেকে শুরু করে ধাপে ধাপে সব পৌঁছানো-যায়-এমন সাবসেট আবিষ্কার করে (তাত্ত্বিক $2^3=8$টি সম্ভাব্য সাবসেটের বদলে বাস্তবে মাত্র কয়েকটি রিচেবল হয়)। এরপর — শুধু অ্যালগরিদম চালানো নয়, থিওরেমের একটি সত্যিকারের সঠিকতা-যাচাই হিসেবে — দৈর্ঘ্য ০ থেকে ৬ পর্যন্ত সব সম্ভাব্য বাইনারি স্ট্রিং (মোট ১২৭টি) NFA ও নতুন DFA উভয়ের বিরুদ্ধে টেস্ট করে assert করা হয়েছে যে প্রতিটিতে ফলাফল হুবহু মেলে।

Python
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 সঠিকভাবে কাজ করছে।")

    
চালানোর পর দেখা যায় ৩-স্টেট NFA থেকে ঠিক ৪টি রিচেবল সাবসেট স্টেট তৈরি হয় — $\{q_0\}$, $\{q_0,q_1\}$, $\{q_0,q_2\}$, এবং $\{q_0,q_1,q_2\}$ (হাতে ট্রেস করলে: $\{q_0\}$ থেকে '0'-এ $\{q_0,q_1\}$, '1'-এ $\{q_0\}$; $\{q_0,q_1\}$ থেকে '0'-এ $\{q_0,q_1\}$, '1'-এ $\{q_0,q_2\}$; $\{q_0,q_2\}$ থেকে '0'-এ $\{q_0,q_1,q_2\}$, '1'-এ $\{q_0,q_2\}$; $\{q_0,q_1,q_2\}$ থেকে উভয় সিম্বলে নিজের কাছেই ঘুরে আসে বা $\{q_0,q_2\}$-এ যায়) — তাত্ত্বিক সর্বোচ্চ $2^3=8$-এর চেয়ে অনেক কম, যেহেতু বাকি সাবসেটগুলো $q_0$ থেকে কখনো পৌঁছানো যায় না। এরপর ১২৭টি টেস্ট স্ট্রিং-এর প্রতিটিতে mismatches == 0 — একটি concrete, code-verified প্রমাণ যে এই নির্দিষ্ট NFA ও তার সাবসেট-কনস্ট্রাক্টেড DFA হুবহু একই ভাষা রিকগনাইজ করে।
মূল কথা · Key takeaway

সাবসেট কনস্ট্রাকশন প্রমাণ করে 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 সাপোর্ট, কিন্তু প্রতিস্থাপন নয়।

অনুশীলন

  1. চিন্তা করুন: যদি NFA-এর $\delta_N(q,a)$ প্রতিটি জোড়ার জন্য ঠিক একটি স্টেট রিটার্ন করে (অর্থাৎ NFA আসলে ইতিমধ্যে একটি DFA), সাবসেট কনস্ট্রাকশনের ফলাফল কী হবে?

    রিচেবল সাবসেটগুলো সবসময় সিঙ্গলটন সেট ($\{q\}$ আকারের) হবে — কারণ প্রতিটি ট্রানজিশন ঠিক একটি স্টেট দেয়, তাই ইউনিয়ন কখনো একাধিক স্টেট একত্র করে না। ফলে DFA স্টেট সংখ্যা মূল NFA-এর স্টেট সংখ্যার সমান (বা কম, যদি কিছু স্টেট আনরিচেবল হয়) — সাবসেট কনস্ট্রাকশন কার্যত মূল DFA-টিরই একটি রিলেবেলড কপি ফেরত দেয়, যা প্রত্যাশিত: যেহেতু ইনপুট NFA-টি ইতিমধ্যেই ডিটারমিনিস্টিক।

  2. পরীক্ষা করুন: কোড সেলে for length in range(0, 7): লাইনটি range(0, 9)-এ পরিবর্তন করে Run চাপুন — mismatches এখনও ০ থাকে কি না দেখুন।

    হ্যাঁ, mismatches এখনও ০ থাকবে (এখন টেস্ট স্ট্রিং সংখ্যা $2^0+2^1+\dots+2^8 = 511$-এ বেড়ে যাবে) — এটি প্রত্যাশিত, কারণ ইনভ্যারিয়েন্ট (সেকশন ৩) স্ট্রিং-দৈর্ঘ্যের উপর নির্ভর করে না, সব দৈর্ঘ্যের স্ট্রিং-এর জন্যই সমানভাবে প্রযোজ্য — বেশি স্ট্রিং টেস্ট করলে শুধু আমাদের কনফিডেন্স বাড়ে, থিওরেমের সত্যতা বদলায় না।

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

আগের পাঠ
NFA — ফরমাল ডেফিনিশন