ডিপি প্যাটার্ন (DP Pattern)
ধরুন আপনি একটি বিশাল বড় জিগস পাজল (jigsaw puzzle) জোড়া লাগাচ্ছেন আর আপনি বারবার একই কোণার অংশটি সমাধান করছেন — যতবারই আপনি সেটিতে ফিরে আসছেন ততবারই। ঠিক এর কাজটাই মূলত সাধারণ রিকার্সন (naive recursion) করে থাকে। ডায়নামিক প্রোগ্রামিং (Dynamic Programming) বা (DP) বলে যে: প্রতিটি টুকরোকে একবারই সমাধান করুন, ফলাফলটিকে একটি স্টিকি নোটে বা কাগজে লিখে রাখুন এবং পরবর্তীতে আপনার যদি এটিকে আবার কখনো প্রয়োজন হয় তবে সেখান থেকেই শুধু দেখে নিন।
এই চমৎকার সরল ধারণাটি — শুধু ফলাফল ক্যাশ (cache) বা জমা করে রাখুন, কখনোই পুনরায় কম্পিউট (recompute) বা গণনা করবেন না — মূলত এক্সপোনেনশিয়াল অ্যালগরিদমগুলোকে (exponential algorithms) পলিনোমিয়াল বা বহুপদী (polynomial) অ্যালগরিদমে পরিণত করে বা বদলে দেয়।
এর জন্য প্রয়োজনীয় দুটি বৈশিষ্ট্য (Two Properties You Need)
ওভারল্যাপিং সাব-প্রোব্লেম বা উপ-সমস্যা (Overlapping Subproblems): এখানে রিকার্সিভ সমাধানটি (recursive solution) মূলত একই সাব-প্রোব্লেম বা উপ-সমস্যাগুলোকেই বারবার কল (calls) করে থাকে। এর ক্লাসিক একটি উদাহরণ হলো: ফিবোনাচি নম্বর বা সংখ্যা (Fibonacci numbers)। F(5) গণনা করার জন্য মূলত F(4) এবং F(3)-কে কল করা হয়। আর আবার F(4) মূলত F(3) এবং F(2)-কে কল করে থাকে। এর ফলে F(3) দুবার এবং F(2) তিনবার গণনা করা হয়... কোনো স্টোর বা ক্যাশিং (caching) ছাড়া, শুধু F(50) কে গণনা করতেই প্রায় ১ ট্রিলিয়নের (trillion) বা এক লক্ষ কোটিরও বেশি ফাংশন কল করতে হয়। কিন্তু এর সাথে ক্যাশ (caching) যুক্ত করলে: শুধুমাত্র 50 টি স্বতন্ত্র বা ইউনিক (unique) কল করা লাগে, এবং কাজ শেষ।
অপ্টিমাল সাবস্ট্রাকচার (Optimal Substructure): যেকোনো বড় সমস্যার অপ্টিমাল বা সর্বোত্তম সমাধান মূলত তার ছোট সাব-প্রোব্লেম বা উপ-সমস্যার অপ্টিমাল বা সর্বোত্তম সমাধানের ওপর নির্ভর করেই তৈরি হয়ে থাকে। ধরি B-এর মাধ্যমে A থেকে C-তে যাওয়ার সবচেয়ে ছোট রুট বা শর্টেস্ট পাথ (Shortest path) বের করতে হবে: সেক্ষেত্রে A→B অংশটিকে অবশ্যই A→B-এর জন্য সবচেয়ে ছোট পাথ হতে হবে এবং B→C অংশটি কেউ অবশ্যই B→C-এর সবচেয়ে ছোট পাথ হতে হবে। যদি এর কোনো সাব-পাথ (sub-path) বা উপ-পথ সাব-অপ্টিমাল (suboptimal) বা অপরদর্শী বা সর্বোত্তম না হতো, তবে আপনি চাইলে সেটির উন্নতি ঘটিয়ে নিজের জন্য আরও ভালো একটি সামগ্রিক পাথ বা পথ পেতে পারতেন — যাকে বিপরীত বক্তব্য বা কনট্রাডিকশন (contradiction) বলা হয়। তাই এখানে অপ্টিমাল সাবস্ট্রাকচার বা বৈশিষ্ট্যটি টিকে থাকে বা কাজ করে।
উভয় বৈশিষ্ট্যই একত্রে যার অর্থ দাঁড়ায়: প্রতিটি সাব-প্রোব্লেমকে বা উপ-সমস্যাকে সর্বোত্তম উপায়ে (optimal fashion) ঠিক একবারই কম্পিউট (compute) বা গণনা করুন, তারপর জিনিসগুলোকে একত্রিত করুন এবং আপনার কাজ শেষ হয়ে যাবে।
ডিপি (DP) ডিজাইনের ফ্রেমওয়ার্ক (The DP Design Framework)
১. স্টেট ডিফাইন (Define the state) বা অবস্থা সংজ্ঞায়িত করুন: এখানে মূলত dp[i] (অথবা dp[i][j]) দিয়ে কী বোঝানো হয়েছে? সেটিকে খুব নিখুঁতভাবে বা প্রিসাইজলি (precise) লিখুন এবং প্রকাশ করুন। ভুল স্টেট (state) ডিফাইন বা সংজ্ঞায়িত করার মানে হলো → এর ডাউনস্ট্রিমে (downstream) থাকা সবকিছুই ভুল হবে।
২. রিকারেন্স বা ট্রানজিশনটি লিখুন (Write the recurrence / transition): dp[i]-কে ছোট ইনডেক্স বা সূচকের (indices) মাধ্যম ব্যবহার করে প্রকাশ করুন। এটি এখানকার সবচেয়ে কঠিন এবং সৃজনশীল (creative) একটি ধাপ বা স্টেপ।
• ফিবোনাচি (Fibonacci): dp[i] = dp[i-1] + dp[i-2]
• 0/1 ন্যাপস্যাক (Knapsack): dp[i][w] = max(dp[i-1][w], dp[i-1][w-wt[i]] + val[i])
• LCS বা লনজেস্ট কমন সাবসিকোয়েন্স: s1[i]==s2[j] হলে dp[i][j] = dp[i-1][j-1]+1 নতুবা max(dp[i-1][j], dp[i][j-1])
৩. বেস কেসগুলো সংজ্ঞায়িত বা ডিফাইন করুন (Define base cases): এগুলো হলো সবচেয়ে ছোট সাব-প্রোব্লেম বা উপ-সমস্যা, আপনি যেগুলোর উত্তর সরাসরি দিতে পারেন।
• ফিবোনাচি (Fibonacci): dp[0]=0, dp[1]=1
• ন্যাপস্যাক (Knapsack): সমস্ত w-এর জন্য dp[0][w]=0 (কোনো আইটেম নেই → তাই কোনো ভ্যালু (value) বা মান নেই)
৪. অর্ডার পূরণ করা (Fill order / Tabulation) বা মেমোইজ (Memoize) করা: হয় আপনি টেবিলটিকে বটম-আপ বা নিচ-থেকে-উপর পদ্ধতিতে (ট্যাবুলেশন / tabulation) পূরণ করতে পারেন, যেখানে প্রয়োজন হওয়ার আগেই সমস্ত ডিপেন্ডেন্সিগুলো (dependencies) কম্পিউট বা গণনা করা হয়, অথবা টপ-ডাউন বা উপর-থেকে-নিচ রিকার্সনে (memoization / মেমোইজেশন) গিয়ে সরাসরি একটি ক্যাশ বা মেমরিকে (cache) যোগ করে নিতে পারেন।
ডিপি (DP) বনাম গ্রিডি (Greedy) বনাম ডিভাইড অ্যান্ড কঙ্কার (Divide & Conquer)
এই তিনটি পদ্ধতিই সমস্যাগুলোকে ছোট টুকরোতে ভেঙে ফেলে, কিন্তু প্রত্যেকে তা আলাদাভাবে করে থাকে:
• ডিভাইড অ্যান্ড কঙ্কার (Divide & Conquer): সাব-প্রোব্লেমগুলো একে অপরের থেকে সম্পূর্ণ স্বাধীন (independent) থাকে — প্রতিটিকে একবার সমাধান করে তারপর একত্রিত করে। (উদাহরণ: মার্জ সর্ট বা Merge sort, ক্লোজেস্ট পেয়ার বা closest pair)
• গ্রিডি (Greedy): একটি স্থানীয়ভাবে সর্বোত্তম (locally-optimal) বা বুদ্ধিদীপ্ত পছন্দ করে, কখনোই পেছনে ফিরে তাকায় না। এটি কেবল তখনই কাজ করে যখন গ্রিডি-চয়েস (greedy-choice) বা বুদ্ধিদীপ্ত পছন্দের বৈশিষ্ট্যটি বজায় থাকে বা কাজ করে।
• ডিপি (DP): সাব-প্রোব্লেমগুলো ওভারল্যাপ (overlap) করে, তাই ফলাফলগুলোকে ক্যাশে (cache) স্টোর করে রাখে। প্রতিটি ধাপে থাকা সমস্ত অপশনগুলোর বা চয়েসগুলো (choices) বিবেচনা করে। গ্রিডি (greedy) পদ্ধতির তুলনায় এটি দশগুণ বেশি পাওয়ারফুল বা শক্তিশালী হলেও, এটি সাধারণত বেশ ধীরগতির (slower) হয়ে থাকে।
একটি সহজ নিয়ম বা রুল অফ থাম্ব (Rule of thumb) হলো: যেকোনো সমস্যায় প্রথমেই গ্রিডি (greedy) পদ্ধতি ট্রাই (try) বা চেষ্টা করে দেখা। তবে গ্রিডি বা বুদ্ধিদীপ্ত চয়েসের (greedy choice) ক্ষেত্রে যদি আপনি এর বিপরীতে কোনো কাউন্টার-অ্যাক্সাম্পল (counterexample) বা উত্তর খুঁজতে বা প্রমাণ করতে ব্যর্থ হন, তাহলে আপনার উচিত সরাসরি ডিপি-তে (DP) সুইচ করা বা বদলানো।
ডিপি ফ্রেমওয়ার্ক (DP Framework) — ফিবোনাচি (ফ্ল্যাট/Naive → মেমোইজ/Memo → টেবিল/Tab → স্পেস-সাশ্রয়ী/Space-Optimised)
Complexity
ছোট কুইজ
পড়া চালিয়ে যান