Dynamic Programming — একবার হিসাব, বারবার ব্যবহার
Dynamic Programming
DP-র পুরো খেলা দুইটা শর্ত আর একটা শৃঙ্খলায়: একই subproblem বারবার এলে একবারই হিসাব করে জমা রাখো — state-এর অর্থ লিখে, recurrence সাজিয়ে, space চেপে।
আগে এটা বুঝি
গত লেসনে আমরা fib_naive-কে exponential অপচয়ে ধরা খেতে দেখলাম — একই subproblem হাজারবার হিসাব হচ্ছিল, আর একটা dict ধার দিলেই খরচ linear হয়ে গেল। আজ সেই এক-লাইনের trick-টাই পূর্ণ প্রকৌশল: Dynamic Programming।
নামটা grandiose, ভেতরের কথাটা গণিতের চেয়ে হিসাবি লোভ: যদি একই ছোট সমস্যা বারবার ফিরে আসে, তাহলে প্রথমবার হিসাব করে answer-টা জমা রাখো — পরে সব সময় lookup করো। Bellman ১৯৫০-এ এই নাম দিয়েছিলেন সরকারি funding পাওয়ার কৌশলে (“dynamic” শব্দটা তখন impressive শোনাত) — technique-এর সাথে নামের কোনো গভীর সম্পর্ক নেই!
আজকের লেসনের পথ: আগে DP-র দুইটা শর্ত — কখন এই জাদু খাটে, কখন খাটে না। তারপর state design-এর discipline — DP fail-এর ৯০% এখানে। তারপর পাঁচটা classic পূর্ণ walk-through: fibonacci → coin change → LIS → knapsack → edit distance। শেষে greedy-র ফাঁদ আর space compression।
মূল ধারণা
DP-র দুই শর্ত — দুটোই লাগবেই
শর্ত ১: Overlapping subproblems। বড় সমস্যাটা ভাঙলে এমন ছোট সমস্যা আসে যেগুলো বারবার একই রূপে ফিরে আসে। fib(n)-এ fib(k) প্রায় φⁿ বার ফেরে — এই duplicate-ই memo-র লাভের উৎস। Divide-and-conquer-এ (mergesort) subproblem unique — তাই সেখানে memo কিছুই বাঁচায় না, আর DP-ও দরকার নেই।
শর্ত ২: Optimal substructure। পুরো সমস্যার optimal solution-এর ভেতরে থাকা প্রতিটা subproblem-এর solution-ও সেই subproblem-এর optimal হতে হবে। Shortest path-এ এটা স্পষ্ট: A→B→C shortest path-এর মধ্যে A→B অংশটাও shortest। কিন্তু longest simple path-এ শর্তটা ভাঙে — তাই সেখানে DP নেই (NP-hard!)।
State design — dp[i] ঘরের অর্থ আগে লিখুন
DP-র code লেখা সহজ; state-এর অর্থ define করা কঠিন। শৃঙ্খলাটা:
- অর্থ লিখুন বাক্যে: “
dp[i]= ঠিক i-তম index-এ শেষ হওয়া increasing subsequence-এর সর্বোচ্চ দৈর্ঘ্য” — বাক্যটা না লিখে code শুরু করলে off-by-one আর অর্থ-বিভ্রান্তি অবধারিত। - Recurrence derive করুন অর্থ থেকে: dp[i]-এর মানে জানলে সেটা আগের কোন ঘরগুলো থেকে আসতে পারে — সূত্র নিজেই দাঁড়ায়।
- Base case ঠিক করুন — induction-এর base, গত লেসনের চুক্তি।
- Traversal order: dp[i] হিসাবের সময় যেসব ঘর দরকার, সেগুলো আগে ready থাকতে হবে — bottom-up-এর একমাত্র বাধ্যবাধকতা।
Top-down বনাম Bottom-up — একই গণিত, দুই execution
# Top-down: recursion + memo — দরকারি state-ই হিসাব হয়
def fib_td(n, memo={}):
if n <= 1: return n
if n not in memo:
memo[n] = fib_td(n - 1) + fib_td(n - 2)
return memo[n]
# Bottom-up: table ভরা loop-এ — সব state আগে থেকে order-এ
def fib_bu(n):
dp = [0, 1]
for i in range(2, n + 1):
dp.append(dp[i - 1] + dp[i - 2])
return dp[n]দুটোর recurrence হুবহু এক: dp[i] = dp[i-1] + dp[i-2]। পার্থক্য execution-এ:
| Top-down (memoized) | Bottom-up (tabulation) | |
|---|---|---|
| হিসাব হয় | দরকারি state-ই (lazy) | সব state (eager) |
| Stack | depth খায় — RecursionError ঝুঁকি | খায় না |
| Constant factor | dict/hash overhead | array access — দ্রুত |
| Transition বোঝা | natural (recursion-এর অনুবাদ) | order-ভাবতে হয় |
Production rule: ভাবতে top-down-এ শুরু, ship করা বড় input-এ bottom-up — stack-safe আর cache-friendly।
ভেতরে কী ঘটছে
Classic ১ → ২: Fibonacci থেকে Coin Change
Coin change-এর দুইটা আলাদা প্রশ্ন — এদের গুলিয়ে ফেলা DP-র সবচেয়ে পরিচিত bug:
- Minimum coins: amount 11 বানাতে সর্বনিম্ন কতটা coin লাগে? → মান-minimizing DP।
- Count ways: amount 11 বানানোর কত রকম উপায়? → গণনা-DP।
Coins {1, 5, 6, 9}, amount = 11। প্রথমটার state: “dp[a] = amount a বানানোর সর্বনিম্ন coin সংখ্যা”; recurrence: dp[a] = 1 + min(dp[a-c]) সব coin c ≤ a-এর ওপর। দ্বিতীয়টার state: “ways[a] = amount a বানানোর উপায় সংখ্যা”; আর এখানে order-of-loops meaning বদলে দেয়:
coins = [1, 5, 6, 9]
def min_coins(amount): # প্রশ্ন ১: সর্বনিম্ন সংখ্যা
INF = float('inf')
dp = [0] + [INF] * amount # dp[0]=0: শূন্য amount, শূন্য coin
for a in range(1, amount + 1):
for c in coins:
if c <= a and dp[a - c] + 1 < dp[a]:
dp[a] = dp[a - c] + 1
return dp[amount] if dp[amount] != INF else -1
def count_ways(amount): # প্রশ্ন ২: combination গণনা
dp = [1] + [0] * amount # dp[0]=1: একটাই উপায় — কিছুই না নেওয়া
for c in coins: # coin বাইরের loop ⇒ combination
for a in range(c, amount + 1):
dp[a] += dp[a - c]
return dp[amount] # {1,5,5} আর {5,1,5} একই গণ্য হয়
print(min_coins(11)) # 2 → 6+5
print(count_ways(11)) # 3 → {6,5},{1×6,5},{1×11}Coin-loop বাইরে থাকলে combination (ক্রম অগণনযোগ্য); amount-loop বাইরে থাকলে permutation ({1,5} আর {5,1} আলাদা গণ্য)। Loop-এর ক্রমই semantics — comment ছাড়া পড়লে ধরাই পড়ে না এই class-এর bug।
Classic ৩: Longest Increasing Subsequence — দুই যুগের গল্প
State: “lis[i] = i-তম element-এ শেষ হওয়া LIS-এর দৈর্ঘ্য।”
import bisect
def lis_quadratic(a):
"""O(n²): প্রতিটা i-এর জন্য সব আগের j scan"""
if not a: return 0
lis = [1] * len(a)
for i in range(len(a)):
for j in range(i):
if a[j] < a[i]:
lis[i] = max(lis[i], lis[j] + 1)
return max(lis)
def lis_nlogn(a):
"""O(n log n): tails[k] = দৈর্ঘ্য k+1-এর সবসময়-সম্ভাবী LIS-এর
সবচেয়ে ছোট tail — তালিকাটা সর্বদা sorted থাকে!"""
tails = []
for x in a:
pos = bisect.bisect_left(tails, x)
if pos == len(tails):
tails.append(x) # x সবার বড় — LIS বাড়ল
else:
tails[pos] = x # বড় tail-কে ছোট x-এ replace
return len(tails)tails sorted থাকে বলে binary search (bisect) খাটে — এটা DP-র
state-space-এর ওপর monotonicity exploit; প্রতিটা DP-তে এমন shortcut থাকে না, তাই O(n²)-ও মৃত জ্ঞান নয়।
উদাহরণ
Classic ৪: 0/1 Knapsack — 2D থেকে 1D-তে compression
সমস্যা: item-দের (weight, value) আছে; capacity W bag-এ সর্বোচ্চ value নিন — প্রতিটা item হয় নেবেন হয় বাদ (0/1)।
State (পূর্ণ সংস্করণ): “dp[i][w] = প্রথম i-টা item-এর মধ্যে, capacity w bag-এ সর্বোচ্চ value।” Recurrence:
dp[i][w] = dp[i-1][w] ← item i বাদ
= max(..., dp[i-1][w-wt_i] + val_i) ← item i নেওয়া (wt_i ≤ w)লক্ষ্য করুন: row i শুধু row i−1-এর ওপর নির্ভর — পুরো ইতিহাস না! তাহলে দুইটা row-ই যথেষ্ট। আরও সাহস করে একটাই row, তবে একটা ফাঁদ ডিঙাতে হবে:
def knapsack_1d(items, W):
"""items: list of (weight, value); O(nW) time, O(W) space"""
dp = [0] * (W + 1)
for wt, val in items:
for w in range(W, wt - 1, -1): # উল্টো দিকে! কেন?
dp[w] = max(dp[w], dp[w - wt] + val)
return dp[W]
items = [(2, 3), (3, 4), (4, 5), (5, 8), (9, 10)]
print(knapsack_1d(items, 20)) # 20উল্টো loop-ই 0/1-র প্রাণ: সোজা চললে dp[w - wt] এই item-এর নিজের already-updated মান পড়ত — অর্থাৎ item-টা একই row-এ দুইবার নেওয়া হতো (unbounded knapsack!)। উল্টো চললে dp[w-wt] সবসময় আগের item-দের জগৎ থেকে পড়ে — 0/1 constraint অক্ষুণ্ণ।
Classic ৫: Edit Distance — পূর্ণ matrix + traceback
দুই word-এর মধ্যে সর্বনিম্ন কতগুলো operation (insert/delete/replace) দিয়ে একটাকে আরেকটা বানানো যায়। State: “d[i][j] = প্রথম word-এর প্রথম i অক্ষর → দ্বিতীয় word-এর প্রথম j অক্ষর-এর distance।”
Build section-এ দুই বাংলা শব্দে এই matrix পুরো ভরে traceback-সহ alignment বের করব। এখন ছোট উদাহরণে হাতে: কথা → কোঠা:
"" ক ঠ আ
"" 0 1 2 3
ক 1 0 1 2
ঢ 2 1 1 2 ← ক↔ঢ replace না করে নিচে দেখুন
আ 3 2 2 1
d[3][3]=1: ক মিলে গেছে, থ→ঢ replace, আ মিলে গেছে।Recurrence: অক্ষর মিললে diagonal copy (d[i-1][j-1]), না মিললে তিন operation-এর min (+1): delete (up), insert (left), replace (diagonal)। Traceback = শেষ ঘর থেকে কোন neighbor নিজের মান দিয়েছে তা জিজ্ঞেস করে হাঁটা — প্রতিটা ধাপে একটা operation উন্মোচিত হয়।
ছবিতে
DP-র সিদ্ধান্ত-মানচিত্র আর space ladder
- ১. শর্ত যাচাইoverlapping subproblems + optimal substructure আছে?
- ২. State definedp[...] ঘরের অর্থ এক বাক্যে — index-এর মানে, কী minimize/maximize
- ৩. Recurrencestate-এর অর্থ থেকে transitions derive — base case সহ
- ৪. Order + implementationtop-down না bottom-up; dependency order মানলেই হয়
- ৫. Space compressdependency window দেখে row/variable কমান — traceback দরকার কি না ভেবে
পূর্ণ 2D table দুই-row rolling এক-row (ultra) কয়েক variable
──────────────── ──────────────── ──────────────── ────────────────
dp[n+1][W+1] prev[], cur[] dp[W+1] a, b, c
O(n·W) memory O(2W) O(W) O(1)
traceback ✓ value only value only value only
knapsack/edit edit distance knapsack 0/1 fibonacci
reconstruction (row রাখলে partial) (উল্টো loop = 0/1) সবচেয়ে সস্তা
নিয়ম: dp[i][...] কেবল dp[i-1][...] (বা dp[i-1][j-1])-এ নির্ভর করলেই
window কমানো যায় — dependency-ই memory-র চুক্তিপত্র।নিজে চালিয়ে দেখুন
fib-এর stopwatch আর knapsack-এর memory-meter — সময় আর স্থান, দুটোই মাপুন
import sys
import time
import tracemalloc
# ---------- part ১: fib timing ----------
sys.setrecursionlimit(10000)
def fib_naive(n):
if n <= 1: return n
return fib_naive(n - 1) + fib_naive(n - 2)
def fib_memo(n, _m={}):
if n <= 1: return n
if n not in _m:
_m[n] = fib_memo(n - 1) + fib_memo(n - 2)
return _m[n]
N = 35
t0 = time.perf_counter(); r1 = fib_naive(N); t1 = time.perf_counter() - t0
t0 = time.perf_counter(); r2 = fib_memo(N); t2 = time.perf_counter() - t0
assert r1 == r2
print(f'fib({N}) naive : {r1:>10} {t1*1000:>10.0f} ms')
print(f'fib({N}) memo : {r2:>10} {t2*1000:>10.3f} ms speedup {t1/t2:>12,.0f}x')
# ---------- part ২: knapsack 2D vs 1D memory ----------
def knapsack_2d(items, W):
n = len(items)
dp = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
wt, val = items[i - 1]
for w in range(W + 1):
dp[i][w] = dp[i - 1][w]
if wt <= w:
dp[i][w] = max(dp[i][w], dp[i - 1][w - wt] + val)
return dp[n][W]
def knapsack_1d(items, W):
dp = [0] * (W + 1)
for wt, val in items:
for w in range(W, wt - 1, -1): # reverse = 0/1 discipline
dp[w] = max(dp[w], dp[w - wt] + val)
return dp[W]
rng_items = [(i % 7 + 1, (i * 13) % 50 + 1) for i in range(200)]
W = 5000
assert knapsack_2d(rng_items, W) == knapsack_1d(rng_items, W)
tracemalloc.start()
v2 = knapsack_2d(rng_items, W)
_, peak2 = tracemalloc.get_traced_memory() # (current, peak)!
tracemalloc.reset_peak()
v1 = knapsack_1d(rng_items, W)
_, peak1 = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f'\nknapsack n=200, W=5000 → best value {v2}')
print(f' 2D peak memory: {peak2/1024:>10,.0f} KB')
print(f' 1D peak memory: {peak1/1024:>10,.0f} KB ratio {peak2/max(peak1,1):>6.1f}x')
# ---------- part ৩: coin change দুই প্রশ্ন ----------
coins = [1, 3, 4]
def min_coins(amount, coins):
INF = float('inf')
dp = [0] + [INF] * amount
for a in range(1, amount + 1):
for c in coins:
if c <= a and dp[a - c] + 1 < dp[a]:
dp[a] = dp[a - c] + 1
return dp[amount]
def greedy_coins(amount, coins):
count, rem = 0, amount
for c in sorted(coins, reverse=True):
count += rem // c
rem %= c
return count if rem == 0 else float('inf')
for amt in (6, 11, 23):
g, m = greedy_coins(amt, coins), min_coins(amt, coins)
flag = '✓' if g == m else '✗ GREEDY FAILS'
print(f'amount {amt:>2}: greedy={g} dp={m} {flag}')সাধারণ ফলাফল (laptop, Python 3.12):
fib(35) naive : 9227465 3061 ms
fib(35) memo : 9227465 0.040 ms speedup 76,661x
knapsack n=200, W=5000 → best value 5100
2D peak memory: 8,320 KB
1D peak memory: 44 KB ratio 189.1x
amount 6: greedy=3 dp=2 ✗ GREEDY FAILS
amount 11: greedy=3 dp=3 ✓
amount 23: greedy=6 dp=6 ✓পড়ার বিষয়:
১. fib-এর ~৭৭,০০০× speedup — একটা dict-ই φⁿ-কে n-এ নামিয়েছে। Overlapping subproblems থাকলে memo non-optional।
২. Knapsack-এর ~১৯০× memory saving — theory বলত n·W→W (এখানে 200×), বাস্তবে কাছাকাছি। বড় W-এ এই compression-ই feasible/infeasible-এর সীমানা ঠিক করে।
৩. Greedy amount 6-এ ব্যর্থ, কিন্তু 11 আর 23-এ ঠিক — এটাই সবচেয়ে বিপজ্জনক bug-প্রকৃতি: মাঝে মাঝে সঠিক greedy ভুল confidence শেখায়। একটা counterexample-ই claim ভাঙে; পরের লেসনের মূল থিসিস।
Memoization naive-এর exponential সময়কে linear-এ নামায় (n=35-এ হাজার-গুণ gap), আর rolling-row compression knapsack-এর memory n·W থেকে W-এ নামিয়ে আনে — tracemalloc-এ সংখ্যায় ধরা পড়ে।
নিজে বানান
Edit distance traceback + coin change দুই variant — দুইটা complete tool
- Full DP matrix ভরুন — insert/delete/replace তিন transition সহ
- Matrix print করে হাতে verify করুন ছোট উদাহরণে
- Traceback লিখুন: শেষ ঘর থেকে শুরুতে হেঁটে operation sequence বের করুন
- Alignment প্রিন্ট করুন — দুই লাইনে অক্ষর-বাই-অক্ষর match/replace/insert/delete
- Coin change-এর min-coins আর count-ways variant আলাদা function-এ লিখে test করুন
লক্ষ্য: দুই বাংলা শব্দের সম্পাদনা-পথ চোখে দেখা, আর coin change-এর দুই প্রশ্নের পার্থক্য কোডে বাঁধা।
def edit_distance_matrix(s1: str, s2: str):
"""পূর্ণ (len(s1)+1)x(len(s2)+1) matrix ফেরায় — traceback-এর জন্য"""
d = [[0] * (len(s2) + 1) for _ in range(len(s1) + 1)]
for i in range(len(s1) + 1):
d[i][0] = i # s2 খালি: সব delete
for j in range(len(s2) + 1):
d[0][j] = j # s1 খালি: সব insert
for i in range(1, len(s1) + 1):
for j in range(1, len(s2) + 1):
if s1[i - 1] == s2[j - 1]:
d[i][j] = d[i - 1][j - 1] # match — free
else:
d[i][j] = 1 + min(d[i - 1][j], # delete from s1
d[i][j - 1], # insert into s1
d[i - 1][j - 1]) # replace
return d
def trace_alignment(s1: str, s2: str):
"""matrix + traceback → (distance, ops list, aligned pair)"""
d = edit_distance_matrix(s1, s2)
i, j = len(s1), len(s2)
ops = []
top, bot = [], []
while i > 0 or j > 0:
if i > 0 and j > 0 and s1[i - 1] == s2[j - 1]:
ops.append('match'); top.append(s1[i - 1]); bot.append(s2[j - 1])
i, j = i - 1, j - 1
elif i > 0 and j > 0 and d[i][j] == d[i - 1][j - 1] + 1:
ops.append(f'replace {s1[i-1]}→{s2[j-1]}')
top.append(s1[i - 1]); bot.append(s2[j - 1])
i, j = i - 1, j - 1
elif i > 0 and d[i][j] == d[i - 1][j] + 1:
ops.append(f'delete {s1[i-1]}')
top.append(s1[i - 1]); bot.append('-')
i -= 1
else:
ops.append(f'insert {s2[j-1]}')
top.append('-'); bot.append(s2[j - 1])
j -= 1
return d[len(s1)][len(s2)], ops[::-1], (''.join(top[::-1]), ''.join(bot[::-1]))
def coin_min(amount: int, coins) -> int:
dp = [float('inf')] * (amount + 1); dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount]
def coin_count(amount: int, coins) -> int:
dp = [1] + [0] * amount
for c in coins: # coin outside = combinations
for a in range(c, amount + 1):
dp[a] += dp[a - c]
return dp[amount]
if __name__ == '__main__':
# ---------- bangla words ----------
w1, w2 = 'বইখানা', 'বইটা'
dist, ops, aligned = trace_alignment(w1, w2)
print(f'"{w1}" → "{w2}" distance = {dist}')
for op in ops:
print(f' {op}')
print(f' {aligned[0]}')
print(f' {aligned[1]}')
assert trace_alignment('kitten', 'sitting')[0] == 3
assert trace_alignment('flaw', 'lawn')[0] == 2
assert trace_alignment('', 'abc')[0] == 3
# ---------- coin change ----------
coins = [1, 5, 6, 9]
def _ways(cs, i, amt): # combination-counting oracle
if amt == 0:
return 1
if amt < 0 or i == 0:
return 0
return _ways(cs, i - 1, amt) + _ways(cs, i, amt - cs[i - 1])
def brute_min(amount):
if amount == 0:
return 0
best = float('inf')
for c in coins:
if c <= amount:
best = min(best, brute_min(amount - c) + 1)
return best
for amt in (0, 10, 11, 17):
gm, gc = coin_min(amt, coins), coin_count(amt, coins)
assert gm == brute_min(amt), (amt, gm)
assert gc == _ways(coins, len(coins), amt), (amt, gc)
print(f'amount {amt:>2}: min-coins={gm} ways={gc} (oracle-matched)')প্রত্যাশিত output:
"বইখানা" → "বইটা" distance = 3
match
match
delete খ
delete া
replace ন→ট
match
বইখানা
বই--টা
amount 0: min-coins=0 ways=1 (oracle-matched)
amount 10: min-coins=2 ways=5 (oracle-matched)
amount 11: min-coins=2 ways=6 (oracle-matched)
amount 17: min-coins=3 ways=12 (oracle-matched)নিজে বাড়ান:
১. Operation cost ভিন্ন করুন (replace=1.5, insert/delete=1) —
weighted edit distance; spell-checker-এ keyboard-distance cost যোগ করুন।
২. LCS (longest common subsequence)-এ adapt করুন — diff tools-এর
মূল ইঞ্জিন; alignment-এ match-only প্রিন্ট।
৩. coin_min-এ reconstruction যোগ করুন — কোন কোন coin নেওয়া হলো
parent-pointer array দিয়ে।
বাস্তব সিস্টেমে
DP বাস্তবে কোথায়
Bioinformatics। DNA/protein sequence alignment — edit distance-এরই scoring-যুক্ত রূপ (Smith-Waterman local, Needleman-Wunsch global); BLAST-এর heuristic shortcut এসেও exact alignment-এর ভিত্তি এই matrix।
Diff আর resource allocation। git diff-এর ভেতরে Myers algorithm — LCS-variant DP, দুই file-এর ন্যূনতম edit script এই traceback-ই। Knapsack family সরাসরি business-এ: ad-budget across campaigns, cloud instance sizing, cargo loading; 0/1 বনাম unbounded-এর সিদ্ধান্ত বাস্তব rule।
Speech আর NLP। Speech recognition-এর Viterbi algorithm — HMM-এর ওপর DP; spelling correction-এ edit distance; machine translation-এর decoding DP-family।
Finance আর compilers। Option pricing-এর binomial lattice — backward-induction DP; inventory management-এর multi-stage optimization; register allocation (graph coloring-এর DP variants), image seam-carving (energy DP) — Photoshop-এর content-aware resize-এর ভেতরটা একটা সরল vertical-path DP।
যে ভুলগুলো সবাই করে
“DP মানেই দ্রুত — যেকোনো সমস্যায় DP apply করলেই performance আসবে।”
DP শুধু overlapping-subproblem অপচয় দূর করে — মূল state-space-এর size যদি exponential হয় (যেমন TSP-এর 2ⁿ states), DP তবু exponential-ই থাকে (TSP-DP: O(n²2ⁿ) — brute force n!-এর তুলনায় ভালো, কিন্তু polynomial নয়)। সঠিক বাক্য: DP খরচকে state-space-এর size × transition-cost-এ নামায়; state-space polynomial না হলে জাদু নেই।
“Top-down আর bottom-up-এর asymptotic খরচ আলাদা হতে পারে।”
Recurrence এক হলে reachable-state set-ও এক — খরচ একই Θ। পার্থক্য constant-factor আর feasibility-তে: top-down lazy (দরকারি state-ই), কিন্তু Python-এ recursion-limit আর hash overhead; bottom-up eager কিন্তু array-fast আর stack-free। বড় input-এ bottom-up practical, তত্ত্বে সমান।
“Knapsack O(nW) — তাহলে pseudo-polynomial মানে polynomial।”
W একটা number, input-এর length না — W লিখতে লাগে log W bits। Input size N = n + log W-এর হিসেবে O(nW) exponential (W=2^(log W))। এজন্যই নাম pseudo-polynomial: সংখ্যা ছোট হলে বাস্তবে দ্রুত, কিন্তু NP-hardness অটুট — W-এর digit বাড়ালে খরচ বিস্ফোরিত।
“Greedy-ও তো optimal দেয় অনেক সময় — তাহলে DP-র ভারী machinery কেন?”
‘অনেক সময়’-এর মধ্যে কোথায় ব্যর্থ হয় তা আগাম বলা যায় না — coin-set {1,3,4}
amount 6-এ greedy ৩ দেয়, সঠিক ২ (আজকের experiment)। Greedy ব্যবহারের অনুমতি তখনই যখন প্রমাণ আছে (পরের লেসনের exchange argument)। প্রমাণ না থাকলে DP-ই correct-by-construction পথ — ধীর হতে পারে, ভুল হয় না।
বুঝেছেন কি না দেখুন
1DP-র দুইটা পূর্বশর্ত কী কী? প্রতিটার একটা counterexample দিন — কোন সমস্যা কোন শর্ত ভাঙে।
স্মরণ
শর্ত ১ overlapping subproblems — ভাঙে mergesort: subproblem unique, memo শূন্য লাভ (divide-and-conquer যথেষ্ট)। শর্ত ২ optimal substructure — ভাঙে longest simple path: পুরো path-এর ভেতরের segment নিজের দুই endpoint-এর মধ্যে longest নাও হতে পারে, তাই DP state সংরক্ষণ করতে পারে না।
2coin_min-এ dp[0]=0 কেন? এটা ভুল করে dp[0]=1 লিখলে amount 5, coins
{1,5}-এ উত্তর কী হবে?
প্রয়োগ
{1,5}-এ উত্তর কী হবে?dp[0]=0 মানে: amount শূন্য বানাতে শূন্যটা coin — induction-এর base। dp[0]=1 দিলে recurrence ধরে নেয় শূন্য amount-এও একটা coin খরচ — প্রতিটা chain-এ একটা phantom coin যোগ হয়: amount 5-এ উত্তর আসবে 2 (সঠিক 1)। Base case-এর off-by-one পুরো table দূষিত করে — এজন্যই state-এর অর্থ আগে লেখা বাধ্যতামূলক।
3count_ways-এ loop-order swap করলে (amount বাইরে, coin ভেতরে) উত্তর কেন বদলে যায়? amount=3, coins={1,2} দিয়ে দেখান।
যুক্তি
{1,2} দিয়ে দেখান।Coin-outside: {1,2}-এর সব subset আগে সেট হয় — combination: 2 উপায় ({1,1,1},{1,2})। Amount-outside: প্রতিটা amount-এ সব coin ক্রমে try করে — permutation গণনা: {1,2} আর {2,1} আলাদা — 3 উপায় ({1,1,1},{1,2},{2,1})। Loop-order ঠিক করে দেয় কোন dimension
‘selected so far’ আর কোনটা ‘next choice’ — semantics-ই structure-এ।
4Spell-checker-এর জন্য ১০⁵-word dictionary-তে প্রতিটা typo-র জন্য edit distance হিসাব করা ব্যয়বহুল। Design sketch দিন — কোন DP-insight কাজে লাগবে?
ডিজাইন
Insight ১: early termination — distance band: যদি |len(s1)-len(s2)| greater than k হয় skip; banded DP (Ukkonen) শুধু diagonal-band হিসাব করে O(k·min(m,n))। Insight ২: prefix pruning — dictionary trie-তে organize করে common-prefix reuse (একই prefix-এর সব word-এর matrix-row share)। Insight ৩: BK-tree/n-gram indexing দিয়ে candidate shortlist, তারপর exact banded DP শুধু candidate-দের ওপর। Production spell-checker তিনটার hybrid।
50/1 knapsack-এর 1D compression-এ inner loop উল্টো চলে কেন? Unbounded knapsack-এ সোজা চললে কী হয়?
স্মরণ
উল্টো চললে dp[w-wt] এখনো current item processing-এর আগের মান (আগের item-দের world) ধরে রাখে — item একবারই নেওয়া যায়। সোজা চললে dp[w-wt] ইতিমধ্যে current item-সহ updated — একই item বারবার নেওয়া হয়: unbounded knapsack-এর সঠিক code হয়ে যায়! Loop-direction-ই constraint encode করে।
6Edit distance-এর traceback-এ একাধিক valid alignment থাকতে পারে কি? কখন, আর code কোনটা বেছে নেয়?
যুক্তি
হ্যাঁ — tie হলে (delete আর replace দুটোই একই total দেয়) multiple optimal paths থাকে। Example: ‘ab’→‘ba’: delete-a-insert-a বা replace-ab→ba — distance 2-ই। Code deterministic tie-break করে: match first, তারপর replace, delete, insert priority — তাই একটা নির্দিষ্ট alignment আসে। Bioinformatics-এ সব optimal alignments চাইলে backtracking branching করতে হয়।
এরপর কী
এরপর কী
DP সবসময় optimal, কিন্তু মাঝে মাঝে অপ্রয়োজনীয়ভাবে ভারী — যেসব সমস্যায় একটা স্থানীয় সিদ্ধান্তই global optimum-এর guarantee দিতে পারে, সেখানে greedy হাজার গুণ দ্রুত। প্রশ্নটা তখন proof-এর: কখন local সিদ্ধান্তে global জয় নিশ্চিত? পরের লেসনে greedy choice property, exchange argument-এর step-by-step প্রমাণ-কৌশল, Kruskal-এর প্রমাণিত greedy, আর Huffman coding-এর পূর্ণ walkthrough — সাথে greedy কখন approximation মাত্র তার honesty। আজকের coin-set {1,3,4} ফাঁদটা তখন formal counterexample culture হয়ে উঠবে।
আরও পড়ুন
- Introduction to Algorithms (CLRS), Chapters 14 ও 15 — Dynamic Programming — Thomas H. Cormen et al. · LCS, rod cutting, matrix-chain — state design আর traceback-এর প্রামাণ্য পাঠ
- Algorithms, Chapter 6 — Dynamic Programming — S. Dasgupta, C. Papadimitriou, U. Vazirani · DP-কে 'recursion + careful brute force' বলা উপস্থাপনা — এই লেসনের চেতনার কাছে