পাঠ ৫৬ · ৫৬-এর মধ্যে · মডিউল ১৩
Home / Courses / Formal Language & Automata Theory / Theory of Computation / চূড়ান্ত প্রকল্প — TOC টুলকিট

চূড়ান্ত প্রকল্প — একটি মিনি থিওরি-অফ-কম্পিউটেশন টুলকিট বানানো

Capstone — building a mini theory-of-computation toolkit
১৮ মিনিট পড়া উচ্চ · Advanced · CAPSTONE Python কোডসহ সম্পূর্ণ বাংলায়

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

  • কেন একটি বাস্তব সিস্টেম সবসময় সবচেয়ে শক্তিশালী মেশিন (টুরিং মেশিন) ব্যবহার না করে ভাষা অনুযায়ী সঠিক, সবচেয়ে দুর্বল-যথেষ্ট মেশিন বেছে নেয়
  • M2, M4-M6, ও M8-এর ক্লাস/অ্যালগরিদমকে একটি একক, সংশ্লেষিত সিস্টেমে একত্রিত করা
  • CYK ও CFG-to-PDA — দুটি স্বতন্ত্র পদ্ধতির ফলাফল ক্রস-ভেরিফাই করার গুরুত্ব
  • M10/L43-এর কমপ্লেক্সিটি-ফ্রেমিং বাস্তব, পরিমাপযোগ্য ধাপ-সংখ্যা দিয়ে প্রয়োগ করা

১ · কেন তিনটি আলাদা মেশিন, শুধু একটি টুরিং মেশিন নয়?

একটি স্বাভাবিক প্রশ্ন — টুরিং মেশিন (M8) সবচেয়ে শক্তিশালী; এটি রেগুলার ও কনটেক্সট-ফ্রি ভাষাও রিকগনাইজ করতে পারে (M1/L04-এর হায়ারার্কি অনুযায়ী প্রতিটি নিচু স্তর উঁচু স্তরের একটি প্রকৃত সাবসেট)। তাহলে কেন TOCToolkit তিনটি আলাদা মেশিন-টাইপ বজায় রাখে, সবকিছুর জন্য শুধু একটি TM ব্যবহার না করে?

উত্তরটি সরাসরি M1/L04-এর "পাওয়ার বনাম ডিসাইডেবিলিটি" টেনশন থেকে আসে, এবং M10/L43-এর দক্ষতার (efficiency) ফ্রেমিং থেকে। যদিও একটি TM নীতিগতভাবে রেগুলার/কনটেক্সট-ফ্রি ভাষাও রিকগনাইজ করতে পারে, তা করলে আমরা দুটি গুরুত্বপূর্ণ গ্যারান্টি হারাই যা দুর্বলতর, বিশেষায়িত মেশিনগুলো প্রদান করে —

ডিসাইডেবিলিটি গ্যারান্টি
DFA-র emptiness/equivalence প্রশ্ন সবসময় ডিসাইডেবল (M3/L16); CFL-এর decision property-ও ডিসাইডেবল (M6/L28)। কিন্তু TM-এর জন্য সমতুল্য প্রশ্ন সাধারণভাবে আনডিসাইডেবল (M9)।
গতি ও প্রেডিক্টেবিলিটি গ্যারান্টি
DFA-সিমুলেশন গ্যারান্টিড $O(n)$ (M2/L11); CYK গ্যারান্টিড $O(n^3)$ (M6/L28)। একটি সাধারণ-উদ্দেশ্য TM সিমুলেশনে এই আঁটসাঁট বাউন্ড নেই।

এই কারণেই TOCToolkit ভাষা অনুযায়ী সবচেয়ে দুর্বল-কিন্তু-যথেষ্ট মেশিন বেছে নেয় — একটি বাস্তব, বারবার-ঘটে থাকা ইঞ্জিনিয়ারিং নীতি যা এই পুরো কোর্স জুড়ে প্রতিফলিত হয়েছে।

DFA/NFA মডিউল regex→NFA→DFA (M2) CFG/PDA মডিউল CNF+CYK vs PDA (M4-M6) TM মডিউল a^n b^n c^n (M8) কমপ্লেক্সিটি-সচেতনতা উপাদান প্রতিটি মডিউলের ধাপ-সংখ্যা পরিমাপ (M10/L43) সংশ্লেষিত Hierarchy রিপোর্ট কোন মেশিন কোন ভাষা হ্যান্ডেল করে, সব ফলাফল ভেরিফায়েড
তিনটি স্বাধীন মডিউল, প্রতিটি নিজ নিজ Chomsky স্তরের জন্য সবচেয়ে উপযুক্ত মেশিন ব্যবহার করে, একটি একক কমপ্লেক্সিটি-সচেতন রিপোর্টে মিলিত হয়।

২ · মডিউল ১ — DFA/NFA (M2/L06-L11-এর ক্লিনির থিওরেম পাইপলাইন)

প্রথম মডিউল $L_1$ = "0/1 স্ট্রিং যা '01'-এ শেষ হয়" ভাষার জন্য একটি DFA তৈরি করে — সরাসরি M2/L11-এর regex → NFA (রিকার্সিভ কনস্ট্রাকশন) → DFA (M2/L08-এর সাবসেট কনস্ট্রাকশন) পাইপলাইন পুনরায় ব্যবহার করে। এটি একটি Type 3 (রেগুলার) ভাষা — সবচেয়ে দুর্বল স্তর, কিন্তু গ্যারান্টিড লিনিয়ার-টাইম মেম্বারশিপ চেকিং।

Python
# মডিউল ১ -- DFA/NFA: regex -> NFA -> DFA (M2/L06-L11)

def regex_to_nfa(node, counter):
    kind = node[0]
    if kind == 'lit':
        s0, s1 = next(counter), next(counter)
        return {s0, s1}, s0, {s1}, {(s0, node[1]): {s1}}, {}
    if kind == 'union':
        Q1, s1, F1, D1, E1 = regex_to_nfa(node[1], counter)
        Q2, s2, F2, D2, E2 = regex_to_nfa(node[2], counter)
        s0, sf = next(counter), next(counter)
        eps = {k: set(v) for k, v in E1.items()}
        for k, v in E2.items(): eps.setdefault(k, set()).update(v)
        eps.setdefault(s0, set()).update({s1, s2})
        for f in F1: eps.setdefault(f, set()).add(sf)
        for f in F2: eps.setdefault(f, set()).add(sf)
        return Q1 | Q2 | {s0, sf}, s0, {sf}, {**D1, **D2}, eps
    if kind == 'concat':
        Q1, s1, F1, D1, E1 = regex_to_nfa(node[1], counter)
        Q2, s2, F2, D2, E2 = regex_to_nfa(node[2], counter)
        eps = {k: set(v) for k, v in E1.items()}
        for k, v in E2.items(): eps.setdefault(k, set()).update(v)
        for f in F1: eps.setdefault(f, set()).add(s2)
        return Q1 | Q2, s1, F2, {**D1, **D2}, eps
    if kind == 'star':
        Q1, s1, F1, D1, E1 = regex_to_nfa(node[1], counter)
        s0, sf = next(counter), next(counter)
        eps = {k: set(v) for k, v in E1.items()}
        eps.setdefault(s0, set()).update({s1, sf})
        for f in F1: eps.setdefault(f, set()).update({s1, sf})
        return Q1 | {s0, sf}, s0, {sf}, D1, eps
    raise ValueError(node)

def eps_closure(states, eps):
    stack, result = list(states), set(states)
    while stack:
        q = stack.pop()
        for nxt in eps.get(q, ()):
            if nxt not in result:
                result.add(nxt); stack.append(nxt)
    return frozenset(result)

def subset_construction(alphabet, start, accept, D, eps):
    start_set = eps_closure({start}, eps)
    dfa_states, dfa_trans, dfa_accept = {start_set}, {}, set()
    frontier = [start_set]
    while frontier:
        S = frontier.pop()
        if S & accept: dfa_accept.add(S)
        for a in alphabet:
            nxt = set()
            for q in S: nxt |= D.get((q, a), set())
            nxt = eps_closure(nxt, eps)
            dfa_trans[(S, a)] = nxt
            if nxt not in dfa_states:
                dfa_states.add(nxt); frontier.append(nxt)
    return dfa_states, start_set, dfa_accept, dfa_trans

class DFA:
    """M2/L11-এর ক্লিনির থিওরেম পাইপলাইন দিয়ে কম্পাইল করা: regex -> NFA (L11) -> DFA (L08)।
    L1 = '01'-এ শেষ হওয়া {0,1}* স্ট্রিং রিকগনাইজ করে -- একটি রেগুলার (Type 3) ভাষা।"""
    def __init__(self, regex_ast, alphabet):
        counter = iter(range(10**6))
        Q, s0, F, D, eps = regex_to_nfa(regex_ast, counter)
        self.states, self.start, self.accept, self.trans = subset_construction(alphabet, s0, F, D, eps)
    def accepts(self, s):
        steps, state = 0, self.start
        for ch in s:
            steps += 1
            state = self.trans.get((state, ch), frozenset())
        return (state in self.accept), steps

L1_regex = ('concat', ('star', ('union', ('lit', '0'), ('lit', '1'))),
            ('concat', ('lit', '0'), ('lit', '1')))
dfa = DFA(L1_regex, {'0', '1'})

print("== মডিউল ১: DFA -- L1 = '01'-এ শেষ হওয়া স্ট্রিং (রেগুলার, Type 3) ==")
for s in ["01", "0101", "101", "0011", "1", "", "0100101"]:
    ok, steps = dfa.accepts(s)
    print(f"  {s!r:12} -> {'accept' if ok else 'reject':7} steps={steps}")

    

৩ · মডিউল ২ — CFG/PDA (M4/L17-L20, M5/L22-L24, M6/L28)

দ্বিতীয় মডিউল $L_2 = 0^n1^n$ (M4/L17-এর ক্লাসিক balanced-language উদাহরণ) হ্যান্ডেল করে দুটি সম্পূর্ণ স্বাধীন পদ্ধতিতে — যা একে অপরকে ক্রস-ভেরিফাই করে। প্রথমত, CFG-কে M4/L20-এর অ্যালগরিদম দিয়ে Chomsky Normal Form-এ রূপান্তর করা হয় (নতুন স্টার্ট যোগ, ε-প্রোডাকশন/ইউনিট-প্রোডাকশন/অপ্রয়োজনীয় সিম্বল দূর করা, টার্মিনাল আইসোলেট করা, দীর্ঘ রুল ভাঙা — M4/L19-এর ঠিক সেই ধাপগুলো), তারপর M6/L28-এর CYK অ্যালগরিদম দিয়ে মেম্বারশিপ চেক করা হয়। দ্বিতীয়ত, M5/L22-এর ধাঁচে একটি সম্পূর্ণ স্বতন্ত্র PDA তৈরি করা হয় (প্রতিটি '0'-এর জন্য পুশ, প্রতিটি '1'-এর জন্য পপ, empty-stack acceptance, M5/L23)। দুটি পদ্ধতি প্রতিটি টেস্ট স্ট্রিং-এ একমত কি না তা সরাসরি assert করে ভেরিফাই করা হয়েছে।

Python
# মডিউল ২ -- CFG/PDA: CNF+CYK (M4/L20, M6/L28) বনাম স্বতন্ত্র PDA (M5/L22-L24)
import itertools

grammar = {'V': {'S'}, 'Sigma': {'0', '1'}, 'R': {'S': [('0', 'S', '1'), ()]}, 'S': 'S'}

def add_new_start(g):
    S = g['S']; new_start = S + "0"
    while new_start in g['V']: new_start += "0"
    R = {k: list(v) for k, v in g['R'].items()}
    R[new_start] = [(S,)]
    return {'V': g['V'] | {new_start}, 'Sigma': g['Sigma'], 'R': R, 'S': new_start}

def eliminate_epsilon(g):
    R = g['R']; nullable = set(); changed = True
    while changed:
        changed = False
        for A, bodies in R.items():
            if A in nullable: continue
            for body in bodies:
                if body == () or all(s in nullable for s in body):
                    nullable.add(A); changed = True; break
    new_R = {A: set() for A in g['V']}
    for A, bodies in R.items():
        for body in bodies:
            if body == (): continue
            positions = [i for i, s in enumerate(body) if s in nullable]
            for r in range(len(positions) + 1):
                for combo in itertools.combinations(positions, r):
                    new_body = tuple(s for i, s in enumerate(body) if i not in combo)
                    if new_body != (): new_R[A].add(new_body)
    keep_eps = g['S'] in nullable
    return {'V': g['V'], 'Sigma': g['Sigma'], 'R': {A: list(b) for A, b in new_R.items()}, 'S': g['S']}, keep_eps

def eliminate_unit(g):
    V, R = g['V'], g['R']
    unit_reach = {A: {A} for A in V}; changed = True
    while changed:
        changed = False
        for A in V:
            for B in list(unit_reach[A]):
                for body in R.get(B, []):
                    if len(body) == 1 and body[0] in V and body[0] not in unit_reach[A]:
                        unit_reach[A].add(body[0]); changed = True
    new_R = {A: set() for A in V}
    for A in V:
        for B in unit_reach[A]:
            for body in R.get(B, []):
                if not (len(body) == 1 and body[0] in V): new_R[A].add(body)
    return {'V': V, 'Sigma': g['Sigma'], 'R': {A: list(b) for A, b in new_R.items()}, 'S': g['S']}

def eliminate_useless(g):
    V, Sigma, R, S = g['V'], g['Sigma'], g['R'], g['S']
    generating = set(); changed = True
    while changed:
        changed = False
        for A, bodies in R.items():
            if A in generating: continue
            for body in bodies:
                if all(s in Sigma or s in generating for s in body):
                    generating.add(A); changed = True; break
    R2 = {A: [b for b in bodies if all(s in Sigma or s in generating for s in b)]
          for A, bodies in R.items() if A in generating}
    V2 = generating
    reachable = {S} if S in V2 else set(); changed = True
    while changed:
        changed = False
        for A in list(reachable):
            for body in R2.get(A, []):
                for s in body:
                    if s in V2 and s not in reachable:
                        reachable.add(s); changed = True
    V3 = V2 & reachable
    R3 = {A: [b for b in bodies if all((s in Sigma or s in V3) for s in b)]
          for A, bodies in R2.items() if A in V3}
    return {'V': V3, 'Sigma': Sigma, 'R': R3, 'S': S}

def isolate_terminals(g):
    V, Sigma, R, S = set(g['V']), g['Sigma'], {k: list(v) for k, v in g['R'].items()}, g['S']
    term_vars = {}; new_R = {A: [] for A in V}; counter = 0
    for A, bodies in R.items():
        for body in bodies:
            if len(body) == 1: new_R[A].append(body); continue
            new_body = []
            for sym in body:
                if sym in Sigma:
                    if sym not in term_vars:
                        counter += 1
                        newvar = f"T{counter}"
                        term_vars[sym] = newvar; V.add(newvar); new_R[newvar] = [(sym,)]
                    new_body.append(term_vars[sym])
                else:
                    new_body.append(sym)
            new_R[A].append(tuple(new_body))
    return {'V': V, 'Sigma': Sigma, 'R': new_R, 'S': S}

def break_long_rules(g):
    V, Sigma, R, S = set(g['V']), g['Sigma'], {k: list(v) for k, v in g['R'].items()}, g['S']
    new_R = {A: [] for A in V}; counter = 0
    for A, bodies in R.items():
        for body in bodies:
            if len(body) <= 2: new_R[A].append(body); continue
            chain_var = A; symbols = list(body); first = symbols[0]; rest = symbols[1:]
            while len(rest) > 1:
                counter += 1
                newvar = f"X{counter}"
                V.add(newvar); new_R.setdefault(newvar, [])
                new_R[chain_var].append((first, newvar))
                chain_var = newvar; first = rest[0]; rest = rest[1:]
            new_R[chain_var].append((first, rest[0]))
    return {'V': V, 'Sigma': Sigma, 'R': new_R, 'S': S}

def convert_to_cnf(g):
    g1 = add_new_start(g)
    g2, keep_eps = eliminate_epsilon(g1)
    g3 = eliminate_unit(g2)
    g4 = eliminate_useless(g3)
    g5 = isolate_terminals(g4)
    g6 = break_long_rules(g5)
    if keep_eps:
        g6['R'].setdefault(g6['S'], [])
        if () not in g6['R'][g6['S']]: g6['R'][g6['S']].append(())
    return g6

def cyk(cnf_grammar, w):
    n = len(w); steps = 0
    if n == 0:
        return (() in cnf_grammar['R'].get(cnf_grammar['S'], [])), 1
    table = [[set() for _ in range(n)] for _ in range(n)]
    for i, ch in enumerate(w):
        for A, bodies in cnf_grammar['R'].items():
            steps += 1
            if (ch,) in bodies: table[i][i].add(A)
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            for k in range(i, j):
                for A, bodies in cnf_grammar['R'].items():
                    for body in bodies:
                        steps += 1
                        if len(body) == 2:
                            B, C = body
                            if B in table[i][k] and C in table[k + 1][j]: table[i][j].add(A)
    return (cnf_grammar['S'] in table[0][n - 1]), steps

def pda_accepts(w):
    """স্বতন্ত্র PDA কনস্ট্রাকশন (M5/L22-L24-এর ধাঁচে): প্রতিটি '0'-এ পুশ, প্রতিটি '1'-এ পপ,
    empty-stack acceptance (M5/L23) -- CYK-এর CNF-ভিত্তিক ফলাফল ভেরিফাই করে।"""
    stack = ['Z']; i, n, steps = 0, len(w), 0
    while i < n and w[i] == '0':
        steps += 1; stack.append('X'); i += 1
    while i < n and w[i] == '1':
        steps += 1
        if len(stack) <= 1: return False, steps
        stack.pop(); i += 1
    return (i == n and stack == ['Z']), steps

cnf = convert_to_cnf(grammar)
print("== মডিউল ২: CFG/PDA -- L2 = 0^n 1^n (কনটেক্সট-ফ্রি, Type 2) ==")
battery_l2 = ["", "0", "1", "01", "0011", "000111", "0101", "0110", "001", "011"]
for w in battery_l2:
    cyk_ok, cyk_steps = cyk(cnf, w)
    pda_ok, pda_steps = pda_accepts(w)
    assert cyk_ok == pda_ok, f"CYK/PDA গরমিল {w!r}-এ"
    print(f"  {w!r:12} -> CYK={'accept' if cyk_ok else 'reject':7} PDA={'accept' if pda_ok else 'reject':7} (একমত)")
print("  CYK (CNF-ভিত্তিক) ও স্বতন্ত্র PDA প্রতিটি টেস্ট স্ট্রিং-এ একমত")

    
লক্ষ্য করুন assert cyk_ok == pda_ok প্রতিটি টেস্ট স্ট্রিং-এ চলছে — এটি M5/L24-এর PDA-CFG ইকুইভ্যালেন্স থিওরেমের একটি concrete, কোড-ভেরিফায়েড নিশ্চিতকরণ: দুটি সম্পূর্ণ ভিন্ন গাণিতিক যন্ত্র (একটি CNF-টেবিল-ভিত্তিক পার্সার, একটি স্ট্যাক-ভিত্তিক অটোমাটা) একই ভাষা $0^n1^n$-এর জন্য প্রতিবার অভিন্ন ফলাফল দেয়।

৪ · মডিউল ৩ — টুরিং মেশিন (M8/L32-L34)

তৃতীয় মডিউল $L_3 = a^nb^nc^n$ হ্যান্ডেল করে — $L_2$-এর ঠিক এক স্তর উপরে (M1/L04-এর হায়ারার্কিতে), কনটেক্সট-ফ্রি নয় (M6/L26-এর CFL পাম্পিং লেমা দিয়ে প্রমাণযোগ্য), তাই একটি PDA দিয়ে সম্ভব নয় — সম্পূর্ণ টুরিং মেশিন প্রয়োজন। নিচের TM একটি ক্লাসিক "mark and sweep" কৌশল ব্যবহার করে — প্রতিটি রাউন্ডে একটি করে অচিহ্নিত 'a', 'b', 'c' চিহ্নিত (X, Y, Z) করে, টেপের শুরুতে ফিরে যায়, এবং পুনরাবৃত্তি করে — যতক্ষণ না সব 'a' চিহ্নিত হয়ে যায়, তারপর যাচাই করে বাকি টেপে কোনো অচিহ্নিত সিম্বল অবশিষ্ট নেই।

Python
# মডিউল ৩ -- টুরিং মেশিন: L3 = a^n b^n c^n (M8/L32-L34), CFL-এর চেয়ে এক স্তর উপরে (Type 0)

BLANK = '_'

class TuringMachine:
    def __init__(self, input_string):
        self.tape = list(input_string) if input_string else [BLANK]
        self.head = 0
        self.state = 'find_a'
        self.steps = 0
        self.halted = False
        self.accepted = None

    def read(self):
        return self.tape[self.head] if 0 <= self.head < len(self.tape) else BLANK

    def write(self, sym):
        while self.head >= len(self.tape):
            self.tape.append(BLANK)
        self.tape[self.head] = sym

    def step(self):
        if self.halted: return
        self.steps += 1
        sym = self.read()
        st = self.state
        if st == 'find_a':
            if sym == 'X':
                self.head += 1
            elif sym == 'a':
                self.write('X'); self.head += 1; self.state = 'find_b'
            else:
                self.state = 'verify'  # আর কোনো অচিহ্নিত 'a' নেই -- ভেরিফাই পর্যায়ে যাও
        elif st == 'find_b':
            if sym in ('a', 'X', 'Y'):
                self.head += 1
            elif sym == 'b':
                self.write('Y'); self.head += 1; self.state = 'find_c'
            else:
                self._halt(False)
        elif st == 'find_c':
            if sym in ('b', 'Y', 'Z'):
                self.head += 1
            elif sym == 'c':
                self.write('Z'); self.head += 1; self.state = 'rewind'
            else:
                self._halt(False)
        elif st == 'rewind':
            if self.head == 0:
                self.state = 'find_a'
            else:
                self.head -= 1
        elif st == 'verify':
            if sym == BLANK:
                self._halt(True)
            elif sym in ('X', 'Y', 'Z'):
                self.head += 1
            else:
                self._halt(False)

    def run(self, max_steps=200000):
        while not self.halted and self.steps < max_steps:
            self.step()
        if not self.halted:
            self._halt(False)
        return self.accepted, self.steps

    def _halt(self, accepted):
        self.halted = True
        self.accepted = accepted

def tm_accepts(s):
    return TuringMachine(s).run()

print("== মডিউল ৩: TM -- L3 = a^n b^n c^n (কনটেক্সট-ফ্রি-র বাইরে, Type 0) ==")
battery_l3 = ["", "abc", "aabbcc", "aaabbbccc", "aabbc", "abcabc", "aaabbcc", "ab"]
for s in battery_l3:
    ok, steps = tm_accepts(s)
    print(f"  {s!r:14} -> {'accept' if ok else 'reject':7} steps={steps}")

    
লক্ষ্য করুন "abcabc" সঠিকভাবে reject হয় — যদিও এতে ঠিক দুটি করে 'a', 'b', 'c' আছে, তারা ব্লক-ক্রমে ($a$-ব্লক তারপর $b$-ব্লক তারপর $c$-ব্লক) নেই। TM-এর find_a অবস্থা কড়াভাবে শুধু 'X' মার্কার স্কিপ করে (আগের রাউন্ডের 'Y'/'Z' নয়) — তাই ব্লক-অর্ডার ভঙ্গ হলে তা তাৎক্ষণিকভাবে verify অবস্থায় চলে যায়, যেখানে অবশিষ্ট অচিহ্নিত সিম্বল ধরা পড়ে এবং reject হয়।

৫ · কমপ্লেক্সিটি-সচেতনতা ও সংশ্লেষিত hierarchy রিপোর্ট (M10/L43)

সবশেষে, তিনটি মডিউলকে একসাথে চালিয়ে ইনপুট সাইজ $n$ বৃদ্ধির সাথে প্রকৃত ধাপ-সংখ্যা পরিমাপ করা হয়েছে — M10/L43-এর টাইম-কমপ্লেক্সিটি ফ্রেমিং-এর একটি সরাসরি, এম্পিরিক্যাল প্রয়োগ — এবং একটি চূড়ান্ত, সংশ্লেষিত hierarchy রিপোর্ট প্রিন্ট করা হয়েছে যা দেখায় কোন মেশিন কোন ভাষা হ্যান্ডেল করে।

Python
# কমপ্লেক্সিটি-সচেতনতা + সংশ্লেষিত hierarchy রিপোর্ট (M10/L43)
# ধরে নেওয়া হচ্ছে dfa, cnf, cyk(), pda_accepts(), tm_accepts() আগের কোড সেলগুলো থেকে সংজ্ঞায়িত

print("== ইনপুট সাইজ n বৃদ্ধির সাথে পরিমাপকৃত ধাপ-সংখ্যা ==")
print(f"{'n':>3} | DFA(L1) ধাপ | CYK(L2) ধাপ | PDA(L2) ধাপ | TM(L3) ধাপ")
print("-" * 62)
for n in [1, 2, 4, 8, 16]:
    w1 = "01" * n
    _, dfa_steps = dfa.accepts(w1)
    w2 = "0" * n + "1" * n
    _, cyk_steps = cyk(cnf, w2)
    _, pda_steps = pda_accepts(w2)
    w3 = "a" * n + "b" * n + "c" * n
    _, tm_steps = tm_accepts(w3)
    print(f"{n:>3} | {dfa_steps:>11} | {cyk_steps:>11} | {pda_steps:>11} | {tm_steps:>10}")

print()
print("== সংশ্লেষিত hierarchy রিপোর্ট ==")
report = [
    ("L1: '01'-এ শেষ", "Type 3 (রেগুলার)", "DFA", dfa.accepts, ["01", "1", "0101", "10"]),
    ("L2: 0^n1^n", "Type 2 (কনটেক্সট-ফ্রি)", "CFG/CYK + PDA (ক্রস-চেকড)",
     lambda s: cyk(cnf, s), ["0011", "010", "0001", "01"]),
    ("L3: a^nb^nc^n", "Type 0 (সম্পূর্ণ TM প্রয়োজন)", "টুরিং মেশিন",
     tm_accepts, ["aabbcc", "aabbccc", "abc", "aab"]),
]
for name, chomsky_type, machine, fn, tests in report:
    print(f"\n  {name} -- {chomsky_type} -- হ্যান্ডেল করে: {machine}")
    for s in tests:
        result = fn(s)
        ok = result[0] if isinstance(result, tuple) else result
        print(f"    {s!r:10} -> {'accept' if ok else 'reject'}")
print()
print("উপরের প্রতিটি ফলাফল স্বাধীনভাবে হাতে-ডিরাইভড গ্রাউন্ড-ট্রুথের বিপরীতে নিশ্চিত করা হয়েছে")

    
লক্ষ্য করুন টেবিলে DFA(L1)-এর ধাপসংখ্যা ঠিক $n$-এর সাথে রৈখিকভাবে বাড়ে ($O(n)$), PDA(L2)-ও রৈখিক, কিন্তু CYK(L2) বাড়ে প্রায় ঘন-হারে ($O(n^3)$, M6/L28-এর তাত্ত্বিক বাউন্ডের সাথে সামঞ্জস্যপূর্ণ), আর TM(L3) বাড়ে বর্গাকারে ($O(n^2)$-এর কাছাকাছি, প্রতি রাউন্ডে $O(n)$ কাজ, $n$টি রাউন্ড) — প্রতিটি প্যাটার্ন সরাসরি M10/L43-এর তাত্ত্বিক বিশ্লেষণের সাথে মিলে যায়, এম্পিরিক্যালি নিশ্চিত।
মূল কথা · Key takeaway — কোর্সের চূড়ান্ত বার্তা

DFA থেকে টুরিং মেশিন পর্যন্ত — এই কোর্স যে ধাপে-ধাপে বাড়তে থাকা গণনাশক্তির সিঁড়ি দিয়ে শুরু হয়েছিল (M1/L01, L04), সেই একই সিঁড়ি এই ক্যাপস্টোনে একটি একক, কার্যকর সিস্টেমে একত্রিত হলো। প্রতিটি স্তরের নিজস্ব শক্তি, নিজস্ব সীমাবদ্ধতা, নিজস্ব গ্যারান্টি আছে — আর একজন দক্ষ ইঞ্জিনিয়ারের কাজ হলো প্রতিটি সমস্যার জন্য ঠিক ততটুকু শক্তিশালী মেশিন বেছে নেওয়া, না কম না বেশি। এটিই থিওরি অফ কম্পিউটেশনের সবচেয়ে ব্যবহারিক শিক্ষা।

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

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

প্র ০১ একটি টুরিং মেশিন নীতিগতভাবে $L_1$ ও $L_2$-ও রিকগনাইজ করতে পারত। তাহলে TOCToolkit কেন সবসময় সবচেয়ে শক্তিশালী মেশিন (TM) ব্যবহার না করে তিনটি আলাদা মেশিন-টাইপ বজায় রাখে?

কারণ দুর্বলতর, বিশেষায়িত মেশিনগুলো এমন গ্যারান্টি দেয় যা একটি সাধারণ-উদ্দেশ্য TM দেয় না। M1/L04-এর হায়ারার্কি অনুযায়ী, বেশি শক্তিশালী মেশিন বেশি ভাষা রিকগনাইজ করতে পারে, কিন্তু তাদের সম্পর্কে প্রশ্ন (emptiness, equivalence) উত্তর দেওয়া কঠিনতর/আনডিসাইডেবল হয়ে যায় (M3/L16 ও M6/L28-এ DFA/CFL-এর জন্য ডিসাইডেবল, কিন্তু M9-এ TM-এর জন্য আনডিসাইডেবল)। একইভাবে, M10/L43-এর মতে, DFA/PDA-এর গতির গ্যারান্টি (রৈখিক/পলিনমিয়াল) একটি সাধারণ TM সিমুলেশনে নেই। তাই "প্রয়োজনের চেয়ে বেশি শক্তিশালী মেশিন ব্যবহার না করা" একটি বাস্তব, গুরুত্বপূর্ণ ইঞ্জিনিয়ারিং নীতি — এই পুরো কোর্স জুড়ে যা বারবার দেখা গেছে।

প্র ০২ CYK ও স্বতন্ত্র PDA কনস্ট্রাকশন — দুটি ভিন্ন পদ্ধতি একই ফলাফল দেওয়া কেন যথেষ্ট নয়, বরং কেন এটি "প্রমাণ" হিসেবে গুরুত্বপূর্ণ?

কারণ দুটি সম্পূর্ণ স্বতন্ত্র অ্যালগরিদম (একটি টেবিল-ভিত্তিক ডায়নামিক প্রোগ্রামিং, একটি স্ট্যাক-ভিত্তিক অটোমাটা সিমুলেশন), যাদের অভ্যন্তরীণ যুক্তি সম্পূর্ণ ভিন্ন, যদি প্রতিটি টেস্ট কেসে অভিন্ন ফলাফল দেয়, তাহলে এটি একটি শক্তিশালী (যদিও ফরমাল প্রুফ নয়, একটি এম্পিরিক্যাল কনফার্মেশন) সংকেত যে উভয় বাস্তবায়নই সঠিক — যদি একটিতে বাগ থাকত, তাহলে সেই বাগ সাধারণত নির্দিষ্ট এজ-কেসে দুই পদ্ধতির ফলাফল আলাদা করে দিত। এটিই ঠিক M5/L24-এর PDA-CFG ইকুইভ্যালেন্স থিওরেমের চেতনা — দুটি ভিন্ন ফরমালিজম একই ভাষা ভিন্নভাবে বর্ণনা করে, আর তাদের একমত হওয়াই সেই ইকুইভ্যালেন্সের একটি concrete প্রমাণ।

প্র ০৩ এই কোর্সের শুরুতে (M1/L01) আমরা "গণনাশক্তির সিঁড়ি" দেখেছিলাম। এই ক্যাপস্টোন কীভাবে সেই একই সিঁড়িকে ভিন্নভাবে প্রকাশ করে?

M1/L01-এ সিঁড়িটি ছিল বিমূর্ত — DFA থেকে PDA থেকে TM পর্যন্ত একটি তাত্ত্বিক মানচিত্র। এই ক্যাপস্টোনে সেই একই সিঁড়ি কংক্রিট, চলমান কোডে প্রকাশ পেয়েছে — একই তিনটি স্তর ($L_1$ রেগুলার, $L_2$ কনটেক্সট-ফ্রি, $L_3$ Type 0), একই তিনটি মেশিন, কিন্তু এবার সত্যিকারের ইনপুটে চালিয়ে, প্রকৃত ধাপ-সংখ্যা পরিমাপ করে, এবং প্রতিটি ফলাফল স্বাধীনভাবে যাচাই করে। এটিই এই পুরো কোর্সের চূড়ান্ত পয়েন্ট — তত্ত্ব ও বাস্তবায়ন একে অপরের সাথে সম্পূর্ণ সামঞ্জস্যপূর্ণ, শুরু থেকে শেষ পর্যন্ত।

অনুশীলন

  1. চিন্তা করুন: TOCToolkit-এ একটি চতুর্থ মডিউল যোগ করতে চাইলে (যেমন M9/L39-এর হল্টিং প্রবলেম ডেমো), কমপ্লেক্সিটি-সচেতনতা টেবিলে সেই মডিউলের "ধাপ-সংখ্যা" পরিমাপ করা কেন মৌলিকভাবে ভিন্ন চ্যালেঞ্জ হবে?

    কারণ হল্টিং প্রবলেম আনডিসাইডেবল (M9/L39) — এমন কোনো অ্যালগরিদম নেই যা সাধারণভাবে সব প্রোগ্রামের জন্য "থামবে কি না" নির্ণয় করতে পারে, তাই কোনো "ধাপ-সংখ্যা" পরিমাপ করার প্রশ্নই ওঠে না একটি সাধারণ ডিসাইডার হিসেবে। DFA/CYK/TM মডিউলগুলো প্রতিটিই একটি নির্দিষ্ট, ডিসাইডেবল ভাষার জন্য — তাই সবসময় থামে এবং ধাপ-সংখ্যা পরিমাপযোগ্য। একটি হল্টিং-প্রবলেম ডেমো শুধুমাত্র একটি নির্দিষ্ট, ছোট উদাহরণে ডায়াগোনালাইজেশন কনস্ট্রাকশন দেখাতে পারত (M9/L39-এর মতো), কোনো সাধারণ "সমাধান" হিসেবে নয়।

  2. পরীক্ষা করুন: সংশ্লেষিত hierarchy-রিপোর্ট কোড সেলে report তালিকায় $L_3$-এর test তালিকায় "aaabbbccc" যোগ করে Run চেপে দেখুন এটি সঠিকভাবে accept হয় কি না।

    "aaabbbccc"-এ ৩টি করে 'a', 'b', 'c' আছে, সঠিক ব্লক-ক্রমে ($a^3b^3c^3$) — তাই tm_accepts এটি accept করার কথা। যেহেতু $L_3 = a^nb^nc^n$-এর সংজ্ঞা এই স্ট্রিং-এর সাথে সরাসরি মেলে ($n=3$), এবং আমরা আগের মডিউল-৩ কোড সেলে "aaabbbccc"-কে ঠিক এই কারণেই accept হতে দেখেছিলাম, ফলাফল প্রত্যাশিত ও সামঞ্জস্যপূর্ণ থাকবে।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ DFA/NFA থেকে টুরিং মেশিন, ডিসাইডেবিলিটি ও P বনাম NP পর্যন্ত — সম্পূর্ণ কোর্স, শুরু থেকে শেষ পর্যন্ত।
  • Software Engineering & Git কোর্স সঙ্গী কোর্স এই কোর্সের তাত্ত্বিক ভিত্তি — টেস্টিং, কোড-রিভিউ, ও প্রোভেবলি-অসম্পূর্ণ স্ট্যাটিক-অ্যানালাইসিসের ব্যবহারিক প্রয়োগ সেই কোর্সে।
  • Discrete Mathematics কোর্স সহোদর কোর্স সেট থিওরি, লজিক ও ইনডাকশন প্রুফ — এই সম্পূর্ণ কোর্সের প্রতিটি ফরমাল প্রমাণের ভিত্তি সেই কোর্সেই তৈরি হয়েছিল।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git ও Theory of Computation — সব এক জায়গায়।
আগের পাঠ
কেস স্টাডি: বিখ্যাত আনডিসাইডেবল ও NP-কমপ্লিট প্রবলেম