পাঠ ৩৩ · ৫৬-এর মধ্যে · মডিউল ৮

টুরিং মেশিন রিকগনাইজার ও ডিসাইডার হিসেবে

Turing machines as recognizers & deciders
৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • রিকগনাইজার ও ডিসাইডারের ফরমাল সংজ্ঞা এবং তাদের মধ্যকার একমুখী (asymmetric) সম্পর্ক
  • টুরিং-রিকগনাইজেবল (recursively enumerable) এবং ডিসাইডেবল (recursive) — এই দুটি টার্মের অর্থ
  • কেন "প্রতিটি ডিসাইডার একটি রিকগনাইজার" সত্য, কিন্তু বিপরীতটি সাধারণভাবে মিথ্যা
  • বাউন্ডেড-সার্চ রিকগনাইজার বনাম সবসময়-হল্টিং ডিসাইডার — একটি সরাসরি কোড-ভিত্তিক বৈসাদৃশ্য

১ · রিকগনাইজার — "হ্যাঁ হলে বলে, না হলে হয়তো চুপ থাকে"

L32-এ দেখা তৃতীয় হল্টিং সম্ভাবনা (চিরকাল লুপে চলা) থেকেই এই পাঠের কেন্দ্রীয় পার্থক্যটি আসে। একটি TM $M$ ভাষা $L$ রিকগনাইজ করে যদি —

  • $w \in L$ হলে $M$ অবশ্যই $w$-কে accept করে, এবং
  • $w \notin L$ হলে $M$ হয় $w$-কে reject করে, নয়তো $w$-তে চিরকাল চলতে থাকে (কখনো হল্ট না করেও পারে)।

এমন ভাষাকে টুরিং-রিকগনাইজেবল (aka recursively enumerable) বলা হয়। লক্ষ্য করুন — রিকগনাইজারের সংজ্ঞা $L$-এর বাইরের স্ট্রিং নিয়ে কোনো গ্যারান্টি দেয় না; "না" উত্তরের ক্ষেত্রে মেশিন চিরকাল চলতেই থাকতে পারে।

২ · ডিসাইডার — "সবসময় হল্ট করে হ্যাঁ/না বলে"

একটি TM $M$ ভাষা $L$ ডিসাইড করে যদি $M$ $L$-কে রিকগনাইজ করে এবং প্রতিটি সম্ভাব্য ইনপুটে হল্ট করে — $w \in L$ হলে accept, $w \notin L$ হলে (লুপ নয়, বরং সত্যিকারের) reject। এমন ভাষাকে ডিসাইডেবল (aka recursive) বলা হয়। L32-এর $\{0^n1^n\}$ TM-টি ঠিক এই ধরনের একটি ডিসাইডার — সেটি প্রতিটি ইনপুটেই finite ধাপে accept বা reject-এ পৌঁছায়।

টুরিং-রিকগনাইজেবল (recursively enumerable) ডিসাইডেবল (recursive) সবসময় হল্ট করে accept অথবা reject (L32-এর 0ⁿ1ⁿ TM এখানে) রিকগনাইজেবল, ডিসাইডেবল নয় — non-member-এ লুপ করতে পারে (M9)
প্রতিটি ডিসাইডেবল ভাষা টুরিং-রিকগনাইজেবলও বটে — কিন্তু ভেতরের বৃত্তের বাইরে, বাইরের বৃত্তের ভেতরেও ভাষা থাকতে পারে, যেগুলো রিকগনাইজেবল অথচ কখনো ডিসাইড করা যায় না।
গুরুত্বপূর্ণ অসাম্য (asymmetry)

প্রতিটি ডিসাইডেবল ভাষা টুরিং-রিকগনাইজেবল — একটি ডিসাইডার নিজেই একটি রিকগনাইজার, শুধু অতিরিক্ত গ্যারান্টি সহ যে এটি সবসময় হল্ট করে। কিন্তু উল্টোটা মিথ্যা — কিছু ভাষা রিকগনাইজেবল, অথচ ডিসাইডেবল নয়: তাদের জন্য একটি রিকগনাইজার তৈরি করা সম্ভব, কিন্তু সেই রিকগনাইজারকে অন্তত কিছু non-member ইনপুটে চিরকাল লুপ করতেই হয় — কোনোভাবেই সেটিকে একটি ডিসাইডারে রূপান্তর করা যায় না। M9/L39-এর হল্টিং প্রবলেম হবে ঠিক এই ফাঁকের সবচেয়ে বিখ্যাত উদাহরণ।

৩ · কোড — বাউন্ডেড রিকগনাইজার বনাম গ্যারান্টিড ডিসাইডার

নিচের কোডে দুটি DFA-র (M2 স্টাইল) ভাষার intersection non-empty কিনা — এই প্রশ্নের দুটি ভিন্ন সমাধান তুলনা করা হয়েছে। প্রথমটি একটি রিকগনাইজার-স্টাইল সার্চ — ক্রমবর্ধমান দৈর্ঘ্যের স্ট্রিং খুঁজে দেখে, কোনো একটি উভয় DFA-তেই accept হলে সাথে সাথে থেমে যায় (হ্যাঁ পাওয়া গেলে হল্ট করে) — কিন্তু যদি এমন কোনো স্ট্রিং না থাকে, তত্ত্বগতভাবে এটি চিরকাল খুঁজতেই থাকবে (এই স্যান্ডবক্সে একটি max_length বাউন্ড দিয়ে নিয়ন্ত্রিত-ভাবে সেই "না হলে লুপ" আচরণটি দেখানো হয়েছে)। দ্বিতীয়টি M3/L12-এর প্রোডাক্ট-কনস্ট্রাকশন পুনর্ব্যবহার করে একটি সত্যিকারের ডিসাইডার — যা রেগুলার ভাষার জন্য সবসময় নির্দিষ্ট হ্যাঁ/না উত্তর দিয়ে হল্ট করে।

Python
import itertools

ALPHABET = ['0', '1']

class DFA:
    def __init__(self, states, alphabet, trans, start, accept):
        self.states, self.alphabet = states, alphabet
        self.trans, self.start, self.accept = trans, start, accept

    def accepts(self, s):
        state = self.start
        for ch in s:
            state = self.trans[(state, ch)]
        return state in self.accept

def dfa_contains_11():           # L(A) = "1" এর পরপর দুটি আছে
    trans = {('q0','0'):'q0', ('q0','1'):'q1',
             ('q1','0'):'q0', ('q1','1'):'q2',
             ('q2','0'):'q2', ('q2','1'):'q2'}
    return DFA({'q0','q1','q2'}, ALPHABET, trans, 'q0', {'q2'})

def dfa_even_length():           # L(B) = জোড় দৈর্ঘ্যের স্ট্রিং
    trans = {('e','0'):'o', ('e','1'):'o', ('o','0'):'e', ('o','1'):'e'}
    return DFA({'e','o'}, ALPHABET, trans, 'e', {'e'})

# --- রিকগনাইজার-স্টাইল: bounded search, হ্যাঁ পেলে সাথে সাথে হল্ট করে ---
def recognizer_for_nonempty_intersection(dfa_a, dfa_b, max_length=6):
    checked = 0
    for length in range(max_length + 1):
        for bits in itertools.product(ALPHABET, repeat=length):
            checked += 1
            s = ''.join(bits)
            if dfa_a.accepts(s) and dfa_b.accepts(s):
                return True, s, checked          # witness পেয়ে গেলে সাথে সাথে accept/halt
    return None, None, checked                  # সত্যিকারের মডেলে: চিরকাল খুঁজতেই থাকত

# --- ডিসাইডার-স্টাইল (M3/L12-এর product construction পুনর্ব্যবহার): সবসময় হল্ট করে ---
def product_dfa_intersection(dfa_a, dfa_b):
    states = {(p, q) for p in dfa_a.states for q in dfa_b.states}
    trans = {}
    for p in dfa_a.states:
        for q in dfa_b.states:
            for a in ALPHABET:
                trans[((p, q), a)] = (dfa_a.trans[(p, a)], dfa_b.trans[(q, a)])
    accept = {(p, q) for p in dfa_a.accept for q in dfa_b.accept}
    return DFA(states, ALPHABET, trans, (dfa_a.start, dfa_b.start), accept)

def is_empty(dfa):                       # BFS reachability -- সবসময় হল্ট করে
    seen, frontier, visited = {dfa.start}, [dfa.start], 0
    while frontier:
        state = frontier.pop()
        visited += 1
        if state in dfa.accept:
            return False, visited
        for a in dfa.alphabet:
            nxt = dfa.trans[(state, a)]
            if nxt not in seen:
                seen.add(nxt); frontier.append(nxt)
    return True, visited

A, Bd = dfa_contains_11(), dfa_even_length()

found, witness, checked = recognizer_for_nonempty_intersection(A, Bd)
print("রিকগনাইজার:", found, "witness =", repr(witness), "| checked", checked, "স্ট্রিং")

prod = product_dfa_intersection(A, Bd)
empty, visited = is_empty(prod)
print("ডিসাইডার is_empty:", empty, "| visited", visited, "states -- সবসময় হল্ট করে")

assert found is True and A.accepts(witness) and Bd.accepts(witness)
assert empty is False   # nonempty, রিকগনাইজারের ফলাফলের সাথে সামঞ্জস্যপূর্ণ

    
দুটি ফাংশনই এখানে একই প্রশ্নের সঠিক উত্তর দেয় (নন-এম্পটি intersection আছে, witness "11") — কিন্তু is_empty DFA/রেগুলার ভাষার জন্য সবসময় হল্ট করার গ্যারান্টি দেয় (M3/L12-এর decidability রেজাল্ট), যেখানে বাউন্ডেড-সার্চ রিকগনাইজারটি এই গ্যারান্টি দেয় না — শুধু "হ্যাঁ" উত্তরের ক্ষেত্রেই দ্রুত হল্ট করার গ্যারান্টি আছে। এখানে DFA-ভিত্তিক উদাহরণেই এই পার্থক্য দেখানো হলো কারণ রেগুলার ভাষার জন্য সবসময় একটি ডিসাইডার পাওয়া যায় (M3) — টুরিং মেশিনের সাধারণ ক্ষেত্রে (M9) এই সৌভাগ্য সবসময় থাকে না।

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

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

প্র ০১ "প্রতিটি ডিসাইডার একটি রিকগনাইজার" — এই দাবিটি সরাসরি সংজ্ঞা থেকেই কেন সত্য?

ডিসাইডারের সংজ্ঞাই বলে: এটি $L$ রিকগনাইজ করে এবং সবসময় হল্ট করে। অর্থাৎ ডিসাইডার হওয়ার শর্তটি রিকগনাইজার হওয়ার শর্তের উপর একটি অতিরিক্ত গ্যারান্টি (সবসময়-হল্টিং) যোগ করে মাত্র — মূল "$w \in L \Rightarrow$ accept" শর্তটি অপরিবর্তিত থাকে। তাই যেকোনো ডিসাইডার স্বয়ংক্রিয়ভাবেই একটি রিকগনাইজারের সংজ্ঞাও পূরণ করে।

প্র ০২ উপরের কোডের recognizer_for_nonempty_intersection ফাংশনটি যদি এমন দুটি DFA পেত যাদের ভাষার intersection সত্যিই খালি, তাহলে "সত্যিকারের" (unbounded) মডেলে কী ঘটত?

এটি চিরকাল খুঁজতেই থাকত — প্রতিটি দৈর্ঘ্যের সব স্ট্রিং পরীক্ষা করে যাবে, কখনোই কোনো witness খুঁজে পাবে না (কারণ নেই), এবং যেহেতু কোনো "খালি" ঘোষণা করার শর্ত এই ডিজাইনে নেই, তাই এটি কখনো হল্ট করবে না। এই কোডে max_length শুধু স্যান্ডবক্সের ব্যবহারিক সীমা হিসেবে ব্যবহৃত হয়েছে — সেই বাউন্ড ছাড়া, এটি একটি প্রকৃত "রিকগনাইজার-কিন্তু-ডিসাইডার-নয়" আচরণ দেখাত।

প্র ০৩ এই পাঠের DFA-ভিত্তিক উদাহরণে সবসময় একটি ডিসাইডার পাওয়া যাচ্ছে (M3-এর কল্যাণে) — তাহলে টুরিং মেশিনের সাধারণ ক্ষেত্রে সমস্যাটা কোথায়?

রেগুলার ভাষার emptiness প্রশ্নটি decidable কারণ DFA-র finite অনেক স্টেট থাকে, আর reachability একটি সহজ, সবসময়-হল্টিং গ্রাফ-সার্চ (M3/L16)। কিন্তু সাধারণ টুরিং মেশিনের ক্ষেত্রে অনুরূপ প্রশ্ন ("এই TM কি কোনো স্ট্রিং accept করে?") জিজ্ঞেস করলে, TM-এর সম্ভাব্য কনফিগারেশনের সংখ্যা অসীম হতে পারে (L32) — তাই কোনো সাধারণ গ্রাফ-সার্চ কৌশল কাজ করে না, এবং প্রকৃতপক্ষে এই প্রশ্নটি undecidable প্রমাণিত হয় (M9-এ দেখা যাবে) — এটিই ঠিক কেন রেগুলার ভাষার জন্য যা সহজ, টুরিং মেশিনের সাধারণ ক্ষেত্রে তা মৌলিকভাবে অসম্ভব হয়ে যায়।

অনুশীলন

  1. চিন্তা করুন: L32-এর $\{0^n1^n\}$ TM-টি কি একটি ডিসাইডার নাকি শুধু একটি রিকগনাইজার? আপনার উত্তরের যুক্তি দিন (আগের পাঠের অনুশীলন ১-এর সাথে সংযোগ)।

    এটি একটি ডিসাইডার। L32-এ দেখা গিয়েছিল সেই মেশিনটি প্রতিটি ইনপুটেই — সদস্য হোক বা না হোক — finite ধাপে হয় accept নয় reject-এ পৌঁছায়, কখনো লুপে আটকায় না। যেহেতু এটি $L=\{0^n1^n\}$ রিকগনাইজও করে (সব সদস্য accept করে) এবং সবসময় হল্টও করে, এটি ডিসাইডারের ফরমাল সংজ্ঞা সম্পূর্ণভাবে পূরণ করে — তাই $\{0^n1^n\}$ একটি ডিসাইডেবল ভাষা।

  2. পরীক্ষা করুন: উপরের কোডে dfa_even_length()-এর বদলে একটি ডিসজয়েন্ট (কোনো common স্ট্রিং নেই এমন) DFA বসিয়ে (যেমন "বিজোড় দৈর্ঘ্য"-এর সাথে "জোড় দৈর্ঘ্য" DFA-এর intersection) চালিয়ে দেখুন is_empty ঠিকভাবে True রিটার্ন করে কিনা।

    হ্যাঁ — "জোড় দৈর্ঘ্য" ও "বিজোড় দৈর্ঘ্য" ভাষার intersection নিশ্চিতভাবেই খালি (কোনো স্ট্রিং একসাথে জোড় ও বিজোড় দৈর্ঘ্যের হতে পারে না), তাই product DFA-তে কোনো reachable accept state থাকবে না, আর is_empty সঠিকভাবে True রিটার্ন করবে — এবং, গুরুত্বপূর্ণভাবে, এটি এখনও guaranteed-হল্টিং BFS-এর মাধ্যমেই এই সিদ্ধান্তে পৌঁছাবে, বাউন্ডেড সার্চের কোনো প্রয়োজন ছাড়াই।

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

আগের পাঠ
টুরিং মেশিন — ফরমাল ডেফিনিশন