পাঠ ৪৩ · ৫৬-এর মধ্যে · মডিউল ১০
Home / Courses / Formal Language & Automata Theory / Theory of Computation / টাইম কমপ্লেক্সিটি ও ক্লাস P

টাইম কমপ্লেক্সিটি ও ক্লাস P

Time complexity & the class P
৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • টাইম কমপ্লেক্সিটির সংজ্ঞা — ইনপুট length-এর ফাংশন হিসেবে TM-এর running time
  • ক্লাস P-এর ফরমাল সংজ্ঞা এবং "কার্যকর" (efficient)-এর প্রমিত, স্বীকৃত সংজ্ঞা হিসেবে এর ভূমিকা
  • P-এর বাস্তব উদাহরণ — সর্টিং, গ্রাফ রিচেবিলিটি, CYK অ্যালগরিদম
  • পলিনমিয়াল বনাম এক্সপোনেনশিয়াল সময়ের গ্রোথ-রেট ব্যবধান বাস্তব সংখ্যায় দেখা

১ · "আদৌ গণনাযোগ্য" থেকে "কতটা কার্যকরভাবে গণনাযোগ্য"

M8-M9 জুড়ে আমরা দেখেছি টুরিং মেশিন (M8) কী গণনা করতে পারে, আর M9-এ দেখেছি কিছু সমস্যা (হল্টিং প্রবলেম, রাইসের থিওরেম, PCP) কোনো টুরিং মেশিন দিয়েই সমাধানযোগ্য নয় — এই প্রশ্নটা ছিল বাইনারি: হ্যাঁ, গণনাযোগ্য, নাকি না। কিন্তু বাস্তবে, একটি সমস্যা "গণনাযোগ্য" হলেই যথেষ্ট নয় — যদি সমাধানটি বের করতে মহাবিশ্বের বয়সের চেয়েও বেশি সময় লাগে, সেটি ব্যবহারিকভাবে অসমাধানযোগ্যই। M10-M11 এই দ্বিতীয়, সূক্ষ্মতর প্রশ্নটি নিয়ে কাজ করবে — L01-এর তৃতীয় স্তম্ভ, কমপ্লেক্সিটি থিওরি।

২ · টাইম কমপ্লেক্সিটি — সংজ্ঞা

একটি অ্যালগরিদম/টুরিং মেশিনের রানিং টাইম বলতে বোঝায় ইনপুট length $n$-এর একটি ফাংশন হিসেবে অ্যালগরিদমটি কতটি ধাপ নেয় — সাধারণত worst case (দৈর্ঘ্য $n$-এর সবচেয়ে খারাপ সম্ভাব্য ইনপুটের জন্য) বিবেচনা করা হয়, এবং সাধারণ Big-O নোটেশনে (../dsa/-এ ইতিমধ্যে বিস্তারিত আলোচিত asymptotic নোটেশনের সরাসরি পুনর্ব্যবহার, এখানে পুনরায় derive করছি না) একটি আপার বাউন্ড হিসেবে প্রকাশ করা হয় (যেমন $O(n^2)$)। টুরিং-মেশিন-ভিত্তিক কমপ্লেক্সিটি থিওরিতে নির্দিষ্টভাবে, "রানিং টাইম" মানে TM ইনপুট প্রক্রিয়া করে থামা পর্যন্ত কতগুলো ট্রানজিশন (ধাপ) নেয়, ইনপুট length-এর ফাংশন হিসেবে।

৩ · ক্লাস P — ফরমাল সংজ্ঞা

ক্লাস P (KaTeX-এ):

$$\mathrm{P} = \{L : L \text{ কোনো ডিটারমিনিস্টিক TM দিয়ে } O(n^k) \text{ সময়ে ডিসাইড করা যায়, কোনো ধ্রুবক } k \geq 1\text{-এর জন্য}\}$$

অর্থাৎ, P হলো সেইসব ল্যাঙ্গুয়েজ যেগুলো পলিনমিয়াল সময়ে ডিসাইডযোগ্য — "কার্যকরভাবে সমাধানযোগ্য"-এর প্রমিত, ব্যাপকভাবে-স্বীকৃত সংজ্ঞা। একটি সৎ সতর্কতা দিয়ে বলা প্রয়োজন — পলিনমিয়াল সময় সবসময় ব্যবহারিকভাবে দ্রুত নয় (যেমন $O(n^{100})$ পলিনমিয়াল হলেও বাস্তবে সম্পূর্ণ অকেজো) — কিন্তু পলিনমিয়াল-বনাম-এক্সপোনেনশিয়াল হলো প্রমিত তাত্ত্বিক বিভাজনরেখা, কারণ এক্সপোনেনশিয়াল অ্যালগরিদম $n$ বাড়ার সাথে সাথে অনেক দ্রুত অসাধ্য হয়ে ওঠে — এই কোর্স এটিকেই "কার্যকর"-এর working সংজ্ঞা হিসেবে গ্রহণ করে।

সর্টিং
মার্জ সর্ট, কুইক সর্ট ইত্যাদি — ../dsa/-এর সব সর্টিং অ্যালগরিদমই পলিনমিয়াল সময়ের, তাই P-তে।
গ্রাফ রিচেবিলিটি
M3/L16-এর DFA-emptiness-স্টাইল BFS/DFS — পলিনমিয়াল সময়ে গ্রাফের দুই নোডের মধ্যে পথ আছে কি না নির্ণয় করে।
CYK অ্যালগরিদম
M6/L28-এ উল্লিখিত CFG-membership অ্যালগরিদম — একটি নির্দিষ্ট গ্রামারের জন্য স্ট্রিং length-এ পলিনমিয়াল সময়ে চলে।

নিচের কোড সেলে পলিনমিয়াল ($O(n^k)$, বিভিন্ন $k$-এর জন্য) বনাম এক্সপোনেনশিয়াল ($O(2^n)$) সময়ের প্রকৃত ধাপ-সংখ্যা পাশাপাশি গণনা করে দেখা যাক — বড় $n$-এ ব্যবধানটা ঠিক কতটা নাটকীয়।

Python
# পলিনমিয়াল O(n^k) বনাম এক্সপোনেনশিয়াল O(2^n) -- বাস্তব ধাপ-সংখ্যা পাশাপাশি

def time_complexity_class(exponent, n_values):
    # O(n^exponent) সময়ের জন্য প্রতিটি n-এ ধাপ-সংখ্যা গণনা
    return {n: n ** exponent for n in n_values}

def exponential_time(n_values):
    # O(2^n) সময়ের জন্য প্রতিটি n-এ ধাপ-সংখ্যা গণনা
    return {n: 2 ** n for n in n_values}

n_values = [10, 100, 1000]

results = {}
for k in [1, 2, 3]:
    results[f"O(n^{k})"] = time_complexity_class(k, n_values)
results["O(2^n)"] = exponential_time(n_values)

print(f"{'n':>6} | {'O(n^1)':>12} | {'O(n^2)':>16} | {'O(n^3)':>20} | {'O(2^n)':>14}")
print("-" * 80)
for n in n_values:
    row = [str(results[f"O(n^{k})"][n]) for k in [1, 2, 3]]
    exp_val = results["O(2^n)"][n]
    exp_str = str(exp_val) if exp_val < 10**20 else f"~10^{len(str(exp_val))-1} (একটি {len(str(exp_val))}-অঙ্কের সংখ্যা)"
    print(f"{n:>6} | {row[0]:>12} | {row[1]:>16} | {row[2]:>20} | {exp_str:>14}")

print()
print(f"n=1000-এ O(n^3) = {results['O(n^3)'][1000]:,} ধাপ")
print(f"n=1000-এ O(2^n) হলো একটি {len(str(results['O(2^n)'][1000]))}-অঙ্কের সংখ্যা -- ব্যবহারিকভাবে অগণনাযোগ্য")

    
লক্ষ্য করুন $n=1000$-এ $O(n^3) = 10^9$ (এক বিলিয়ন) ধাপ — আধুনিক কম্পিউটারে কয়েক সেকেন্ডের ব্যাপার — কিন্তু $O(2^{1000})$ হলো ৩০০-এর বেশি অঙ্কের একটি সংখ্যা, যা মহাবিশ্বের পরিচিত পরমাণুর সংখ্যার চেয়েও বহুগুণ বেশি। এই ব্যবধানই পলিনমিয়াল-বনাম-এক্সপোনেনশিয়ালকে একটি নিছক তাত্ত্বিক সংজ্ঞার বদলে একটি বাস্তব, গুরুত্বপূর্ণ বিভাজনরেখা করে তোলে।
মূল কথা · Key takeaway

ক্লাস P সংজ্ঞায়িত করে কোন সমস্যাগুলো "কার্যকরভাবে সমাধানযোগ্য" — পলিনমিয়াল সময়ে ডিসাইডযোগ্য ল্যাঙ্গুয়েজের সেট। পরের পাঠে (L44) আমরা দেখব একটি সম্পূর্ণ ভিন্ন, কিন্তু ঘনিষ্ঠভাবে সম্পর্কিত ক্লাস — NP — যেখানে উত্তর যাচাই করা পলিনমিয়াল সময়ে সম্ভব, এমনকি খুঁজে বের করা কঠিন হলেও।

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

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

প্র ০১ "পলিনমিয়াল সময়" মানেই কি সবসময় "ব্যবহারিকভাবে দ্রুত"? উদাহরণসহ ব্যাখ্যা করুন।

না, সবসময় নয় — $O(n^{100})$ পলিনমিয়াল, কিন্তু $n=10$-এও এটি $10^{100}$ ধাপ, সম্পূর্ণ অব্যবহারযোগ্য। তবুও, পলিনমিয়াল-বনাম-এক্সপোনেনশিয়াল বিভাজনরেখাটি এখনও কেন গুরুত্বপূর্ণ, তার কারণ হলো — বাস্তবে দেখা যাওয়া বেশিরভাগ পলিনমিয়াল অ্যালগরিদমের exponent $k$ ছোট (১, ২, ৩) হয়, আর গুরুত্বপূর্ণভাবে, exponent যত বড়ই হোক না কেন, $n$ বড় হওয়ার সাথে সাথে যেকোনো পলিনমিয়াল ফাংশন যেকোনো এক্সপোনেনশিয়াল ফাংশনের চেয়ে ধীরে বাড়ে — তাই এটি এখনও একটি অর্থপূর্ণ, প্রমিত তাত্ত্বিক বিভাজনরেখা, যদিও একমাত্র ব্যবহারিক মানদণ্ড নয়।

প্র ০২ ক্লাস P-এর সংজ্ঞায় "ডিটারমিনিস্টিক TM" শব্দটি স্পষ্টভাবে উল্লেখ করা হয়েছে কেন? এটি কি গুরুত্বপূর্ণ?

হ্যাঁ, এটি গুরুত্বপূর্ণ — কারণ পরের পাঠে (L44) আমরা দেখব একটি ভিন্ন ক্লাস, NP, যেটি নন-ডিটারমিনিস্টিক TM-এর পলিনমিয়াল সময়ের সাথে সংজ্ঞায়িত (M8/L36-এর NTM ধারণার সরাসরি পুনর্ব্যবহার)। P-এর সংজ্ঞায় "ডিটারমিনিস্টিক" স্পষ্ট রাখাটা এই দুই ক্লাসের মধ্যে সুনির্দিষ্ট পার্থক্য বজায় রাখে — আর ঠিক এই দুই ক্লাস সমান কি না ($\mathrm{P} = \mathrm{NP}$?), সেটিই L49-এ আলোচিত বিখ্যাত খোলা প্রশ্ন।

প্র ০৩ উপরের কোড সেলে n=10-এ $O(n^3)$ ও $O(2^n)$-এর ফলাফল প্রায় কাছাকাছি, কিন্তু n=100-এ ব্যবধান হঠাৎ বিরাট হয়ে যায় কেন?

কারণ পলিনমিয়াল ফাংশন ($n^3$) $n$-এর সাথে বহুপদী হারে বাড়ে, কিন্তু এক্সপোনেনশিয়াল ফাংশন ($2^n$) $n$-এর সাথে সূচকীয় হারে বাড়ে — প্রতিটি নতুন ইনপুট-অক্ষরে $2^n$ দ্বিগুণ হয়ে যায়, অথচ $n^3$ শুধু অনুপাতিকভাবে বাড়ে। $n=10$-এ $2^{10}=1024$ এখনও $10^3=1000$-এর কাছাকাছি, কিন্তু $n=100$-এ $2^{100}$ ইতিমধ্যে একটি ৩১-অঙ্কের সংখ্যা, অথচ $100^3$ মাত্র সাত অঙ্কের — গ্রোথ-রেটের এই মৌলিক পার্থক্যই এত দ্রুত এত বিরাট ব্যবধান তৈরি করে।

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোড সেলে n_values-এ 10000 যোগ করে Run চেপে দেখুন — O(n^3) ও O(2^n)-এর ফলাফলের অঙ্কসংখ্যায় ব্যবধান আরও কতটা বাড়ে লক্ষ করুন।

    $n=10000$-এ $O(n^3) = 10^{12}$ (এক ট্রিলিয়ন) — এখনও আধুনিক কম্পিউটারে কয়েক মিনিটে গণনাযোগ্য। কিন্তু $O(2^{10000})$ প্রায় ৩০০০-এর বেশি অঙ্কের একটি সংখ্যা — এটি লেখার জন্যই এই পৃষ্ঠার চেয়ে বেশি জায়গা লাগবে, বাস্তবে গণনার তো প্রশ্নই ওঠে না। এটিই দেখায় কেন কমপ্লেক্সিটি থিওরি পলিনমিয়াল-বনাম-এক্সপোনেনশিয়ালকে এত গুরুত্বের সাথে দেখে।

  2. চিন্তা করুন: M3/L16-এ দেখানো হয়েছিল DFA-এর emptiness প্রশ্ন ডিসাইডেবল, BFS/DFS দিয়ে গ্রাফ-রিচেবিলিটি চেক করে। এই অ্যালগরিদমটি কি ক্লাস P-তে? কেন?

    হ্যাঁ — BFS/DFS-ভিত্তিক গ্রাফ-রিচেবিলিটি চেক DFA-এর স্টেট-সংখ্যায় পলিনমিয়াল সময়ে (আসলে লিনিয়ার, $O(|Q| + |\delta|)$) চলে — প্রতিটি স্টেট ও ট্রানজিশন অন্তত একবার ভিজিট করা হয়, কোনো এক্সপোনেনশিয়াল ব্লো-আপ নেই। তাই এটি স্পষ্টভাবে P-তে — এটিই ঠিক কেন M3/L16-এ DFA-এর ডিসিশন প্রপার্টিগুলো শুধু "ডিসাইডেবল" নয়, বরং ব্যবহারিকভাবেও "কার্যকরভাবে ডিসাইডেবল।"

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

আগের পাঠ
L42 · পোস্ট করেসপন্ডেন্স প্রবলেম