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

নন-ডিটারমিনিস্টিক টুরিং মেশিন ও ইকুইভ্যালেন্স

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

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

  • NTM-এর ফরমাল সংজ্ঞা এবং "exists a path" accept নিয়ম (L07-এর সরাসরি সমান্তরাল)
  • NTM-ডিটারমিনিস্টিক TM ইকুইভ্যালেন্স থিওরেম এবং এর BFS-ভিত্তিক প্রমাণ-কৌশল
  • কেন এই সিমুলেশনে L35-এর মাল্টি-টেপ কৌশল প্রয়োজন হয়
  • এক্সপোনেনশিয়াল ব্রাঞ্চ-কাউন্ট ব্লো-আপ — একটি জেনুইন কোড-ভিত্তিক পরিমাপ, ও P বনাম NP-এর প্রথম ইঙ্গিত

১ · NTM — নন-ডিটারমিনিজম, এবার TM-এ

M2/L07-এ দেখা NFA নন-ডিটারমিনিজমের ধারণাটি সরাসরি TM মডেলেও প্রয়োগ করা যায়। একটি নন-ডিটারমিনিস্টিক টুরিং মেশিন (NTM)-এ $\delta$ একটি (state, symbol) জোড়ার জন্য একাধিক সম্ভাব্য (নতুন state, লেখার সিম্বল, দিক) রিটার্ন করতে পারে। গ্রহণযোগ্যতার নিয়মও L07-এর NFA-র মতোই — একটি NTM একটি ইনপুট accept করে যদি সম্ভাব্য পছন্দের কোনো একটি সিকোয়েন্স $q_{accept}$-এ পৌঁছায়, এমনকি অন্য সব সম্ভাব্য পছন্দের পথ ব্যর্থ হলেও।

২ · ইকুইভ্যালেন্স থিওরেম — এই কোর্সের চূড়ান্ত এমন থিওরেম

এই কোর্সে বারবার ফিরে আসা প্যাটার্নটির শেষ উদাহরণ (M2/L08-এর NFA-DFA প্রুফ, L35-এর মাল্টি-টেপ ইকুইভ্যালেন্সের সরাসরি সমান্তরাল — নন-ডিটারমিনিজম/সুবিধা কোনো মৌলিক ক্ষমতা যোগ করে না, শুধু সুবিধা যোগ করে): প্রতিটি NTM-এর একটি সমতুল্য ডিটারমিনিস্টিক TM আছে। কনস্ট্রাকশন আইডিয়া — এবার একেবারে ভিন্ন একটি কৌশল, M2/L08-এর সাবসেট কনস্ট্রাকশনের সাথে গুলিয়ে ফেলবেন না: NTM-এর সম্ভাব্য কম্পিউটেশন-ব্রাঞ্চের সম্পূর্ণ ট্রি একটি L35-স্টাইল মাল্টি-টেপ ডিটারমিনিস্টিক মেশিনে ব্রেডথ-ফার্স্ট সার্চ (BFS) দিয়ে সিমুলেট করা হয় —

  • একটি টেপে মূল ইনপুট (কখনো পরিবর্তন হয় না — প্রতিটি নতুন ব্রাঞ্চ শুরু করার জন্য দরকার)।
  • একটি টেপে বর্তমানে কোন ব্রাঞ্চ সিমুলেট করা হচ্ছে (choice-index-এর একটি সিকোয়েন্স হিসেবে)।
  • একটি টেপ সেই নির্দিষ্ট ব্রাঞ্চ চালানোর জন্য স্ক্র্যাচ স্পেস।

প্রতিটি সম্ভাব্য ব্রাঞ্চ ব্রেডথ-ফার্স্ট ক্রমে সিস্টেমেটিকভাবে চেষ্টা করা হয়, এবং কোনো ব্রাঞ্চ $q_{accept}$-এ পৌঁছালেই accept করা হয়।

start NTM: সব ব্রাঞ্চ সমান্তরালে "বিদ্যমান" কোনো একটি accept-এ পৌঁছালেই accept ডিটারমিনিস্টিক BFS সিমুলেশন টেপ ১: অপরিবর্তিত ইনপুট টেপ ২: choice-index সিকোয়েন্স টেপ ৩: বর্তমান ব্রাঞ্চের স্ক্র্যাচ একে একে সব ব্রাঞ্চ ট্রাই করে গুরুত্বপূর্ণ পরিণতি: শাখা-প্রশাখা $b$, গভীরতা $d$ হলে সম্ভাব্য $b^d$ ব্রাঞ্চ ওয়ার্স্ট-কেসে এক্সপোনেনশিয়াল স্লোডাউন — P বনাম NP-এর বীজ (M10)
NTM-এ সব ব্রাঞ্চ যেন সমান্তরালে ঘটে — ডিটারমিনিস্টিক সিমুলেশনে সেগুলো একে একে, BFS ক্রমে চেষ্টা করতে হয়।
কম্পিউটেবিলিটি পাওয়ার বনাম দক্ষতা — একটি গুরুত্বপূর্ণ ফোরশ্যাডো

L35-এর "পলিনোমিয়াল স্লোডাউন" nuance থেকে এটি সম্পূর্ণ ভিন্ন এবং আরও গুরুত্বপূর্ণ একটি পয়েন্ট: এই BFS-ওভার-অল-ব্রাঞ্চ সিমুলেশন ওয়ার্স্ট-কেসে এক্সপোনেনশিয়াল স্লোডাউন ঘটাতে পারে — শাখা-প্রশাখা (branching factor) $b$ এবং গভীরতা $d$ হলে, মোট $b^d$ পর্যন্ত ব্রাঞ্চ এক্সপ্লোর করতে হতে পারে। এটি একটি সরাসরি, স্পষ্ট প্রিভিউ যে — "নন-ডিটারমিনিজম কম্পিউটেবিলিটি ক্ষমতায় কিছু যোগ করে না, কিন্তু দক্ষতায় (efficiency) সত্যিই গুরুত্বপূর্ণ হতে পারে" — এটিই ঠিক M10/L44-এর P বনাম NP প্রশ্নের বীজ।

৩ · কোড — NTM-স্টাইল বনাম ডিটারমিনিস্টিক BFS, ব্রাঞ্চ-কাউন্ট তুলনা

নিচের কোডে একটি ছোট্ট, স্বাভাবিকভাবেই নন-ডিটারমিনিস্টিক সমস্যা — "এই স্ট্রিং-এ কি '101' সাবস্ট্রিং হিসেবে আছে?" — যেখানে নন-ডিটারমিনিস্টিকভাবে "guess" করা হয় সাবস্ট্রিং ঠিক কোথায় শুরু হতে পারে। প্রথম ফাংশনটি L07/L22-এর মতোই একসাথে সব সম্ভাব্য কনফিগারেশনের সেট ট্র্যাক করে নন-ডিটারমিনিজম সিমুলেট করে (কোনো literal backtracking ছাড়াই)। দ্বিতীয়টি একই কাজ ডিটারমিনিস্টিকভাবে করে — প্রতিটি সম্ভাব্য শুরু-অবস্থান একে একে (sequentially) চেষ্টা করে।

Python
def ntm_recognize(s, pattern="101"):
    """সব সম্ভাব্য (position, match_progress) কনফিগারেশন একসাথে সেটে ট্র্যাক করে --
    প্রতিটি ধাপে দুটি 'পছন্দ' সমান্তরালে ট্র্যাক করা হয়: (ক) নতুন করে ম্যাচ শুরুর guess,
    (খ) চলমান ম্যাচ চালিয়ে যাওয়া -- ঠিক NTM/NFA নন-ডিটারমিনিজম সিমুলেশনের স্ট্যান্ডার্ড কৌশল।"""
    m, n = len(pattern), len(s)
    configs = {(0, 0)}
    branch_count = 0
    for step in range(n + 1):
        branch_count += len(configs)          # সমান্তরালে যত কনফিগারেশন বিদ্যমান
        if any(progress == m for _, progress in configs):
            return True, branch_count
        if step == n:
            break
        ch = s[step]
        new_configs = set()
        for (pos, progress) in configs:
            new_configs.add((pos, 1 if ch == pattern[0] else 0))   # নতুন attempt-এর guess
            if progress < m and ch == pattern[progress]:
                new_configs.add((pos, progress + 1))               # চলমান ম্যাচ চালিয়ে যাওয়া
        configs = new_configs
    return any(p == m for _, p in configs), branch_count

def deterministic_bfs_recognize(s, pattern="101"):
    """ডিটারমিনিস্টিক সংস্করণ -- প্রতিটি সম্ভাব্য শুরু-অবস্থান একে একে, sequentially চেষ্টা করে,
    ঠিক একই কাজ করছে, কিন্তু কোনো সমান্তরাল ট্র্যাকিং ছাড়া।"""
    n, m = len(s), len(pattern)
    branch_count = 0
    for start in range(max(0, n - m + 1)):
        branch_count += 1
        match = True
        for offset in range(m):
            branch_count += 1
            if s[start + offset] != pattern[offset]:
                match = False
                break
        if match:
            return True, branch_count
    return False, branch_count

tests = ["101", "1101", "0101100", "111000111", "100110", "000000", "1"*15]
for s in tests:
    nt_result, nt_branches = ntm_recognize(s)
    det_result, det_branches = deterministic_bfs_recognize(s)
    match = "IDENTICAL" if nt_result == det_result else "MISMATCH"
    print(f"{s!r:14s} NTM={nt_result!s:5s}(branch={nt_branches:3d})  "
          f"DET-BFS={det_result!s:5s}(branch={det_branches:3d})  [{match}]")

    
হাতে-যাচাই: প্রতিটি টেস্ট স্ট্রিং-এ NTM ও DET-BFS ঠিক একই accept/reject ফলাফল দেয় (ইকুইভ্যালেন্স থিওরেমের concrete প্রমাণ) — কিন্তু ব্রাঞ্চ-কাউন্ট ভিন্ন: "111000111"-এ DET-BFS ১৯টি ব্রাঞ্চ পরীক্ষা করে, যেখানে NTM মাত্র ১১টি কনফিগারেশনেই একই কাজ শেষ করে (সমান্তরাল ট্র্যাকিংয়ের সুবিধা) — লম্বা স্ট্রিং-এ এই ফারাক আরও স্পষ্ট হয়। এই নির্দিষ্ট উদাহরণে branching factor ছোট বলে ফারাকটা লিনিয়ার-ঘেঁষা, কিন্তু বড় branching factor-এর সাধারণ NTM-এ এই ফারাক সত্যিকারের এক্সপোনেনশিয়ালে পরিণত হতে পারে।

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

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

প্র ০১ NTM-DTM ইকুইভ্যালেন্সের এই BFS-প্রুফ M2/L08-এর সাবসেট কনস্ট্রাকশন থেকে কীভাবে মৌলিকভাবে ভিন্ন?

সাবসেট কনস্ট্রাকশনে (L08) NFA-র সম্ভাব্য সব স্টেটের একটি সেট-কে একটিমাত্র নতুন DFA-স্টেট হিসেবে ট্রিট করা হয় — ফলাফল একটি নতুন, কিন্তু এখনও একটি সিঙ্গল-পাস, ডিটারমিনিস্টিক মেশিন যা একই গতিতে চলে। NTM-এর ক্ষেত্রে এই কৌশলটি কাজ করে না, কারণ প্রতিটি ব্রাঞ্চ নিজে থেকেই একটি সম্পূর্ণ, সম্ভাব্য-অসীম কম্পিউটেশন — তাই বদলে ডিটারমিনিস্টিক মেশিনকে একে একে প্রতিটি ব্রাঞ্চ পুনরায় চালিয়ে (re-run করে, BFS ক্রমে) দেখতে হয়, যা মূল NTM-এর একটি ধাপের বদলে সম্ভাব্য অনেক ধাপ সময় নেয়।

প্র ০২ BFS সিমুলেশনে (DFS নয়, BFS-ই) ব্যবহার করা কেন জরুরি — গভীরতায় (depth) অগ্রাধিকার দিলে (DFS) কী সমস্যা হতে পারত?

কারণ কিছু ব্রাঞ্চ চিরকাল লুপে চলতে পারে (L32/L33-এর ফোরশ্যাডো) — যদি DFS ব্যবহার করে প্রথম ব্রাঞ্চে "গভীরে" চলে যাওয়া হয়, এবং সেই ব্রাঞ্চটি কখনো হল্ট না করে, তাহলে মেশিন কখনো অন্য ব্রাঞ্চগুলো (যেগুলোর মধ্যে হয়তো একটি accept করত) চেষ্টাই করতে পারবে না। BFS নিশ্চিত করে প্রতিটি finite-length ব্রাঞ্চ শেষমেশ পালাক্রমে পরীক্ষিত হবে — তাই যদি কোনো ব্রাঞ্চ finite ধাপে accept করে, BFS তা শেষপর্যন্ত খুঁজে পাবেই, অন্য কোনো ব্রাঞ্চ অসীম লুপে থাকলেও।

প্র ০৩ "নন-ডিটারমিনিজম কম্পিউটেবিলিটি ক্ষমতায় কিছু যোগ করে না, কিন্তু দক্ষতায় গুরুত্বপূর্ণ হতে পারে" — এই দুটি দাবি একসাথে কীভাবে সত্য হতে পারে, স্ববিরোধী না হয়ে?

দুটি ভিন্ন প্রশ্নের উত্তর — কম্পিউটেবিলিটি থিওরি জিজ্ঞেস করে "এই ভাষাটি আদৌ ডিসাইড করা যায় কি না" (হ্যাঁ/না প্রশ্ন, সময় নিয়ে ভাবে না); সেই প্রশ্নের উত্তরে NTM আর DTM সমতুল্য (এই পাঠের থিওরেম)। কমপ্লেক্সিটি থিওরি জিজ্ঞেস করে "কতক্ষণ লাগে" — সেখানে ইকুইভ্যালেন্স প্রমাণে ব্যবহৃত BFS সিমুলেশনের এক্সপোনেনশিয়াল স্লোডাউন সত্যিই গুরুত্বপূর্ণ হয়ে ওঠে। তাই "একই ক্ষমতা" আর "একই গতি" — দুটি সম্পূর্ণ ভিন্ন claim, আর একটি সত্য হলে আরেকটি মিথ্যা হতে কোনো বাধা নেই।

অনুশীলন

  1. চিন্তা করুন: উপরের কোডের ntm_recognize-এ প্রতিটি ধাপে দুই ধরনের "পছন্দ" ট্র্যাক করা হয় (নতুন attempt শুরু, বা চলমান ম্যাচ চালিয়ে যাওয়া) — branching factor $b$ এখানে ঠিক কত? এই নির্দিষ্ট সমস্যায় $b^d$ কেন সত্যিকারের এক্সপোনেনশিয়াল ব্লো-আপ ঘটায় না?

    এখানে $b \le 2$ (প্রতি ধাপে সর্বোচ্চ দুটি নতুন কনফিগারেশন তৈরি হতে পারে), কিন্তু কনফিগারেশনগুলো একটি সেটে রাখা হচ্ছে — তাই ডুপ্লিকেট (একই (position, progress) জোড়া একাধিকবার তৈরি হলেও) স্বয়ংক্রিয়ভাবে একত্রিত হয়ে যায়। যেহেতু progress-এর সম্ভাব্য মান মাত্র $m+1$টি (pattern-এর দৈর্ঘ্য অনুযায়ী), সেটের আকার কখনোই একটি ছোট ধ্রুবকের বেশি বাড়ে না — তাই এই নির্দিষ্ট সমস্যায় "সেটে ট্র্যাক করা"-ই কার্যকরভাবে ব্লো-আপ প্রতিরোধ করে, যা সাধারণ NTM-এর জন্য সবসময় সম্ভব নয় (যদি সম্ভাব্য কনফিগারেশনের সংখ্যা নিজেই ইনপুটের সাথে বাড়ে)।

  2. পরীক্ষা করুন: কোডে pattern="101"-এর বদলে pattern="1010101" (লম্বা প্যাটার্ন) ব্যবহার করে "1"*20 + "0"*20-এর মতো একটি দীর্ঘ স্ট্রিং-এ ব্রাঞ্চ-কাউন্ট তুলনা করুন — NTM বনাম DET-BFS-এর ব্যবধান কি বাড়ে?

    হ্যাঁ, লম্বা প্যাটার্নে DET-BFS-কে প্রতিটি শুরু-অবস্থানে গড়ে বেশি ক্যারেক্টার তুলনা করতে হয় (যেহেতু দীর্ঘ প্যাটার্নের আংশিক মিল বেশিক্ষণ ধরে চলতে পারে আগে ব্যর্থ হওয়ার আগে), তাই এর ব্রাঞ্চ-কাউন্ট বাড়ে দ্রুত, যেখানে NTM-এর সেট-ভিত্তিক ট্র্যাকিং এখনও কার্যকরভাবে ছোট (progress-এর সম্ভাব্য মান সংখ্যা এখনও pattern-এর দৈর্ঘ্যের সমানুপাতিকই থাকে) — ব্যবধানটি স্পষ্টভাবে বাড়তে দেখা যাবে, যদিও এই নির্দিষ্ট সমস্যা-শ্রেণিতে তা এখনও সত্যিকারের এক্সপোনেনশিয়াল নয়।

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

আগের পাঠ
টুরিং মেশিনের ভ্যারিয়েন্ট — মাল্টি-টেপ ও মাল্টি-ট্র্যাক