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

রিডাকশন ও আনডিসাইডেবিলিটি প্রুফ

Reductions & undecidability proofs
৯ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • রিডাকশনের ধারণা এবং "$A$ রিডিউস করে $B$-তে" নোটেশনের নির্ভুল অর্থ ও দিক
  • কেন একটি আনডিসাইডেবল ভাষা থেকে নতুন ভাষায় রিডাকশন, নতুন ভাষাকেও আনডিসাইডেবল প্রমাণ করে (দিকটা সহ)
  • $E_{TM}$-এর সম্পূর্ণ ওয়ার্কড উদাহরণ — HALT থেকে রিডাকশনের মাধ্যমে আনডিসাইডেবিলিটি প্রমাণ
  • $M_w$ কনস্ট্রাকশন — কীভাবে একটি নতুন মেশিন বানানো হয় যা তার নিজের ইনপুট ignore করে একটি ফিক্সড $w$-এর উপর $M$ সিমুলেট করে
  • Python দিয়ে একটি real, working build_Mw — যা L37-এর UniversalSimulator দিয়ে সরাসরি চালিয়ে verified হয়, জানা হল্টিং আচরণের (M,w) জোড়ায়

১ · রিডাকশন — একটি সাধারণ কৌশল

L39-এর ডায়াগোনালাইজেশন একটি নির্দিষ্ট, বিশেষভাবে-নির্মিত প্রমাণ ছিল। কিন্তু প্রতিটি নতুন আনডিসাইডেবল ভাষার জন্য নতুন করে ডায়াগোনালাইজেশন করা লাগে না — রিডাকশনReductionএকটি ভাষাকে ইতিমধ্যে-জানা একটি আনডিসাইডেবল ভাষার সাথে সম্পর্কিত করে আনডিসাইডেবল প্রমাণ করার কৌশল হলো সেই সাধারণ, পুনর্ব্যবহারযোগ্য কৌশল যা এই কোর্সের বাকি সব আনডিসাইডেবিলিটি প্রমাণে ব্যবহৃত হবে।

মূল ধারণাটা পরিষ্কার — নতুন একটি ভাষা $B$-কে আনডিসাইডেবল প্রমাণ করতে, দেখাতে হবে — যদি $B$-এর একটি decider থাকত, তাহলে সেই decider ব্যবহার করে HALT-এর (বা অন্য কোনো ইতিমধ্যে-জানা আনডিসাইডেবল ভাষার) একটি decider বানানো যেত। যেহেতু HALT আনডিসাইডেবল (L39-এ প্রমাণিত), এটি একটি সরাসরি কনট্রাডিকশন — তাই মূল অনুমান ("B-এর decider আছে") মিথ্যা — $B$-ও আনডিসাইডেবল।

নোটেশন — "$A$ রিডিউস করে $B$-তে" ($A \leq B$ লেখা হয় প্রায়ই) মানে — $B$ সমাধান করতে পারলে সেটা ব্যবহার করে $A$-ও সমাধান করা যেত। এখানে দিকটা নিয়ে একটি সাধারণ বিভ্রান্তি এড়ানো জরুরি — আমরা সবসময় জানা-আনডিসাইডেবল ভাষা ($A$ = HALT) থেকে নতুন ভাষায় ($B$) রিডিউস করি, উল্টো দিকে নয়। যদি $A$ আনডিসাইডেবল হয় এবং $A$ রিডিউস করে $B$-তে, তাহলে $B$-ও অবশ্যই আনডিসাইডেবল হতে বাধ্য।

সাবধান · দিকটা গুলিয়ে ফেলবেন না

"$B$-কে সমাধান করা যায় যদি $A$ সমাধান করা যায়" — এই দিকে রিডাকশন করলে কিছুই প্রমাণ হয় না ($B$ সহজ হলেও এটা সত্যি হতে পারে)। সঠিক দিক সবসময় — জানা-কঠিন সমস্যা ($A$)-কে নতুন সমস্যার ($B$) সমাধানকারী দিয়ে সমাধান করা যায় দেখানো — তাহলেই $B$ অন্তত ততটাই কঠিন প্রমাণিত হয় যতটা $A$।

২ · ওয়ার্কড উদাহরণ — $E_{TM}$ আনডিসাইডেবল

$$E_{TM} = \{\langle M \rangle : L(M) = \emptyset\}$$

অর্থাৎ — "এই TM $M$-এর ভাষা কি খালি (empty)?" — প্রমাণ, HALT থেকে রিডাকশনের মাধ্যমে —

  1. ধরা যাক $E_{TM}$-এর একটি decider $R$ আছে (কনট্রাডিকশনের জন্য)।
  2. ইনপুট $\langle M, w \rangle$ (HALT-এর একটি ইনস্ট্যান্স) দেওয়া হলে, একটি নতুন মেশিন $M_w$ বানাই — $M_w$ তার নিজের ইনপুট $x$ ignore করে, এবং সবসময় $M$-কে ফিক্সড স্ট্রিং $w$-এর উপর সিমুলেট করে — $M$ যদি $w$-এর উপর accept করে, $M_w$ তখন $x$ accept করে (যে $x$ই হোক না কেন)।
  3. লক্ষ্য করুন — $L(M_w)$ হয় $\Sigma^*$ (সব স্ট্রিং, যদি $M$, $w$-এর উপর হল্ট-accept করে), অথবা $\emptyset$ (খালি, যদি $M$, $w$-এর উপর হল্ট না করে বা reject করে)।
  4. এবার $\langle M_w \rangle$ কে assumed decider $R$-কে দিই — $R$ সঠিকভাবে বলবে $L(M_w)$ খালি কি না — যা ঠিক বলে দেয় $M$, $w$-এর উপর accept করে (হল্ট করে) কি না।
  5. এই পুরো প্রক্রিয়াটাই তাহলে HALT-এর একটি decider — কিন্তু L39 অনুযায়ী HALT আনডিসাইডেবল — কনট্রাডিকশন। তাই $E_{TM}$-এর decider $R$ থাকতে পারে না — $E_{TM}$ আনডিসাইডেবল। ∎
⟨M, w⟩ HALT-এর একটি ইনস্ট্যান্স build_Mw(M, w) ⟨M_w⟩ কনস্ট্রাক্ট করে R(⟨M_w⟩) assumed E_TM decider R-এর উত্তর = M, w-এর উপর হল্ট করে কি না অর্থাৎ R আসলে একটি HALT-decider -- কিন্তু L39 বলে তা অসম্ভব
যদি $E_{TM}$-এর একটি decider $R$ থাকত, তাহলে এই পাইপলাইনটাই HALT-এর একটি decider হয়ে যেত — L39-এর সাথে সরাসরি কনট্রাডিকশন।

৩ · কোড সেল — build_Mw এবং verified রিডাকশন কনস্ট্রাকশন

নিচের কোড সেলে build_Mw ফাংশনটি সত্যিই ⟨M_w⟩ কনস্ট্রাক্ট করে — $M_w$ তার নিজের ইনপুট $x$ সম্পূর্ণ ignore করে (নিরাপদে, টেপের অনেক বাঁ-দিকে সরে গিয়ে, যাতে $x$-এর এলাকা কখনো স্পর্শ না হয়), সেখানে $w$ লিখে, তারপর $M$-এর ট্রানজিশন টেবিলে প্রবেশ করে। এই কনস্ট্রাক্টেড $M_w$-কে L37-এর UniversalSimulator দিয়ে সত্যিই চালিয়ে যাচাই করা হচ্ছে — দুটো জানা $(M, w)$ জোড়ার জন্য (একটির জন্য $M$ accept করে, আরেকটির জন্য reject করে) — $L(M_w)$ সত্যিই দাবিকৃত "সব স্ট্রিং" বা "খালি" আচরণ দেখায় কি না।

Python
BLANK = '_'

class TuringMachine:
    """L32/L37-এর ধাঁচের একটি জেনেরিক TM।"""
    def __init__(self, transitions, start, accept, reject, blank=BLANK):
        self.transitions = transitions
        self.start, self.accept, self.reject, self.blank = start, accept, reject, blank

    def run(self, input_string, max_steps=3000):
        tape = {i: c for i, c in enumerate(input_string)}
        head, state, steps = 0, self.start, 0
        while state != self.accept and state != self.reject and steps < max_steps:
            symbol = tape.get(head, self.blank)
            key = (state, symbol)
            if key not in self.transitions:
                return 'reject', steps
            new_state, write_symbol, direction = self.transitions[key]
            tape[head] = write_symbol
            head += 1 if direction == 'R' else -1
            state = new_state
            steps += 1
        if state == self.accept:
            return 'accept', steps
        elif state == self.reject:
            return 'reject', steps
        else:
            return 'timeout', steps


# L37-এর M -- L = {0^n 1^n : n >= 0}
zn_transitions = {
    ('q_find0', 'X'): ('q_find0', 'X', 'R'), ('q_find0', 'Y'): ('q_find0', 'Y', 'R'),
    ('q_find0', '0'): ('q_goright', 'X', 'R'), ('q_find0', '1'): ('q_reject', '1', 'R'),
    ('q_find0', BLANK): ('q_accept', BLANK, 'R'),
    ('q_goright', '0'): ('q_goright', '0', 'R'), ('q_goright', '1'): ('q_goright', '1', 'R'),
    ('q_goright', 'X'): ('q_goright', 'X', 'R'), ('q_goright', 'Y'): ('q_goright', 'Y', 'R'),
    ('q_goright', BLANK): ('q_findlast', BLANK, 'L'),
    ('q_findlast', 'X'): ('q_findlast', 'X', 'L'), ('q_findlast', 'Y'): ('q_findlast', 'Y', 'L'),
    ('q_findlast', '1'): ('q_gohome', 'Y', 'L'), ('q_findlast', '0'): ('q_reject', '0', 'L'),
    ('q_findlast', BLANK): ('q_reject', BLANK, 'L'),
    ('q_gohome', '0'): ('q_gohome', '0', 'L'), ('q_gohome', '1'): ('q_gohome', '1', 'L'),
    ('q_gohome', 'X'): ('q_gohome', 'X', 'L'), ('q_gohome', 'Y'): ('q_gohome', 'Y', 'L'),
    ('q_gohome', BLANK): ('q_find0', BLANK, 'R'),
}
M = TuringMachine(zn_transitions, 'q_find0', 'q_accept', 'q_reject')


class TMEncoder:
    """L37-এর এনকোডার -- একটি TM-কে একটি স্ট্রিং-এ এনকোড করে।"""
    SEP_FIELD, SEP_RULE, SEP_PARTS, SEP_ARROW = ';;', '|', ',', '>'

    @staticmethod
    def encode(tm):
        parts = ['START=' + tm.start, 'ACCEPT=' + tm.accept,
                 'REJECT=' + tm.reject, 'BLANK=' + tm.blank]
        rules = []
        for (state, symbol), (ns, ws, d) in tm.transitions.items():
            lhs = TMEncoder.SEP_PARTS.join([state, symbol])
            rhs = TMEncoder.SEP_PARTS.join([ns, ws, d])
            rules.append(lhs + TMEncoder.SEP_ARROW + rhs)
        parts.append('TRANS=' + TMEncoder.SEP_RULE.join(rules))
        return TMEncoder.SEP_FIELD.join(parts)

    @staticmethod
    def decode(encoded):
        fields = dict(f.split('=', 1) for f in encoded.split(TMEncoder.SEP_FIELD))
        transitions = {}
        if fields['TRANS']:
            for rule in fields['TRANS'].split(TMEncoder.SEP_RULE):
                lhs, rhs = rule.split(TMEncoder.SEP_ARROW)
                state, symbol = lhs.split(TMEncoder.SEP_PARTS)
                ns, ws, d = rhs.split(TMEncoder.SEP_PARTS)
                transitions[(state, symbol)] = (ns, ws, d)
        return transitions, fields['START'], fields['ACCEPT'], fields['REJECT'], fields['BLANK']


class UniversalSimulator:
    """L37-এর UTM -- এনকোডেড TM ডিকোড করে জেনেরিকভাবে সিমুলেট করে।"""
    def __init__(self, encoded_tm):
        (self.transitions, self.start, self.accept,
         self.reject, self.blank) = TMEncoder.decode(encoded_tm)

    def run(self, input_string, max_steps=3000):
        tape = {i: c for i, c in enumerate(input_string)}
        head, state, steps = 0, self.start, 0
        while state != self.accept and state != self.reject and steps < max_steps:
            symbol = tape.get(head, self.blank)
            key = (state, symbol)
            if key not in self.transitions:
                return 'reject', steps
            ns, ws, d = self.transitions[key]
            tape[head] = ws
            head += 1 if d == 'R' else -1
            state = ns
            steps += 1
        if state == self.accept:
            return 'accept', steps
        elif state == self.reject:
            return 'reject', steps
        else:
            return 'timeout', steps


def build_Mw(m_encoded, w, buffer=60):
    """L40-এর রিডাকশন কনস্ট্রাকশন: ⟨M⟩ + ফিক্সড w নিয়ে ⟨M_w⟩ বানায় -- M_w তার নিজের
    ইনপুট x সম্পূর্ণ ignore করে (নিরাপদে টেপের অনেক বাঁ-দিকে সরে গিয়ে, x-এর এলাকা কখনো
    স্পর্শ না করে), সেখানে w লিখে M-কে সেই w-এর উপর চালায়।
    L(M_w) = সব স্ট্রিং (M যদি w-এর উপর accept করে), অথবা ফাঁকা (যদি না করে)।"""
    transitions, start, accept, reject, blank = TMEncoder.decode(m_encoded)
    new_transitions = dict(transitions)

    seek_len = buffer + max(len(w), 1)   # x-এর এলাকা এড়াতে যথেষ্ট বাঁ-দিকে সরে যাওয়া
    for i in range(seek_len):
        cur = f'mw_seek_{i}'
        nxt = f'mw_seek_{i+1}' if i + 1 < seek_len else 'mw_write_0'
        for sym in ('0', '1', blank):
            new_transitions[(cur, sym)] = (nxt, sym, 'L')   # x যা-ই থাকুক, স্পর্শ না করেই টপকে যাই

    if len(w) == 0:
        for sym in ('0', '1', blank):
            new_transitions[('mw_write_0', sym)] = ('mw_return_0', sym, 'R')
    else:
        for i, ch in enumerate(w):
            cur = f'mw_write_{i}'
            nxt = f'mw_write_{i+1}' if i + 1 < len(w) else 'mw_return_0'
            for sym in ('0', '1', blank):
                new_transitions[(cur, sym)] = (nxt, ch, 'R')   # w-এর ক্যারেক্টার একে একে লেখা

    return_len = max(len(w), 1)
    for i in range(return_len):
        cur = f'mw_return_{i}'
        nxt = f'mw_return_{i+1}' if i + 1 < return_len else start   # শেষে M-এর নিজের start-এ প্রবেশ
        for sym in ('0', '1', blank):
            new_transitions[(cur, sym)] = (nxt, sym, 'L')

    mw_tm = TuringMachine(new_transitions, 'mw_seek_0', accept, reject, blank)
    return TMEncoder.encode(mw_tm)


# --- যাচাই: দুটো (M, w) জোড়া -- একটির জন্য M(w) accept করে, আরেকটির জন্য reject করে ---
m_encoded = TMEncoder.encode(M)
known_cases = [("0011", "accept"), ("01001", "reject")]   # সরাসরি M চালিয়ে যাচাইযোগ্য
test_xs = ["", "0", "1111", "0101", "111000"]              # M_w-এর নিজস্ব ইনপুট -- ignored হওয়ার কথা

for w, expected in known_cases:
    direct, _ = M.run(w)
    assert direct == expected, "M(w)-এর সরাসরি ফলাফল প্রত্যাশার সাথে মেলেনি"
    mw_encoded = build_Mw(m_encoded, w)
    sim = UniversalSimulator(mw_encoded)
    print(f"M({w!r}) সরাসরি = {direct}  =>  M_w = build_Mw(M, {w!r})")
    results = []
    for x in test_xs:
        r, steps = sim.run(x, max_steps=3000)
        results.append(r)
        print(f"  M_w({x!r}) = {r} (steps={steps})")
    same = all(r == results[0] for r in results)
    matches_expected = all(r == expected for r in results)
    print(f"  সব x-এ একই ফলাফল (L(M_w) হয় সবকিছু, নয় ফাঁকা): {same}")
    print(f"  M(w)-এর সাথে মিলছে (L(M_w) সঠিক): {matches_expected}\n")
    assert same and matches_expected

print("রিডাকশন কনস্ট্রাকশন যাচাই সফল -- build_Mw ঠিক দাবিকৃত ভাষাই তৈরি করে।")

    
লক্ষ্য করুন build_Mw-এ M_w-এর সেটআপ ফেজ ($M$-এর states-এ কখনো নাম-সংঘর্ষ না করার জন্য mw_ প্রিফিক্স ব্যবহার করে) টেপের অনেক বাঁ-দিকে সরে গিয়ে $w$ লেখে, যাতে $M_w$-এর নিজের ইনপুট $x$ (যা পজিশন $0$ থেকে শুরু) কখনোই স্পর্শ না হয় — একটি সহজ, কিন্তু গুরুত্বপূর্ণ বাস্তবায়ন-বিস্তারিত যা কনস্ট্রাকশনটাকে সত্যিকারের কাজ করতে দেয়।
মূল কথা · Key takeaway

রিডাকশন হলো এই কোর্সের বাকি অংশের (এবং M10-M11-এর NP-completeness প্রমাণের) সবচেয়ে গুরুত্বপূর্ণ টেকনিক — একটি জানা-কঠিন সমস্যাকে নতুন সমস্যায় রূপান্তর করে দেখানো যে নতুনটাও কম-কঠিন হতে পারে না। $M_w$ কনস্ট্রাকশনটা L41-এর রাইসের থিওরেমেও ঠিক একইভাবে ফিরে আসবে — এটাই দেখাবে যে HALT ও $E_{TM}$ আসলে একটি অনেক বৃহত্তর প্যাটার্নের মাত্র দুটো উদাহরণ।

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

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

প্র ০১ "$A$ রিডিউস করে $B$-তে" মানে কী, এবং কেন এই দিকটাই গুরুত্বপূর্ণ?

এর মানে — যদি $B$ সমাধান করার একটি পদ্ধতি থাকে, সেই পদ্ধতি ব্যবহার করে $A$-ও সমাধান করা যায়। এই দিকটা গুরুত্বপূর্ণ কারণ আমরা এটা ব্যবহার করি জানা-আনডিসাইডেবল ($A$ = HALT) থেকে নতুন ভাষায় ($B$) রিডিউস করতে — যদি এটা সম্ভব হয়, তাহলে $B$-র decider থাকলে HALT-এরও decider থাকত, যা অসম্ভব (L39) — তাই $B$-ও আনডিসাইডেবল। উল্টো দিকে (নতুন থেকে জানায় রিডিউস) করলে কিছুই প্রমাণ হয় না, কারণ সহজ সমস্যাও কঠিন সমস্যার দিকে রিডিউস করতে পারে।

প্র ০২ $M_w$ কীভাবে তার নিজের ইনপুট ignore করে?

$M_w$ প্রথমে তার নিজের ইনপুট $x$ পড়ার বা তাতে লেখার চেষ্টাই করে না — বরং টেপের অনেক বাঁ-দিকে (একটি নিরাপদ বাফার-এলাকায়, যেখানে $x$ কখনো পৌঁছাতে পারে না) সরে গিয়ে সেখানে ফিক্সড স্ট্রিং $w$ লেখে, তারপর সেই এলাকা থেকেই $M$-এর নিজের ট্রানজিশন টেবিলে প্রবেশ করে। ফলে $M$-এর সিমুলেশন সম্পূর্ণভাবে $w$-এর উপর চলে, $x$-এর কোনো প্রভাবই থাকে না — কোড সেলে এটাই যাচাই করা হয়েছে, একাধিক ভিন্ন $x$-এ একই ফলাফল দেখিয়ে।

প্র ০৩ $E_{TM}$-কে আনডিসাইডেবল প্রমাণ করা, HALT-কে প্রমাণ করার চেয়ে আলাদা কৌশল কেন ব্যবহার করে?

HALT-এর প্রমাণ (L39) সরাসরি ডায়াগোনালাইজেশন/স্ব-নির্দেশনা ব্যবহার করেছিল — একটি নতুন মেশিন $D$ যে নিজের সম্পর্কে প্রশ্ন করে। কিন্তু $E_{TM}$-এর প্রমাণে কোনো স্ব-নির্দেশনা নেই — বরং এটি HALT-কে একটি "কালো বাক্স" (ব্ল্যাক-বক্স, ইতিমধ্যে আনডিসাইডেবল প্রমাণিত) হিসেবে ধরে নিয়ে, তার সাথে একটি রূপান্তর (build_Mw) সম্পর্কিত করে। এটাই রিডাকশনের শক্তি — একবার একটা ভাষা (HALT) কঠিনভাবে প্রমাণ করা হয়ে গেলে, বাকি সব প্রমাণ শুধু সেই একটার সাথে "সংযোগ" তৈরি করেই সম্পন্ন করা যায়, নতুন করে ডায়াগোনালাইজেশন লাগে না।

অনুশীলন

  1. চিন্তা করুন: কেউ যদি ভুলভাবে দাবি করে "M-এর ঠিক ৫টি state আছে কি না" — এই প্রশ্নও build_Mw-স্টাইল রিডাকশন ব্যবহার করে আনডিসাইডেবল প্রমাণ করা যাবে — এই দাবিটা কি সঠিক? কেন বা কেন নয়?

    না, এই দাবিটা ভুল। "M-এর ঠিক ৫টি state আছে" প্রশ্নটা $M$-এর নিজস্ব বর্ণনা (কতগুলো state সে ব্যবহার করে) সম্পর্কে, $L(M)$ (M যে ভাষা recognize করে) সম্পর্কে নয় — এটা $M$-এর এনকোডিং সরাসরি পড়েই (state গুলো গুনে) নির্ণয় করা যায়, কোনো সিমুলেশন ছাড়াই। এটা $E_{TM}$-এর মতো ভাষা-নির্ভর প্রশ্ন নয়, তাই HALT থেকে এভাবে রিডিউস করা যাবে না — বরং এটি সহজেই ডিসাইডেবল, শুধু state সংখ্যা গোনার মাধ্যমে। L41-এ এই পার্থক্যটাই (ভাষা-প্রপার্টি বনাম implementation-প্রপার্টি) আরও নির্ভুলভাবে সংজ্ঞায়িত হবে।

  2. পরীক্ষা করুন: কোড সেলে known_cases-এ আপনার নিজের একটি নতুন $(M, w)$ জোড়া (যেমন ("000111", "accept")) যোগ করে Run চেপে দেখুন — build_Mw-এর তৈরি $M_w$ সত্যিই প্রতিটি $x$-এ প্রত্যাশিত আচরণ দেখায় কি না।

    "000111"-এ তিনটি 0 ও তিনটি 1, তাই এটি $0^n1^n$-এ ($n=3$) — $M$ এটি accept করে (L37-এ যাচাই করা হয়েছিল)। তাই এই $w$-এর জন্য build_Mw-এর তৈরি $M_w$-এর $L(M_w)$ "সব স্ট্রিং" হওয়ার কথা — অর্থাৎ প্রতিটি টেস্ট $x$-এ accept ফেরত আসার কথা, ঠিক যেমন "0011"-এর কেসে দেখানো হয়েছিল।

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

আগের পাঠ
হল্টিং প্রবলেম