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

টুরিং মেশিন ডিজাইন করা — worked examples

Designing Turing machines — worked examples
৯ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • টেপকে "স্ক্র্যাচ স্পেস" হিসেবে ব্যবহারের কৌশল — মার্ক করা/অচিহ্নিত সিম্বল-ভ্যারিয়েন্ট দিয়ে
  • $\{ww\}$ ভাষার জন্য মিডপয়েন্ট খোঁজা ও দুই অর্ধেক মেলানোর সম্পূর্ণ ডিজাইন
  • বাইনারি ইনক্রিমেন্ট TM — স্ক্যান-রাইট-দেন-ক্যারি-প্রোপাগেট-লেফট ডিজাইন প্যাটার্ন
  • কেন টেপ "ইনপুটের দৈর্ঘ্যের চেয়ে বেশি" বাড়তে পারা এই দুটি ডিজাইনের জন্যই অপরিহার্য

১ · উদাহরণ ১ — $\{ww : w \in \{0,1\}^*\}$

এই ভাষার প্রতিটি স্ট্রিং হলো কোনো স্ট্রিং $w$-এর ঠিক দুইবার পুনরাবৃত্তি (যেমন "0101", যেখানে $w = $ "01")। L32-এর $\{0^n1^n\}$-এর মতো সরাসরি বাম-ডান ক্রসিং-অফ এখানে কাজ করে না, কারণ আগে থেকে জানা নেই কোথায় প্রথমার্ধ শেষ হয়ে দ্বিতীয়ার্ধ শুরু হচ্ছে — প্রথমেই সেই মিডপয়েন্ট বের করতে হয়।

ডিজাইন আইডিয়া (দুই ধাপে): প্রথম ধাপে, বাম প্রান্ত থেকে leftmost অচিহ্নিত সিম্বলকে a0/a1 (মূল মান-সহ) দিয়ে চিহ্নিত করা হয়, আর একই সাথে ডান প্রান্ত থেকে rightmost অচিহ্নিত সিম্বলকে b0/b1 দিয়ে চিহ্নিত করা হয় — এই দুই দিক থেকে "ক্রলিং" চালিয়ে যাওয়া হয় যতক্ষণ না বাম দিকের সার্চ সরাসরি একটি b-চিহ্নিত সেলে ধাক্কা খায় — তখনই মিডপয়েন্ট পাওয়া যায় (দৈর্ঘ্য জোড়; বিজোড় হলে দুই সার্চ একই অচিহ্নিত সেলে মিলে যাবে, তখন reject)। দ্বিতীয় ধাপে, position $i$-এর a-ট্যাগ আর position $mid+i$-এর b-ট্যাগ একে একে মিলিয়ে দেখা হয়।

ধাপ ১ — মিডপয়েন্ট খোঁজা বাম থেকে a-ট্যাগ, ডান থেকে b-ট্যাগ a আর b মিলে গেলে মিডপয়েন্ট পাওয়া গেল ধাপ ২ — অর্ধেক মেলানো a[i] বনাম b[i] একে একে তুলনা সব মিললে accept, একটি না মিললে reject "0101" -> a0 a1 b0 b1 -> a[0]=0=b[0], a[1]=1=b[1] -> accept "0110" -> মিডপয়েন্ট a0 a1 | b1 b0 -> a[0]=0≠1=b[0] -> reject
প্রথমে টেপ স্ক্যান করে মিডপয়েন্ট বের করা হয়, তারপর সেই একই টেপকে স্ক্র্যাচ-স্পেস হিসেবে ব্যবহার করে দুই অর্ধেক মেলানো হয়।

২ · কোড — $\{ww\}$-এর জন্য সম্পূর্ণ TM

নিচের কোডে a0/a1 (বাম-অর্ধের ট্যাগ, মূল মান-সহ), b0/b1 (ডান-অর্ধের ট্যাগ) এবং X0/X1 (দ্বিতীয় ধাপে ইতিমধ্যে মিলে-যাওয়া সিম্বল) — এই অতিরিক্ত টেপ-সিম্বলগুলো ($\Gamma$-তে আছে, $\Sigma=\{0,1\}$-এ নেই) দিয়ে সত্যিকারের স্ক্র্যাচ-স্পেস ব্যবহার প্রদর্শিত হয়েছে।

Python
B = '_'

class TM:
    def __init__(self, trans, start, accept, reject):
        self.trans, self.start = trans, start
        self.accept, self.reject = accept, reject

    def run(self, s, max_steps=50000):
        tape = {i: c for i, c in enumerate(s)}
        head, state, steps = 0, self.start, 0
        while steps < max_steps:
            if state == self.accept:
                return "accept", steps
            if state == self.reject:
                return "reject", steps
            sym = tape.get(head, B)
            key = (state, sym)
            if key not in self.trans:
                return "reject", steps
            ns, w, mv = self.trans[key]
            tape[head] = w
            head += 1 if mv == 'R' else -1
            state = ns
            steps += 1
        return "timeout", steps

def build_ww_tm():
    t = {}
    t[('find_left', B)] = ('accept', B, 'R')          # খালি স্ট্রিং -- accept

    # ধাপ ১: leftmost অচিহ্নিত সিম্বলকে a-ট্যাগ দাও, তারপর ডানপ্রান্তে গিয়ে
    # rightmost অচিহ্নিত সিম্বলকে b-ট্যাগ দাও -- এভাবে দুই দিক থেকে এগোতে থাকো
    t[('find_left', '0')] = ('seek_right', 'a0', 'R')
    t[('find_left', '1')] = ('seek_right', 'a1', 'R')
    t[('find_left', 'a0')] = ('find_left', 'a0', 'R')
    t[('find_left', 'a1')] = ('find_left', 'a1', 'R')
    t[('find_left', 'b0')] = ('phase2_start', 'b0', 'L')  # a আর b মিলে গেল -- মিডপয়েন্ট!
    t[('find_left', 'b1')] = ('phase2_start', 'b1', 'L')

    for sym in ('0', '1', 'a0', 'a1', 'b0', 'b1'):
        t[('seek_right', sym)] = ('seek_right', sym, 'R')
    t[('seek_right', B)] = ('find_right', B, 'L')

    t[('find_right', 'b0')] = ('find_right', 'b0', 'L')
    t[('find_right', 'b1')] = ('find_right', 'b1', 'L')
    t[('find_right', '0')] = ('back_to_left', 'b0', 'L')
    t[('find_right', '1')] = ('back_to_left', 'b1', 'L')
    t[('find_right', 'a0')] = ('reject', 'a0', 'L')      # একটিমাত্র মাঝের সেল -- বিজোড় দৈর্ঘ্য
    t[('find_right', 'a1')] = ('reject', 'a1', 'L')

    for sym in ('0', '1', 'b0', 'b1'):
        t[('back_to_left', sym)] = ('back_to_left', sym, 'L')
    t[('back_to_left', 'a0')] = ('find_left', 'a0', 'R')
    t[('back_to_left', 'a1')] = ('find_left', 'a1', 'R')

    # ধাপ ২: বাম প্রান্তে ফিরে গিয়ে a[i] বনাম b[i] একে একে মেলাও
    t[('phase2_start', 'a0')] = ('phase2_start', 'a0', 'L')
    t[('phase2_start', 'a1')] = ('phase2_start', 'a1', 'L')
    t[('phase2_start', B)] = ('cmp_find_a', B, 'R')

    t[('cmp_find_a', 'X0')] = ('cmp_find_a', 'X0', 'R')
    t[('cmp_find_a', 'X1')] = ('cmp_find_a', 'X1', 'R')
    t[('cmp_find_a', 'a0')] = ('cmp_seek_b0', 'X0', 'R')
    t[('cmp_find_a', 'a1')] = ('cmp_seek_b1', 'X1', 'R')
    t[('cmp_find_a', B)] = ('accept', B, 'R')            # সব জোড়া মিলে গেছে -- accept

    for v, other in (('0', '1'), ('1', '0')):
        st = 'cmp_seek_b' + v
        t[(st, 'a0')] = (st, 'a0', 'R')
        t[(st, 'a1')] = (st, 'a1', 'R')
        t[(st, 'X0')] = (st, 'X0', 'R')
        t[(st, 'X1')] = (st, 'X1', 'R')
        t[(st, 'b' + v)] = ('cmp_back', 'X' + v, 'L')     # মিলেছে
        t[(st, 'b' + other)] = ('reject', 'b' + other, 'R')  # মেলেনি

    for sym in ('a0', 'a1', 'X0', 'X1', 'b0', 'b1'):
        t[('cmp_back', sym)] = ('cmp_back', sym, 'L')
    t[('cmp_back', B)] = ('cmp_find_a', B, 'R')

    return TM(t, 'find_left', 'accept', 'reject')

tm = build_ww_tm()
tests = ["0101", "1111", "", "0110", "010"]
for s in tests:
    result, steps = tm.run(s)
    w_repr = s[:len(s)//2] if len(s) % 2 == 0 else "N/A"
    print(f"{s!r:8s} -> {result:8s} (ধাপ {steps:3d}) | w={w_repr!r}")

    
হাতে-যাচাই: "0101" ($w=$"01") ও "1111" ($w=$"11") accept হয়; "" ($w=$"") accept হয়; "0110" জোড় দৈর্ঘ্যের হলেও প্রথমার্ধ "01" ≠ দ্বিতীয়ার্ধ "10" বলে reject হয়; "010" বিজোড় দৈর্ঘ্যের বলে কখনোই $ww$ আকারে লেখা যায় না, তাই reject হয় — সবগুলোই এই কোড চালিয়ে সরাসরি নিশ্চিত করা হয়েছে।

৩ · উদাহরণ ২ — বাইনারি ইনক্রিমেন্ট (TM কে "সাধারণ কম্পিউটার" হিসেবে)

দ্বিতীয় উদাহরণটি দেখায় একটি TM কীভাবে সাধারণ গাণিতিক গণনাও করতে পারে — টেপে বাইনারি সংখ্যায় ১ যোগ করা। ডিজাইন: রাইটমোস্ট ডিজিট পর্যন্ত স্ক্যান করো, তারপর ক্যারি-সহ বামে যোগ করতে থাকো — 0+1=1 (ক্যারি থামে), 1+1=10 (লেখো 0, ক্যারি বামে চালিয়ে যাও)। এটি L37-এর চার্চ-টুরিং থিসিসের সরাসরি ফোরশ্যাডো — "যেকোনো কার্যকরভাবে গণনাযোগ্য ফাংশন কোনো-না-কোনো TM দিয়ে গণনা করা যায়"।

Python
def build_increment_tm():
    t = {}
    t[('scan_right', '0')] = ('scan_right', '0', 'R')
    t[('scan_right', '1')] = ('scan_right', '1', 'R')
    t[('scan_right', B)] = ('carry', B, 'L')      # রাইটমোস্ট ডিজিটের পরে ব্ল্যাংক পেলে ক্যারি শুরু

    t[('carry', '0')] = ('done', '1', 'L')        # 0+1=1 -- ক্যারি থেমে গেল
    t[('carry', '1')] = ('carry', '0', 'L')       # 1+1=10 -- 0 লেখো, ক্যারি বামে চালিয়ে যাও
    t[('carry', B)] = ('done', '1', 'L')          # সব ডিজিট 1 ছিল -- বাম প্রান্তে নতুন 1 বসাও (টেপ বাড়ল!)

    t[('done', '0')] = ('done', '0', 'L')
    t[('done', '1')] = ('done', '1', 'L')
    t[('done', B)] = ('accept', B, 'R')

    return TM(t, 'scan_right', 'accept', 'reject')

def read_tape(tm, s):
    tape = {i: c for i, c in enumerate(s)}
    head, state, steps = 0, tm.start, 0
    while steps < 5000 and state != tm.accept:
        sym = tape.get(head, B)
        ns, w, mv = tm.trans[(state, sym)]
        tape[head] = w
        head += 1 if mv == 'R' else -1
        state = ns
        steps += 1
    lo, hi = min(tape), max(tape)
    return ''.join(tape.get(i, B) for i in range(lo, hi + 1)).strip(B), steps

incr = build_increment_tm()
for s in ["011", "111", "0", "1", "1011", "1111"]:
    out, steps = read_tape(incr, s)
    print(f"{s!r:6s} (={int(s,2):2d}) -> {out!r:6s} (={int(out,2):2d})  ধাপ={steps}  "
          f"টেপ {'বাড়ল' if len(out) > len(s) else 'বাড়েনি'}")

    
হাতে-যাচাই: "011" (৩) → "100" (৪), এবং "111" (৭) → "1000" (৮) — দ্বিতীয় ক্ষেত্রে টেপ ৩ সেল থেকে ৪ সেলে সত্যিকারেই বেড়ে গেছে, কারণ সবগুলো ডিজিটই 1 ছিল বলে ক্যারি বাম প্রান্ত পর্যন্ত পৌঁছে একটি সম্পূর্ণ নতুন ডিজিট যোগ করেছে। এই আচরণ L29-এর বাউন্ডেড-টেপ LBA-র পক্ষে কখনোই সম্ভব নয় — LBA-র টেপ ইনপুটের দৈর্ঘ্যেই আটকে থাকে, TM-এর টেপ প্রয়োজনমতো বাড়তে পারে (L32)।
মূল কথা · Key takeaway

দুটি উদাহরণই দেখায় কীভাবে টেপকে শুধু "ইনপুট রাখার জায়গা" নয় বরং একটি সক্রিয় স্ক্র্যাচ স্পেস হিসেবে ব্যবহার করা যায় — মার্ক-করা সিম্বল-ভ্যারিয়েন্ট দিয়ে (ww), অথবা টেপকে বাড়িয়ে (ইনক্রিমেন্ট)। এই দুই কৌশলই M8-এর পরের পাঠগুলোতে (মাল্টি-টেপ, নন-ডিটারমিনিজম) আরও শক্তিশালী রূপে ফিরে আসবে।

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

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

প্র ০১ $\{ww\}$ TM-এ কেন সরাসরি "প্রথম অর্ধেকের দৈর্ঘ্য একটি ভ্যারিয়েবলে গুণে রাখা" যায় না?

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

প্র ০২ বাইনারি ইনক্রিমেন্ট TM-এ ('carry', B) ট্রানজিশনটি ঠিক কখন ট্রিগার হয়, এবং কেন সেটিই একমাত্র জায়গা যেখানে টেপ বাড়ে?

('carry', B) ট্রিগার হয় যখন ক্যারি বহন করতে করতে হেড বাম প্রান্তের ব্ল্যাংক পর্যন্ত পৌঁছে যায় — অর্থাৎ ইনপুটের প্রতিটি ডিজিটই 1 ছিল (যেমন "111")। এই একমাত্র ক্ষেত্রেই একটি সম্পূর্ণ নতুন ডিজিট (একটি নতুন 1) বসাতে হয়, কারণ বিদ্যমান কোনো ডিজিটেই ক্যারি "শোষিত" হতে পারেনি। অন্য যেকোনো ক্ষেত্রে (কোনো 0 পাওয়া গেলে) ক্যারি সেখানেই থেমে যায় এবং টেপের দৈর্ঘ্য অপরিবর্তিত থাকে।

প্র ০৩ $\{ww\}$-এর ধাপ ১-এ "বাম সার্চ সরাসরি b-ট্যাগে ধাক্কা খায়" এই শর্তটি ছাড়া, কীভাবে মেশিন ভুলভাবে একটি বিজোড়-দৈর্ঘ্যের স্ট্রিংকে accept করে ফেলতে পারত?

যদি এই দুটি সংঘর্ষ-সনাক্তকরণ শর্ত (বাম সার্চ সরাসরি b-তে ধাক্কা = জোড় দৈর্ঘ্য, বনাম ডান সার্চ সরাসরি a-তে ধাক্কা = বিজোড় দৈর্ঘ্য) আলাদা করে চেক না করা হতো, মেশিন হয়তো মাঝের অতিরিক্ত সেলটিকে ভুলভাবে অগ্রাহ্য করে দুই অর্ধেক মিলিয়ে ফেলত — যেমন "010"-এ মাঝের সেলটি (position 1) বাদ পড়ে গিয়ে "0" বনাম "0" মিলে গেছে ভেবে ভুলভাবে accept করে ফেলতে পারত। কোডে স্পষ্টভাবে ('find_right', 'a0'/'a1')-কে সরাসরি reject-এ পাঠিয়ে এই ভুলটি প্রতিরোধ করা হয়েছে।

অনুশীলন

  1. চিন্তা করুন: read_tape ফাংশনে tm.reject স্টেট চেক করা হয়নি — শুধু while ... state != tm.accept লুপ চলছে। ইনক্রিমেন্ট TM-এর ট্রানজিশন টেবিল দেখে বলুন, এই TM কি কখনো reject স্টেটে যেতে পারে? কেন বা কেন নয়?

    না, কখনো যেতে পারে না — ট্রানজিশন টেবিলে reject-এ যাওয়ার কোনো এন্ট্রিই নেই। scan_right, carry, ও done — প্রতিটি স্টেটেই সম্ভাব্য প্রতিটি সিম্বলের ('0', '1', ব্ল্যাংক) জন্য একটি সংজ্ঞায়িত ট্রানজিশন আছে, তাই কোনো "অসংজ্ঞায়িত ট্রানজিশন" পরিস্থিতিই ঘটে না। এটি স্বাভাবিক — ইনক্রিমেন্ট একটি টোটাল ফাংশন (যেকোনো বাইনারি সংখ্যার জন্যই সংজ্ঞায়িত), তাই এই TM-এর কখনো "reject করার" কোনো কারণই নেই।

  2. পরীক্ষা করুন: $\{ww\}$-এর টেস্টে "0000" ($w=$"00") যোগ করে Run চাপুন — এটি accept হবে কি? আর "0001" (জোড় দৈর্ঘ্য, কিন্তু $ww$ নয়) দিয়ে কী হয়?

    "0000" accept হবে, কারণ প্রথমার্ধ "00" = দ্বিতীয়ার্ধ "00"। "0001" reject হবে — যদিও দৈর্ঘ্য জোড় (৪), প্রথমার্ধ "00" ≠ দ্বিতীয়ার্ধ "01", তাই ধাপ ২-এর তুলনায় প্রথম মিসম্যাচেই (position 1: 0 বনাম 1) সরাসরি reject হয়ে যাবে।

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

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