কমপ্লেক্সিটি থিওরি ও অ্যালগরিদম ডিজাইন
এই পাঠে যা শিখবেন
- 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 সংখ্যায় নাটকীয় পার্থক্য।
# 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)$ জোড়া ঠিক একবার গণনা হয়। এটিই ডায়নামিক প্রোগ্রামিং-এর সম্পূর্ণ পয়েন্ট — ওভারল্যাপিং
সাবপ্রবলেম চিনে পুনঃগণনা সম্পূর্ণ বাদ দেওয়া।
কমপ্লেক্সিটি থিওরি শুধু "এটি সম্ভব কি না" প্রশ্নের বাইরে গিয়ে প্রতিদিনের অ্যালগরিদম-ডিজাইন সিদ্ধান্তে সরাসরি প্রভাব ফেলে — একটি সমস্যা 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-এ সবসময় অপ্টিমালের সর্বোচ্চ ২ গুণের মধ্যে) থাকার গ্যারান্টি দিতে পারে, এবং সেই গ্যারান্টি পেতে সময় লাগে মাত্র পলিনমিয়াল — যেখানে নিখুঁত সমাধান খুঁজতে এক্সপোনেনশিয়াল সময় লাগতে পারে। বাস্তব ইঞ্জিনিয়ারিং-এ "৯৫% ভালো, তাৎক্ষণিক" প্রায়ই "১০০% নিখুঁত, কয়েক শতাব্দী পরে" এর চেয়ে অনেক বেশি উপযোগী।
অনুশীলন
-
চিন্তা করুন: ফিবোনাচ্চি সংখ্যা গণনার 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)$-এর তুলনায় বিশাল উন্নতি। -
পরীক্ষা করুন: উপরের কোড সেলে
pairsতালিকায় দুটি দীর্ঘতর স্ট্রিং (যেমন দৈর্ঘ্য ১০-১২) যোগ করে Run চেপে দেখুন speedup অনুপাত কীভাবে আরও বৃদ্ধি পায়।স্ট্রিং যত লম্বা হবে,
lcs_naive-এর call সংখ্যা তত দ্রুত (এক্সপোনেনশিয়ালভাবে) বাড়বে, কিন্তুlcs_dp-এর call সংখ্যা শুধু $m \times n$-এর সাথে (পলিনমিয়ালভাবে) বাড়বে — ফলে speedup অনুপাত ইনপুট দৈর্ঘ্য বাড়ার সাথে সাথে দ্রুত বৃদ্ধি পাবে, ঠিক যেমন উপরের উদাহরণগুলোতে ৩.৬x থেকে ৮.২x পর্যন্ত পার্থক্য দেখা গিয়েছিল ছোট স্ট্রিং-এই।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ: কেস স্টাডি — বাস্তব রেগেক্স ইঞ্জিন ও তাদের সীমাবদ্ধতা পাঠ ৫৪ M13 মডিউলের প্রথম কেস স্টাডি — বাস্তব regex ইঞ্জিন কীভাবে ফরমাল regex-কে ছাড়িয়ে যায়, কী মূল্যে।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স DP ও গ্রিডি অ্যালগরিদমের বাস্তব প্র্যাক্টিক্যাল ইমপ্লিমেন্টেশন এই কোর্সে বিস্তারিত কভার করা হয়েছে।
- পুনরালোচনা: NP-হার্ডনেস মোকাবিলা পাঠ ৫০ অ্যাপ্রক্সিমেশন ও হেউরিস্টিকের বিস্তারিত আলোচনা, যা এই পাঠের গ্রিডি অ্যালগরিদম আলোচনার ভিত্তি।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ DFA/NFA থেকে টুরিং মেশিন, ডিসাইডেবিলিটি ও P বনাম NP পর্যন্ত সম্পূর্ণ কোর্স।