কেস স্টাডি — সঠিক অ্যালগরিদমিক প্যারাডাইম বেছে নেওয়া
এই পাঠে যা শিখবেন
- একটি সমস্যার বৈশিষ্ট্য দেখে কোন অ্যালগরিদমিক প্যারাডাইম প্রযোজ্য তা যাচাই করার একটি প্র্যাকটিক্যাল ফ্রেমওয়ার্ক
- কেন "গ্রিডি তো সবসময় দ্রুত ও সহজ" চিন্তাটা প্রমাণ ছাড়া বিপজ্জনক — একটি জেনুইন গণনা করা কাউন্টার-এক্সাম্পল
- কীভাবে সমস্যার স্কেল বদলে গেলে (instance ছোট থেকে বড়) সঠিক প্যারাডাইমও বদলে যায়
- M5 (গ্রিডি), M6 (DP), M8 (ব্যাকট্র্যাকিং/ব্রাঞ্চ-অ্যান্ড-বাউন্ড) ও M12 (অ্যাপ্রক্সিমেশন) — এই চারটি মডিউল একটি একক সিদ্ধান্ত-গাছে কীভাবে সাজে
১ · সমস্যা — একটি ডেলিভারি ট্রাকের দৈনিক শিডিউল
ধরা যাক একটি ছোট কুরিয়ার কোম্পানির একটি ট্রাক আছে এবং আজকের জন্য কয়েকটি প্রার্থী ডেলিভারি জব পাওয়া গেছে। প্রতিটি জবের একটি নির্দিষ্ট সময়-উইন্ডো (শুরু ও শেষ সময়) এবং একটি লাভ (profit) আছে — কিছু জব দূরের বা জরুরি ক্লায়েন্টের হওয়ায় বেশি লাভজনক, কিছু কম। ট্রাক একসাথে শুধু একটি জব করতে পারে, এবং দুটি জব একে অপরের সময়ে ওভারল্যাপ করলে দুটোই নেওয়া যায় না। লক্ষ্য: সামগ্রিক লাভ সর্বোচ্চ করা এমন একটি সেট জব বেছে নেওয়া।
এই সমস্যাটি দেখতে পুরোপুরি M5/L20-এর অ্যাক্টিভিটি সিলেকশন প্রবলেমের মতোই — কিন্তু একটি গুরুত্বপূর্ণ পার্থক্য আছে: L20-এ লক্ষ্য ছিল সংখ্যায় সর্বোচ্চ অ্যাক্টিভিটি বাছাই করা (সব অ্যাক্টিভিটির "মূল্য" সমান ধরে নেওয়া হয়েছিল), কিন্তু এখানে প্রতিটি জবের ভিন্ন ভিন্ন লাভ আছে। এই একটি পার্থক্যই পুরো সিদ্ধান্ত-প্রক্রিয়া বদলে দেয় — এটাই এই কেস স্টাডির মূল শিক্ষা।
২ · ধাপ ১ — দ্রুত গ্রিডি হিউরিস্টিক (এবং কেন এটি এখানে বিপজ্জনক)
L20-এ প্রমাণিত হয়েছিল যে আর্লিয়েস্ট-ফিনিশ-টাইম গ্রিডি নিয়ম (সবসময় সবচেয়ে আগে শেষ হওয়া, বর্তমানের সাথে সামঞ্জস্যপূর্ণ অ্যাক্টিভিটি বেছে নেওয়া) সর্বোচ্চ সংখ্যক অ্যাক্টিভিটি বাছাই করার জন্য অপটিমাল — এক্সচেঞ্জ আর্গুমেন্ট দিয়ে প্রমাণিত। প্ল্যানারের হাতে সময় কম থাকলে এই একই নিয়মটাই এখানে "স্বাভাবিকভাবে" প্রয়োগ করার লোভ হতে পারে — দ্রুত, সহজ, এবং ইতিমধ্যে-প্রমাণিত মনে হয়।
কিন্তু L20-এর প্রমাণটি নির্দিষ্টভাবে সংখ্যা সর্বোচ্চ করার জন্য, লাভ সর্বোচ্চ করার জন্য নয়। যখন জবগুলোর লাভ ভিন্ন ভিন্ন হয়, তখন এক্সচেঞ্জ আর্গুমেন্টের প্রমাণ আর প্রযোজ্য থাকে না — এবং M6/L56-এ (পরের পাঠে বিস্তারিত) আমরা দেখব "গ্রিডি অতীতে কাজ করেছিল" এটা প্রমাণ নয়। নিচের কোডে ঠিক এটাই ঘটতে দেখা যাবে।
৩ · ধাপ ২ — এক্স্যাক্ট সমাধান: ওয়েটেড ইন্টারভাল শিডিউলিং DP
যেহেতু একটি ট্রাকের জন্য প্রার্থী জবের সংখ্যা স্বাভাবিকভাবেই ছোট (একটি শিফটে হাতে গোনা কয়েকটি সম্ভাব্য জব), তাই M6-এর optimal substructure যুক্তি প্রয়োগ করা যায়: ফিনিশ-টাইম অনুযায়ী সাজানো জবগুলোর মধ্যে $i$-তম জব পর্যন্ত সর্বোচ্চ লাভ হয় হয় (ক) $i$-তম জব বাদ দিয়ে প্রথম $i-1$টির সর্বোচ্চ লাভ, অথবা (খ) $i$-তম জব নিয়ে তার সাথে সামঞ্জস্যপূর্ণ সর্বশেষ জব $p(i)$ পর্যন্ত সর্বোচ্চ লাভ যোগ করে:
$$dp[i] = \max\big(dp[i-1],\ \text{profit}_i + dp[p(i)]\big)$$এখানে $p(i)$ হলো ফিনিশ-টাইম অনুযায়ী সাজানো তালিকায় $i$-তম জবের সাথে সময়ে সামঞ্জস্যপূর্ণ (ওভারল্যাপ করে না) এমন সর্বশেষ জবের ইনডেক্স। এই রিকারেন্সেই ওভারল্যাপিং সাব-প্রবলেম দেখা যায় — $p(i)$ বিভিন্ন $i$-এর জন্য একই সাব-প্রবলেমে ফিরে আসতে পারে — তাই এটি একটি আসল DP, প্লেইন রিকার্সন নয়। এই সমাধান $O(n \log n)$ সময়ে চলে (সাজানো + প্রতিটি জবের জন্য বাইনারি সার্চে $p(i)$ বের করা) — নিচের কোডে ব্রুট-ফোর্সের বিপরীতে যাচাই করা হয়েছে।
import bisect
from itertools import combinations
# একটি ডেলিভারি ট্রাকের প্রার্থী জব: (শুরু, শেষ, লাভ)
jobs = [
(1, 4, 5), (3, 5, 6), (0, 6, 10), (5, 7, 4),
(6, 9, 5), (8, 10, 3), (2, 8, 100),
]
def compatible(a, b):
return a[1] <= b[0] or b[1] <= a[0]
def brute_force_optimal(jobs):
n = len(jobs)
best = 0
for r in range(n + 1):
for combo in combinations(range(n), r):
ok = all(compatible(jobs[i], jobs[j])
for x, i in enumerate(combo) for j in combo[x + 1:])
if ok:
best = max(best, sum(jobs[i][2] for i in combo))
return best
def greedy_earliest_finish(jobs):
# M5/L20-এর নিয়ম: সবচেয়ে আগে শেষ হওয়া, সামঞ্জস্যপূর্ণ জব বেছে নেওয়া
order = sorted(range(len(jobs)), key=lambda i: jobs[i][1])
chosen, last_finish = [], -1
for i in order:
s, f, p = jobs[i]
if s >= last_finish:
chosen.append(i)
last_finish = f
return sum(jobs[i][2] for i in chosen), chosen
def weighted_interval_dp(jobs):
n = len(jobs)
order = sorted(range(n), key=lambda i: jobs[i][1])
sj = [jobs[i] for i in order]
finishes = [f for _, f, _ in sj]
dp = [0] * (n + 1)
for i in range(1, n + 1):
s, f, profit = sj[i - 1]
p_idx = bisect.bisect_right(finishes, s, 0, i - 1)
dp[i] = max(dp[i - 1], profit + dp[p_idx])
return dp[n]
opt_bf = brute_force_optimal(jobs)
opt_dp = weighted_interval_dp(jobs)
g_profit, g_chosen = greedy_earliest_finish(jobs)
print("জব তালিকা (শুরু, শেষ, লাভ):", jobs)
print(f"ব্রুট-ফোর্স সর্বোচ্চ লাভ : {opt_bf}")
print(f"DP (weighted interval) লাভ : {opt_dp} (ব্রুট-ফোর্সের সাথে মিলল: {opt_dp == opt_bf})")
print(f"গ্রিডি (earliest-finish) লাভ : {g_profit} বাছাই করা জব index: {g_chosen}")
print(f"\nফাঁক (gap) = {opt_bf - g_profit} -- গ্রিডি অপটিমালের মাত্র {100 * g_profit / opt_bf:.1f}% অর্জন করেছে")
৪ · ধাপ ৩ — স্কেল বড় হলে কী বদলায়
উপরের সমস্যায় DP পুরোপুরি ব্যবহারযোগ্য ছিল কারণ একটি ট্রাকের জব-সংখ্যা ছোট এবং সাব-প্রবলেম গঠন সরল। কিন্তু কোম্পানিটি বড় হয়ে বহু ট্রাক ও শত শত সম্ভাব্য জব নিয়ে কাজ করতে শুরু করলে সমস্যাটি রূপান্তরিত হয় ভেহিকল-রাউটিং-সদৃশ একটি সমস্যায় — যেখানে কোন ট্রাক কোন জব নেবে এবং কোন ক্রমে যাবে তা একসাথে সিদ্ধান্ত নিতে হয়। এই সাধারণ রূপটি TSP ও সেট-কভার-ধর্মী সমস্যার সাথে গঠনগতভাবে সম্পর্কিত এবং ব্যবহারিকভাবে NP-hard (M11/L48-এর "NP-hard সমস্যা চেনা" চেকলিস্ট দিয়ে যাচাইযোগ্য)।
NP-hardness-এর মুখোমুখি হলে M11/L49-এর সিদ্ধান্ত-ফ্রেমওয়ার্ক অনুযায়ী দুটো পথ থাকে:
যদি ট্রাক ও জব-সংখ্যা এখনও ছোট (যেমন হাতে গোনা কয়েক ডজন), ব্রাঞ্চ-অ্যান্ড-বাউন্ড এখনও এক্স্যাক্ট সর্বোত্তম সমাধান বের করতে পারে, প্লেইন ব্রুট-ফোর্সের চেয়ে বহুগুণ কম সার্চ করে।
শত-হাজার জব হয়ে গেলে এক্স্যাক্ট সমাধান অবাস্তব। তখন মেট্রিক TSP-এর ২-অ্যাপ্রক্সিমেশনের মতো প্রমাণিত-গ্যারান্টিসহ অ্যালগরিদম, বা নিয়ারেস্ট-নেইবার-ধর্মী হিউরিস্টিক ব্যবহার করা হয় — L57-এর ক্যাপস্টোনে এই একই কৌশলগুলো বিস্তারিত দেখা যাবে।
একটি অ্যালগরিদমিক প্যারাডাইম কোনো সমস্যার জন্য "চিরস্থায়ীভাবে সঠিক" নয় — এটি সমস্যার নির্দিষ্ট বৈশিষ্ট্যের (সাব-প্রবলেম ওভারল্যাপ করে কিনা, এক্সচেঞ্জ আর্গুমেন্ট প্রযোজ্য কিনা, ইনস্ট্যান্স কত বড়, NP-hard কিনা) উপর নির্ভরশীল একটি সিদ্ধান্ত। একই "শিডিউলিং" পরিবারের সমস্যাতেই আমরা দেখলাম গ্রিডি (ভুল প্রয়োগে), এক্স্যাক্ট DP (সঠিক, ছোট স্কেলে), এবং ব্রাঞ্চ-অ্যান্ড-বাউন্ড/অ্যাপ্রক্সিমেশন (বড়, NP-hard স্কেলে) — সবগুলো একই সিদ্ধান্ত-গাছের অংশ। এই কোর্সের বাকি দুটি পাঠ (L56, L57) ঠিক এই দক্ষতাকেই আরও শক্ত করবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ L20-এর আর্লিয়েস্ট-ফিনিশ গ্রিডি প্রমাণিতভাবে অপটিমাল, তবু এই কেস স্টাডিতে তা ব্যর্থ হলো কেন — এটা কি L20-এর প্রমাণ ভুল ছিল?
না, L20-এর প্রমাণ সম্পূর্ণ সঠিক — কিন্তু সেটি একটি নির্দিষ্ট প্রশ্নের উত্তর দেয়: সর্বোচ্চ সংখ্যক সামঞ্জস্যপূর্ণ অ্যাক্টিভিটি বাছাই করা, যেখানে প্রতিটি অ্যাক্টিভিটির "মূল্য" সমান ধরে নেওয়া হয়। এই কেস স্টাডিতে প্রশ্নটা ভিন্ন — সর্বোচ্চ লাভ সর্বোচ্চ করা, যেখানে জবগুলোর লাভ ভিন্ন ভিন্ন। প্রমাণের এক্সচেঞ্জ আর্গুমেন্ট নির্দিষ্টভাবে "সংখ্যা" ধরে লেখা হয়েছিল বলেই তা "লাভ"-এর ক্ষেত্রে খাটে না। এটাই মূল শিক্ষা: একটি প্রমাণ ঠিক কোন প্রশ্নের উত্তর দেয় তা স্পষ্টভাবে বোঝা জরুরি, নাহলে ভুল প্রসঙ্গে প্রয়োগ করার ঝুঁকি থাকে।
প্র ০২ উপরের DP-টি $O(n \log n)$ সময়ে চলে, যেখানে ব্রুট-ফোর্স $O(2^n)$ (সব সাবসেট চেক করে)। তাহলে ব্রুট-ফোর্স কোড রাখার দরকার কী ছিল?
ব্রুট-ফোর্সটি এখানে উত্তর বের করার জন্য নয়, DP-র সঠিকতা যাচাই করার জন্য রাখা হয়েছে — এই কোর্সের একটি মূল নীতি (CLAUDE.md-এ বর্ণিত): DP-র রিকারেন্স ঠিক আছে কিনা তা নিশ্চিত করতে প্রতিটি নতুন DP-কে একটি স্বাধীন, সরল (কিন্তু ধীরগতির) পদ্ধতির বিপরীতে যাচাই করা উচিত। যেহেতু এখানে $n=7$ ছোট, ব্রুট-ফোর্স দ্রুত চলে এবং $103 = 103$ মিলে যাওয়াটাই আমাদের DP বাস্তবায়নে আত্মবিশ্বাস দেয় — বড় $n$-এ শুধু DP-ই ব্যবহারযোগ্য থাকবে।
প্র ০৩ বহু-ট্রাক সংস্করণে কেন সমস্যাটি "ব্যবহারিকভাবে NP-hard" হয়ে যায় — নাকি এটি এখনও DP দিয়ে সমাধানযোগ্য?
একটি ট্রাকের সংস্করণে সাব-প্রবলেমের গঠন সরল (ফিনিশ-টাইম অনুযায়ী একমাত্রিক ক্রম) বলেই পলিনমিয়াল-সময়ের DP সম্ভব হয়েছিল। বহু ট্রাকে একই সাথে "কোন ট্রাক" এবং "কোন ক্রমে" — এই দুটি সিদ্ধান্ত মিলিয়ে অবস্থা-স্থান exponentially বেড়ে যায় (প্রতিটি ট্রাকের রুট নিজেই একটি TSP-সদৃশ উপ-সমস্যা)। M11/L48-এর চেকলিস্ট অনুযায়ী এই ধরনের বহু-এজেন্ট রাউটিং সমস্যা ক্লাসিক NP-hard ভেহিকল-রাউটিং প্রবলেমের পরিবারভুক্ত — তাই ব্যবহারিক আকারে (কয়েকশ জব) এক্স্যাক্ট পলিনমিয়াল সমাধান আশা করা যায় না, ব্রাঞ্চ-অ্যান্ড-বাউন্ড বা অ্যাপ্রক্সিমেশনই বাস্তবসম্মত পথ।
অনুশীলন
-
চিন্তা করুন: উপরের কোডে জব তালিকা থেকে সবচেয়ে লাভজনক জবটি ($(2, 8, 100)$) সরিয়ে ফেললে
গ্রিডি ও DP-র ফলাফল কি একই হয়ে যাবে বলে আপনার ধারণা? কেন?
হ্যাঁ, সম্ভবত কাছাকাছি বা একই হবে — কারণ বাকি জবগুলোর লাভ তুলনামূলক কাছাকাছি মানের (৩ থেকে ১০), তাই "সংখ্যায় বেশি জব নেওয়া" মোটামুটি "লাভে বেশি" নেওয়ার কাছাকাছি ফল দেয়। গ্রিডি বিশেষভাবে ব্যর্থ হয় যখন একটি একক জবের লাভ বাকি সবগুলোর সম্মিলিত লাভের চেয়ে অনেক বেশি অথচ সময়ের দিক থেকে "কম আকর্ষণীয়" (দেরিতে শেষ হয়) — এটাই দেখায় গ্রিডির ব্যর্থতা নির্দিষ্ট ইনস্ট্যান্স-নির্ভর, সবসময় নয়।
-
পরীক্ষা করুন: উপরের কোড সেলে
jobsথেকে(2, 8, 100)এন্ট্রিটি মুছে Run চেপে আপনার অনুমান যাচাই করুন — নতুন গ্যাপ কত হয়?এন্ট্রিটি সরানোর পর ব্রুট-ফোর্স ও DP উভয়ই বাকি ছয়টি জবের মধ্যে সর্বোচ্চ লাভ খুঁজবে, আর গ্রিডিও একই ছয়টি জবের উপর কাজ করবে — ফলাফল আউটপুটে দেখা যাবে গ্যাপ অনেক ছোট হয়ে যায় (কারণ এখন কোনো একক জব "আউটলায়ার" হিসেবে বাকি সবাইকে ছাড়িয়ে যাচ্ছে না)। এটি নিশ্চিত করে যে গ্রিডির ব্যর্থতা লাভের ভিন্নতার মাত্রার সাথে সরাসরি সম্পর্কিত।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ অ্যাসিম্পটোটিক অ্যানালাইসিস, রিকারেন্স, ডিভাইড অ্যান্ড কনকার, গ্রিডি, DP, গ্রাফ অ্যালগরিদম, ব্যাকট্র্যাকিং, স্ট্রিং অ্যালগরিদম, অ্যামর্টাইজড অ্যানালাইসিস, NP-কমপ্লিটনেস ও অ্যাপ্রক্সিমেশন — কোর্স শেষের দিকে।
- অ্যালগরিদম অ্যানালাইসিসের সাধারণ ভুল পরের পাঠ এই কেস স্টাডিতে দেখা "প্রমাণ ছাড়া গ্রিডি" ভুলটিকে আরও সাধারণীকরণ করে আরও কয়েকটি সাধারণ অ্যানালাইসিস-ভুল কভার করা হবে।
- Data Structures & Algorithms কোর্স সহোদর কোর্স গ্রিডি ও DP-র ইমপ্লিমেন্টেশন ওয়াকথ্রু সেই কোর্সেই আছে — এই কোর্স সঠিকতা প্রমাণ ও প্যারাডাইম-নির্বাচনে গভীরে যায়।