Dynamic programming — টেবিল কীভাবে উত্তর তৈরি করে¶
অধ্যায়ের সূচি · সহজ ব্যাখ্যা ও বাস্তব ব্যবহার · অনুশীলন ও সমাধান
DP-তে প্রতিটি cell একটি ছোট প্রশ্নের উত্তর রাখে। নতুন cell হিসাব করতে আগেই জানা cell ব্যবহার করি। আগে প্রশ্নটি কী, তারপর কোন cell থেকে কোন cell-এর মান আসছে, তা দেখো।
Grid paths — ওপরের ও বাঁয়ের উত্তর যোগ¶
একটি 3×3 grid-এ ওপরের বাঁ কোণ থেকে নিচের ডান কোণে যেতে হবে। শুধু right ও down চলা যায়। dp[r][c] মানে ওই ঘরে পৌঁছানোর পথসংখ্যা; তাই dp[r][c] = ওপরের মান + বাঁয়ের মান।
১. শুরুতে দাঁড়ানোর একটি উপায়
চিহ্নিত ঘর: 1········Base case dp[0][0] = 1। বিন্দু মানে এখনো হিসাব হয়নি।
২. প্রথম সারি
1চিহ্নিত ঘর: 1চিহ্নিত ঘর: 1······ওপরের ঘর নেই; শুধু বাঁ দিক থেকে আসা যায়। তাই দুটো মানই 1।
৩. দ্বিতীয় সারি
111চিহ্নিত ঘর: 1চিহ্নিত ঘর: 2চিহ্নিত ঘর: 3···বাঁ থেকে ডানে 1, তারপর 1+1 = 2, তারপর 1+2 = 3।
৪. শেষ সারি
111123চিহ্নিত ঘর: 1চিহ্নিত ঘর: 3চিহ্নিত ঘর: 6মানগুলো 1, তারপর 2+1 = 3, তারপর 3+3 = 6। উত্তর 6টি পথ।
শেষ ঘরের 6 কোথা থেকে এলো:
flowchart TD
top["ওপর: dp[1][2] = 3"] -->|নিচে আসা| result["dp[2][2] = 3 + 3 = 6"]
left["বাঁয়ে: dp[2][1] = 3"] -->|ডানে আসা| resultওপর থেকে আসা পথ আর বাঁ থেকে আসা পথ আলাদা; তাই যোগ করা যায়। যাচাই করার আরেক উপায়: মোট চার move-এ দুইটি right ও দুইটি down। চার জায়গায় দুইটি right বসানোর উপায় C(4,2)=6।
Coin change — আগের amount-এর সেরা উত্তর নাও¶
Coins [1,2,5] দিয়ে amount 11 বানাতে সর্বনিম্ন কয়টি coin লাগে? প্রতিটি coin যতবার দরকার ব্যবহার করা যায়। dp[a] হলো amount a-এর সর্বনিম্ন coin সংখ্যা।
Base case dp[0]=0। যে coin c ≤ a, শুধু সেটির জন্য dp[a−c]+1 candidate। তাই এই উদাহরণে dp[a] = 1 + min(dp[a−1], dp[a−2], dp[a−5]), তবে negative index-এর term বাদ দিতে হবে।
শুরু — শুধু amount 0-এর উত্তর জানা
চিহ্নিত ঘর: 001∞2∞3∞4∞5∞6∞7∞8∞9∞10∞11∞∞ মানে এখনো কোনো সমাধান পাওয়া যায়নি।
Amount 5 পর্যন্ত হিসাব
0011213242চিহ্নিত ঘর: 516∞7∞8∞9∞10∞11∞Amount 5-এ একটি 5-coin নিলেই হয়; পাঁচটি 1-coin নেওয়ার দরকার নেই।
পুরো table — amount 11-এর উত্তর 3
00112132425162728393102চিহ্নিত ঘর: 113একটি সর্বোত্তম সমাধান 5+5+1।
প্রতিটি amount-এর হিসাব নিচে আছে। Candidate column-এ coin 1, 2, 5 নেওয়ার আগের amount-এর dp value দেখানো হয়েছে; শেষে একটি coin যোগ হয়।
| Amount a | আগের dp values | 1 + min(...) |
একটি সর্বোত্তম বাছাই |
|---|---|---|---|
| 1 | dp[0]=0 |
1 | 1 |
| 2 | dp[1]=1, dp[0]=0 |
1 | 2 |
| 3 | dp[2]=1, dp[1]=1 |
2 | 2+1 |
| 4 | dp[3]=2, dp[2]=1 |
2 | 2+2 |
| 5 | dp[4]=2, dp[3]=2, dp[0]=0 |
1 | 5 |
| 6 | dp[5]=1, dp[4]=2, dp[1]=1 |
2 | 5+1 |
| 7 | dp[6]=2, dp[5]=1, dp[2]=1 |
2 | 5+2 |
| 8 | dp[7]=2, dp[6]=2, dp[3]=2 |
3 | 5+2+1 |
| 9 | dp[8]=3, dp[7]=2, dp[4]=2 |
3 | 5+2+2 |
| 10 | dp[9]=3, dp[8]=3, dp[5]=1 |
2 | 5+5 |
| 11 | dp[10]=2, dp[9]=3, dp[6]=2 |
3 | 5+5+1 |
Amount 11-এর তিনটি candidate-কে ছবিতে দেখলে:
flowchart TD
a["dp[10] = 2"] -->|coin 1: মোট 3| result["dp[11] = min(3, 4, 3) = 3"]
b["dp[9] = 3"] -->|coin 2: মোট 4| result
c["dp[6] = 2"] -->|coin 5: মোট 3| resultশুধু coin সংখ্যা নয়, আসল coin-ও ফেরত পেতে হলে winning choice ধরে পিছিয়ে যাই। Tie হলে যেকোনো সর্বোত্তম choice নেওয়া যায়। এখানে coin 5 আগে বেছে নিচ্ছি:
flowchart LR
a["amount 11"] -->|নাও 5| b["amount 6"]
b -->|নাও 5| c["amount 1"]
c -->|নাও 1| d["amount 0 · শেষ"]জমা coin 5,5,1; সংখ্যা 3। DP table সেরা মান রাখে; winning choice-এর পথ আসল সমাধান ফিরিয়ে দেয়।
Memoization — একই প্রশ্নের উত্তর আবার হিসাব নয়¶
সাধারণ recursive fib(5)-এ প্রতিটি box একটি function call। একই label একাধিক box-এ আছে, কারণ একই প্রশ্নের উত্তর বারবার হিসাব হচ্ছে। মোট 15টি call।
flowchart TD
a["fib(5)"] --> b["fib(4)"]
a --> c["fib(3)"]
b --> d["fib(3)"]
b --> e["fib(2)"]
c --> f["fib(2)"]
c --> g["fib(1)"]
d --> h["fib(2)"]
d --> i["fib(1)"]
e --> j["fib(1)"]
e --> k["fib(0)"]
f --> l["fib(1)"]
f --> m["fib(0)"]
h --> n["fib(1)"]
h --> o["fib(0)"]Memoization-এ প্রথমবার পাওয়া উত্তর cache-এ রাখি; পরের call একই প্রশ্ন করলে সেই উত্তর সরাসরি ফেরত দিই। নিচে বাঁয়ের call আগে চলে এবং base case-এর উত্তরও cache হয়। মোট 9টি call; ছয়টি আলাদা value হিসাব হয়, তিনটি cache hit।
flowchart TD
a["fib(5)"] --> b["fib(4)"]
a --> c["fib(3) · cache hit"]
b --> d["fib(3)"]
b --> e["fib(2) · cache hit"]
d --> f["fib(2)"]
d --> g["fib(1) · cache hit"]
f --> h["fib(1)"]
f --> i["fib(0)"]Cache hit-এর নিচে আর subtree নেই। বড় n-এ এই ছোট পার্থক্যই বিশাল কাজ বাঁচায়।
| n | সাধারণ recursion-এর call | Memoization-এর call, cache hit-সহ |
|---|---|---|
| 5 | 15 | 9 |
| 10 | 177 | 19 |
| 20 | 21,891 | 39 |
| 50 | 40,730,022,147 | 99 |
এই সাধারণ fib(n−1)+fib(n−2) recursion-এ naive call সংখ্যা 2F(n+1)−1; memoized version-এ n≥2 হলে 2n−1। Function call মানেই নতুন state হিসাব নয়—cache hit-ও call হিসেবে গোনা হয়েছে।
নতুন DP-তে এই পাঁচটি প্রশ্ন করো¶
- State: প্রতিটি cell ঠিক কোন প্রশ্নের উত্তর রাখে?
- Base: কোন ছোট উত্তরগুলো আগে থেকেই জানা?
- Transition: কোন আগের cell থেকে বর্তমান উত্তর আসবে?
- Order: যাদের উত্তর লাগবে, তারা আগে হিসাব হচ্ছে তো?
- Answer: কোন cell-এ চূড়ান্ত মান, এবং choice ফিরিয়ে আনতে হবে কি?
Code লেখার আগে ছোট input-এ পুরো table ভরো। Grid-এ একটি দেয়াল বসালে base/transition কী বদলায়, অথবা coin 1 বাদ দিলে কোন amount অসম্ভব হয়—এ দুটি পরিবর্তন নিজে trace করলে state-এর অর্থ আরও পরিষ্কার হবে।