পাঠ ৫৩ · ৫৬-এর মধ্যে · মডিউল ১২
Home / Courses / Formal Language & Automata Theory / Theory of Computation / কমপ্লেক্সিটি থিওরি ও অ্যালগরিদম ডিজাইন

কমপ্লেক্সিটি থিওরি ও অ্যালগরিদম ডিজাইন

Complexity theory & algorithm design
৭ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • NP-কমপ্লিটনেস চেনা কেন একজন প্রোগ্রামারের জন্য বাস্তব, প্র্যাক্টিক্যাল মূল্য বহন করে
  • ডায়নামিক প্রোগ্রামিং কীভাবে ওভারল্যাপিং সাবপ্রবলেম এড়িয়ে সময় বাঁচায় — নির্দিষ্ট গাণিতিক কারণ সহ
  • গ্রিডি অ্যালগরিদম কখন কাজ করে (এবং কখন করে না) — অপ্টিমালিটি বনাম গতির ট্রেড-অফ
  • একটি বাস্তব, কোড-ভেরিফায়েড তুলনা — naive recursive LCS বনাম DP LCS, উভয়ই একই উত্তর দেয় কিন্তু ভিন্ন খরচে

১ · NP-কমপ্লিটনেস চেনা কেন প্র্যাক্টিক্যালি গুরুত্বপূর্ণ

M10/L45-L47-এ আমরা দেখেছিলাম কীভাবে একটি সমস্যা NP-কমপ্লিট প্রমাণ করতে হয় — একটি পলিনমিয়াল-টাইম রিডাকশন দিয়ে। এই পাঠের কেন্দ্রীয় বার্তা হলো: এই স্কিলটি শুধু তাত্ত্বিক পরীক্ষার জন্য নয়, বরং বাস্তব ইঞ্জিনিয়ারিং সময় বাঁচানোর একটি হাতিয়ার। যখন আপনি চিনতে পারেন "এই শিডিউলিং/রাউটিং/ম্যাচিং সমস্যাটি মূলত ছদ্মবেশে TSP/vertex-cover/graph-coloring" (M10/L47) — তখন আপনি জানেন একটি নিখুঁত, দ্রুত সমাধান খোঁজা যুক্তিসঙ্গত সময়ের মধ্যে সম্ভব নয় (M11/L49-এর শক্তিশালী তাত্ত্বিক প্রমাণের ভিত্তিতে) — এবং সরাসরি M11/L50-এর বিকল্পগুলোতে চলে যেতে পারেন: অ্যাপ্রক্সিমেশন, হেউরিস্টিক, বা সমস্যাকে একটি বিশেষ সহজতর সাব-কেসে সীমাবদ্ধ করা।

"আরও চেষ্টা করলেই পারব" — একটি ব্যয়বহুল ভুল

NP-কমপ্লিটনেস না চিনে একজন ইঞ্জিনিয়ার সপ্তাহের পর সপ্তাহ একটি "নিখুঁত" দ্রুত অ্যালগরিদম খুঁজতে ব্যয় করতে পারেন — যেখানে সমস্যাটি আসলে একটি পরিচিত NP-কমপ্লিট সমস্যার ছদ্মবেশ মাত্র। M10-M11-এর তত্ত্ব জানা থাকলে এই সময় সাশ্রয় হয় — সরাসরি প্রমাণিতভাবে-ভালো একটি অ্যাপ্রক্সিমেশন কৌশলে চলে যাওয়া যায়।

২ · ডায়নামিক প্রোগ্রামিং — ওভারল্যাপিং সাবপ্রবলেম এড়ানো

M6/L28-এর CYK অ্যালগরিদম ইতিমধ্যে ডায়নামিক প্রোগ্রামিংDynamic Programmingওভারল্যাপিং সাবপ্রবলেমের ফলাফল একবার সংরক্ষণ করে বারবার পুনঃগণনা এড়ানোর একটি অ্যালগরিদম-ডিজাইন টেকনিক।-এর একটি TOC-সংলগ্ন উদাহরণ ছিল — একটি টেবিলে ছোট সাবস্ট্রিং-এর ফলাফল সংরক্ষণ করে বড় সাবস্ট্রিং গণনায় পুনরায় ব্যবহার করা, বারবার পুনঃগণনা এড়িয়ে। এই টেকনিকের মূল অন্তর্দৃষ্টি — একটি সমস্যা ওভারল্যাপিং সাবপ্রবলেম ধারণ করলে (একই সাবপ্রবলেম বারবার সমাধান করার প্রয়োজন হয়), তাহলে সেই ফলাফল একবার সংরক্ষণ করে (memoization বা bottom-up টেবিল দিয়ে) পুনরায় ব্যবহার করাই একটি সত্যিকারের এক্সপোনেনশিয়াল-দেখতে সমস্যাকে গোপনে পলিনমিয়াল বানিয়ে দিতে পারে।

৩ · গ্রিডি অ্যালগরিদম — গতির জন্য অপ্টিমালিটি বিনিময়

M11/L50-এর vertex-cover অ্যাপ্রক্সিমেশন একটি গ্রিডি কৌশলের উদাহরণ ছিল — প্রতিটি ধাপে স্থানীয়ভাবে সবচেয়ে ভালো মনে হওয়া পছন্দ করা, পুরো সমস্যা পুনর্বিবেচনা না করেই। গ্রিডি অ্যালগরিদম দ্রুত কিন্তু সাধারণত নিখুঁত অপ্টিমাল সমাধানের গ্যারান্টি দেয় না — শুধুমাত্র সমস্যার একটি নির্দিষ্ট গাণিতিক structure (যেমন "matroid structure," একটি উন্নত ধারণা, শুধু নামমাত্র উল্লেখ) থাকলেই গ্রিডি প্রমাণিতভাবে সঠিক ফলাফল দেয়।

ডায়নামিক প্রোগ্রামিং
ওভারল্যাপিং সাবপ্রবলেম এড়ানো — সাধারণত নিখুঁত সঠিক উত্তর দেয়, কিন্তু সব সমস্যায় প্রযোজ্য নয় (শুধু অপ্টিমাল সাব-স্ট্রাকচার থাকলে)।
গ্রিডি অ্যালগরিদম
স্থানীয়ভাবে সেরা পছন্দ — সবচেয়ে দ্রুত, কিন্তু শুধু নির্দিষ্ট গাণিতিক structure-এ প্রমাণিতভাবে অপ্টিমাল।

৪ · কোড দিয়ে যাচাই — LCS: naive exponential বনাম DP

নিচের কোড সেলে longest common subsequence (LCS) সমস্যার দুটি সমাধান তুলনা করা হয়েছে — একই ছোট ইনপুটে, একই সঠিক উত্তর দেয়, কিন্তু recursive-call সংখ্যায় নাটকীয় পার্থক্য।

Python
# LCS -- naive exponential recursion বনাম DP -- একই উত্তর, ভিন্ন খরচ

def lcs_naive(x, y):
    calls = [0]
    def rec(i, j):
        calls[0] += 1
        if i == 0 or j == 0:
            return 0
        if x[i-1] == y[j-1]:
            return 1 + rec(i-1, j-1)
        return max(rec(i-1, j), rec(i, j-1))  # -- ওভারল্যাপিং সাবপ্রবলেম বারবার পুনঃগণনা হয়
    result = rec(len(x), len(y))
    return result, calls[0]

def lcs_dp(x, y):
    calls = [0]
    m, n = len(x), len(y)
    table = [[0]*(n+1) for _ in range(m+1)]
    for i in range(1, m+1):
        for j in range(1, n+1):
            calls[0] += 1  # প্রতিটি টেবিল-সেল গণনা = একটি "কল" -- প্রতিটি সাবপ্রবলেম ঠিক একবার সমাধান হয়
            if x[i-1] == y[j-1]:
                table[i][j] = 1 + table[i-1][j-1]
            else:
                table[i][j] = max(table[i-1][j], table[i][j-1])
    return table[m][n], calls[0]

pairs = [("ABCBDAB", "BDCABA"), ("AGGTAB", "GXTXAYB"), ("ABCDEFGH", "ACBDEGFH")]
print(f"{'x':>10} {'y':>10} | naive-উত্তর | dp-উত্তর | naive-কল | dp-কল | speedup")
print("-" * 78)
for x, y in pairs:
    a1, c1 = lcs_naive(x, y)
    a2, c2 = lcs_dp(x, y)
    assert a1 == a2, f"গরমিল {x},{y}: {a1} != {a2}"
    print(f"{x:>10} {y:>10} | {a1:>10} | {a2:>7} | {c1:>8} | {c2:>5} | {c1/c2:>6.1f}x")

    
লক্ষ্য করুন assert a1 == a2 নিশ্চিত করছে দুটি সমাধান প্রতিবার ঠিক একই সঠিক উত্তর দেয় — শুধু কাজের পরিমাণ ভিন্ন। lcs_naive-এর call সংখ্যা ইনপুট দৈর্ঘ্যের সাথে এক্সপোনেনশিয়ালভাবে বাড়ে (একই $(i, j)$ জোড়া বহুবার পুনরায় গণনা হয়), কিন্তু lcs_dp-এর call সংখ্যা ঠিক $m \times n$ — প্রতিটি $(i, j)$ জোড়া ঠিক একবার গণনা হয়। এটিই ডায়নামিক প্রোগ্রামিং-এর সম্পূর্ণ পয়েন্ট — ওভারল্যাপিং সাবপ্রবলেম চিনে পুনঃগণনা সম্পূর্ণ বাদ দেওয়া।
মূল কথা · Key takeaway

কমপ্লেক্সিটি থিওরি শুধু "এটি সম্ভব কি না" প্রশ্নের বাইরে গিয়ে প্রতিদিনের অ্যালগরিদম-ডিজাইন সিদ্ধান্তে সরাসরি প্রভাব ফেলে — একটি সমস্যা NP-কমপ্লিট চেনা সময় বাঁচায়, ওভারল্যাপিং সাবপ্রবলেম চেনা এক্সপোনেনশিয়াল সমাধানকে পলিনমিয়ালে রূপান্তরিত করে, আর গ্রিডি বনাম DP-এর মধ্যে সঠিক পছন্দ একজন ইঞ্জিনিয়ারের বাস্তব দক্ষতা নির্ধারণ করে।

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

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

প্র ০১ "ওভারল্যাপিং সাবপ্রবলেম" ঠিক কী বোঝায়, এবং কোড সেলে এটি কীভাবে চোখে দেখা যায়?

ওভারল্যাপিং সাবপ্রবলেম মানে একই ছোট সাবপ্রবলেম (এখানে, একটি নির্দিষ্ট $(i, j)$ ইনডেক্স জোড়ার LCS) সমাধানের প্রয়োজন বহুবার পড়ে, ভিন্ন ভিন্ন কলিং-পাথ থেকে। কোড সেলে এটি সরাসরি দেখা যায় — lcs_naive-এর calls কাউন্টার $m \times n$-এর চেয়ে বহুগুণ বড় সংখ্যায় পৌঁছায় (যেমন "ABCBDAB"/"BDCABA"-এ ১৫২টি কল, যেখানে টেবিলের আকার মাত্র ৭×৬=৪২), যা প্রমাণ করে একই সাবপ্রবলেম বহুবার পুনরায় সমাধান করা হচ্ছে।

প্র ০২ প্রতিটি এক্সপোনেনশিয়াল-দেখতে অ্যালগরিদমকেই কি DP দিয়ে পলিনমিয়ালে রূপান্তর করা সম্ভব?

না। DP শুধু তখনই কাজ করে যখন সমস্যাটি সত্যিই ওভারল্যাপিং সাবপ্রবলেম ও অপ্টিমাল সাব-স্ট্রাকচার (optimal substructure — একটি সমস্যার অপ্টিমাল সমাধান তার সাবপ্রবলেমগুলোর অপ্টিমাল সমাধান থেকে গঠিত হয়) ধারণ করে। যদি কোনো সমস্যা সত্যিকারের NP-হার্ড হয় (M10/L45-L47, যেমন SAT বা TSP), তাহলে তার সাবপ্রবলেমগুলো একে অপরের সাথে এমনভাবে জড়িত থাকে যে কোনো পলিনমিয়াল-সংখ্যক ভিন্ন সাবপ্রবলেম দিয়ে পুরো সমাধান তৈরি করা যায় না — DP প্রয়োগ করলেও এক্সপোনেনশিয়াল সংখ্যক টেবিল-এন্ট্রি প্রয়োজন হয়ে পড়ে (M11/L49-এর তত্ত্ব অনুযায়ী, যদি P ≠ NP)।

প্র ০৩ গ্রিডি অ্যালগরিদম যখন ভুল উত্তর দিতে পারে, তখনও কেন কখনো কখনো এটি ব্যবহারযোগ্য?

কারণ M11/L50-এর মতো ক্ষেত্রে গ্রিডি অ্যালগরিদম নিখুঁত অপ্টিমাল নাও হলেও একটি প্রমাণিত সীমার মধ্যে (যেমন vertex-cover-এ সবসময় অপ্টিমালের সর্বোচ্চ ২ গুণের মধ্যে) থাকার গ্যারান্টি দিতে পারে, এবং সেই গ্যারান্টি পেতে সময় লাগে মাত্র পলিনমিয়াল — যেখানে নিখুঁত সমাধান খুঁজতে এক্সপোনেনশিয়াল সময় লাগতে পারে। বাস্তব ইঞ্জিনিয়ারিং-এ "৯৫% ভালো, তাৎক্ষণিক" প্রায়ই "১০০% নিখুঁত, কয়েক শতাব্দী পরে" এর চেয়ে অনেক বেশি উপযোগী।

অনুশীলন

  1. চিন্তা করুন: ফিবোনাচ্চি সংখ্যা গণনার naive recursive সংস্করণও (fib(n) = fib(n-1) + fib(n-2)) এক্সপোনেনশিয়াল সময় নেয় — কেন? এবং কীভাবে DP দিয়ে এটি রৈখিক (linear) সময়ে সমাধান করা যায়?

    কারণ fib(n-1) ও fib(n-2) উভয়ই আবার fib(n-3) কল করে (এবং আরও গভীরে), ফলে একই সাবপ্রবলেম বহুবার পুনরায় গণনা হয় — ঠিক LCS-এর মতোই ওভারল্যাপিং সাবপ্রবলেম। DP সমাধান: একটি array-তে fib(0) থেকে fib(n) পর্যন্ত bottom-up ক্রমে গণনা করে সংরক্ষণ করা, প্রতিটি মান ঠিক একবার গণনা করে — মোট $O(n)$ সময়, $O(2^n)$-এর তুলনায় বিশাল উন্নতি।

  2. পরীক্ষা করুন: উপরের কোড সেলে pairs তালিকায় দুটি দীর্ঘতর স্ট্রিং (যেমন দৈর্ঘ্য ১০-১২) যোগ করে Run চেপে দেখুন speedup অনুপাত কীভাবে আরও বৃদ্ধি পায়।

    স্ট্রিং যত লম্বা হবে, lcs_naive-এর call সংখ্যা তত দ্রুত (এক্সপোনেনশিয়ালভাবে) বাড়বে, কিন্তু lcs_dp-এর call সংখ্যা শুধু $m \times n$-এর সাথে (পলিনমিয়ালভাবে) বাড়বে — ফলে speedup অনুপাত ইনপুট দৈর্ঘ্য বাড়ার সাথে সাথে দ্রুত বৃদ্ধি পাবে, ঠিক যেমন উপরের উদাহরণগুলোতে ৩.৬x থেকে ৮.২x পর্যন্ত পার্থক্য দেখা গিয়েছিল ছোট স্ট্রিং-এই।

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

আগের পাঠ
কম্পিউটেবিলিটি ও ক্রিপ্টোগ্রাফি