রিডাকশন ও আনডিসাইডেবিলিটি প্রুফ
এই পাঠে যা শিখবেন
- রিডাকশনের ধারণা এবং "$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 থেকে রিডাকশনের মাধ্যমে —
- ধরা যাক $E_{TM}$-এর একটি decider $R$ আছে (কনট্রাডিকশনের জন্য)।
- ইনপুট $\langle M, w \rangle$ (HALT-এর একটি ইনস্ট্যান্স) দেওয়া হলে, একটি নতুন মেশিন $M_w$ বানাই — $M_w$ তার নিজের ইনপুট $x$ ignore করে, এবং সবসময় $M$-কে ফিক্সড স্ট্রিং $w$-এর উপর সিমুলেট করে — $M$ যদি $w$-এর উপর accept করে, $M_w$ তখন $x$ accept করে (যে $x$ই হোক না কেন)।
- লক্ষ্য করুন — $L(M_w)$ হয় $\Sigma^*$ (সব স্ট্রিং, যদি $M$, $w$-এর উপর হল্ট-accept করে), অথবা $\emptyset$ (খালি, যদি $M$, $w$-এর উপর হল্ট না করে বা reject করে)।
- এবার $\langle M_w \rangle$ কে assumed decider $R$-কে দিই — $R$ সঠিকভাবে বলবে $L(M_w)$ খালি কি না — যা ঠিক বলে দেয় $M$, $w$-এর উপর accept করে (হল্ট করে) কি না।
- এই পুরো প্রক্রিয়াটাই তাহলে HALT-এর একটি decider — কিন্তু L39 অনুযায়ী HALT আনডিসাইডেবল — কনট্রাডিকশন। তাই $E_{TM}$-এর decider $R$ থাকতে পারে না — $E_{TM}$ আনডিসাইডেবল। ∎
৩ · কোড সেল — 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)$ সত্যিই দাবিকৃত "সব স্ট্রিং" বা "খালি" আচরণ দেখায় কি না।
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$ থেকে শুরু) কখনোই স্পর্শ না হয় — একটি সহজ, কিন্তু গুরুত্বপূর্ণ বাস্তবায়ন-বিস্তারিত যা কনস্ট্রাকশনটাকে
সত্যিকারের কাজ করতে দেয়।
রিডাকশন হলো এই কোর্সের বাকি অংশের (এবং 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) কঠিনভাবে প্রমাণ করা হয়ে গেলে, বাকি সব প্রমাণ শুধু সেই একটার সাথে "সংযোগ" তৈরি করেই সম্পন্ন করা যায়, নতুন করে ডায়াগোনালাইজেশন লাগে না।
অনুশীলন
-
চিন্তা করুন: কেউ যদি ভুলভাবে দাবি করে "M-এর ঠিক ৫টি state আছে কি না" — এই প্রশ্নও
build_Mw-স্টাইল রিডাকশন ব্যবহার করে আনডিসাইডেবল প্রমাণ করা যাবে — এই দাবিটা কি সঠিক? কেন বা কেন নয়?না, এই দাবিটা ভুল। "M-এর ঠিক ৫টি state আছে" প্রশ্নটা $M$-এর নিজস্ব বর্ণনা (কতগুলো state সে ব্যবহার করে) সম্পর্কে, $L(M)$ (M যে ভাষা recognize করে) সম্পর্কে নয় — এটা $M$-এর এনকোডিং সরাসরি পড়েই (state গুলো গুনে) নির্ণয় করা যায়, কোনো সিমুলেশন ছাড়াই। এটা $E_{TM}$-এর মতো ভাষা-নির্ভর প্রশ্ন নয়, তাই HALT থেকে এভাবে রিডিউস করা যাবে না — বরং এটি সহজেই ডিসাইডেবল, শুধু state সংখ্যা গোনার মাধ্যমে। L41-এ এই পার্থক্যটাই (ভাষা-প্রপার্টি বনাম implementation-প্রপার্টি) আরও নির্ভুলভাবে সংজ্ঞায়িত হবে।
-
পরীক্ষা করুন: কোড সেলে
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-এ আপনার পরবর্তী পদক্ষেপ
- পাঠ ৪১ · রাইসের থিওরেম পরের পাঠ $E_{TM}$-এর মতো অসংখ্য আলাদা আলাদা রিডাকশন প্রমাণ আসলে একটি একক, সুইপিং থিওরেমের বিশেষ ঘটনা — সেই থিওরেমই এই পাঠে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ DFA থেকে টুরিং মেশিন, ডিসাইডেবিলিটি ও P বনাম NP পর্যন্ত — সম্পূর্ণ পাঠক্রম।
- পাঠ ৩৯ · হল্টিং প্রবলেম আগের পাঠ এই পাঠের রিডাকশনের ভিত্তি — HALT আনডিসাইডেবল, সেই মূল প্রমাণ।