Greedy — local সিদ্ধান্তে global জয়, কখন সম্ভব
Greedy Algorithms and the Exchange Argument
Greedy প্রতিটা ধাপে locally best নেয় — global optimum তখনই নিশ্চিত যখন exchange argument-এ প্রমাণ আছে; Huffman সেই প্রমাণের সবচেয়ে সুন্দর জয়।
আগে এটা বুঝি
গত লেসনে DP দেখলাম — সব ভবিষ্যৎ সিদ্ধান্তের ওপর চোখ রেখে এখনকার সিদ্ধান্ত নেয়। Greedy ঠিক বিপরীত চরিত্র: ভবিষ্যৎ ভুলে যাও, এখনকার সবচেয়ে লাভজনক সিদ্ধান্তটাই নাও, কখনো ফিরে তাকাও না।
শুনতে reckless। আর অনেক ক্ষেত্রে সেটাই — coin change-এ greedy গত লেসনেই ধরা খেয়েছে (coins {1,3,4}, amount 6)। কিন্তু কিছু সমস্যায় এই recklessness provably optimal: interval scheduling, Kruskal-এর MST, Huffman coding। প্রশ্নটা তাহলে দাঁড়ায়: কোন সমস্যায় local সিদ্ধান্তে global জয় প্রমাণ করা যায়, আর কোথায় greedy-র আকর্ষণীয় ফলাফল শুধু approximation?
আজকের অস্ত্র একটাই: exchange argument — “greedy-র পছন্দ আর optimal solution-এর পছন্দ swap করলে যদি solution খারাপ না হয়, তাহলে greedy-র পথেও optimal solution আছে।” এই একটা কৌশল তিনটা সমস্যায় চালিয়ে দেখব, আর Huffman দিয়ে শেষ করব একটা পূর্ণ, measurable compression engine build করে।
মূল ধারণা
Greedy কখন entitlement পায় — দুই শর্ত
DP-র মতো greedy-রও দুইটা পূর্বশর্ত আছে:
- Greedy choice property: একটা locally-best সিদ্ধান্ত নেওয়ার পর বাকি সমস্যা এখনো সমাধানযোগ্য থাকে এমনভাবে যে সম্পূর্ণ solution optimal হতে পারে — অর্থাৎ প্রথম সিদ্ধান্তটা কখনো regret হয় না। DP-র সাথে পার্থক্য: greedy-র সিদ্ধান্ত আটকে যায় (subproblem আর পরিবর্তন হয় না), DP-র সিদ্ধান্ত সব option খোলা রাখে।
- Optimal substructure: গত লেসনেরই শর্ত — পুরো সমাধান optimal হলে তার অংশগুলোও নিজেদের subproblem-এ optimal।
দুই শর্ত মিললে প্রমাণের মানক কাঠামো: induction — প্রতিটা ধাপে greedy choice optimal solution-এর সাথে compatible দেখাও, exchange argument দিয়ে।
Exchange argument — প্রমাণের recipe
Recipe চার ধাপে:
- ধরো G = greedy-র solution, O = কোনো optimal solution।
- যদি G = O, কাজ শেষ। নাহলে প্রথম স্থান খুঁজো যেখানে দুটো আলাদা।
- Exchange: O-কে হাত না ভাঙে greedy-র choice-টা ঢোকাও — দেখাও নতুন O′ এখনো valid এবং মান খারাপ হয়নি।
- O′ এখন greedy-র সাথে আরও এক ধাপ বেশি মিলে গেছে; repeat করলে finite step-এ O → G। যেহেতু exchange মান কমায়নি, G-ও optimal।
এই কৌশলের সৌন্দর্য: পুরো search space-এর কোনো বিশ্লেষণ লাগে না — শুধু এক pair swap-এর local হিসাব।
Interval scheduling — recipe চালু করি
সমস্যা: n-টা activity (start, finish); একটাই room; সর্বোচ্চ কতটা non-overlapping activity নেওয়া যায়? তিনটা tempting greedy rule:
| Rule | কেন আকর্ষণীয় | ভাগ্য |
|---|---|---|
| যেটা আগে shuru হয় | chronological স্বাভাবিক মনে হয় | ✗ লম্বা activity পুরো day block করে |
| যেটা ছোট | কম জায়গা নেয় | ✗ দুই পাশে বিশাল ফাঁক ঢেকে দেয় |
| যেটা আগে শেষ হয় | room দ্রুত ফাঁকা ছাড়ে | ✓ প্রমাণযোগ্য optimal |
Earliest-finish rule-এর প্রমাণ exchange argument-এ:
ধরি f₁ = min সব finish time (greedy-র পছন্দ), O = কোনো optimal set।
O-এর প্রথম (earliest-finishing) activity ধরি j, finish fⱼ।
f₁ ≤ fⱼ — কারণ f₁ globally minimum।
তাহলে O-এ j-কে বাদ দিয়ে ১-কে ঢোকাই (O′):
১-এর finish আগে ⇒ ১-ও O-এর বাকিদের সাথে non-overlapping!
|O′| = |O| — খারাপ হয়নি।
O′ এখন greedy-র প্রথম পছন্দ মানে; recursion-এ বাকি timeline-এও একই। ∎লক্ষ্য করুন প্রমাণটা কোথাও counting বা case-analysis করেনি — একটাই swap-এর feasibility দেখাল। এটাই exchange argument-এর পুরো জাদু।
ভেতরে কী ঘটছে
Counterexample culture — greedy-র সীমানা মাপা
গত লেসনের coin-set {1,3,4} ফাঁদটা formalize করি। Greedy বড় coin আগে:
amount = 6, coins = {1,3,4}
greedy : 4 + 1 + 1 → ৩টা coin ✗
optimal: 3 + 3 → ২টা coin ✓একটা instance-এ ভাঙলেই theory সম্পূর্ণ — greedy-র correctness claim আর দাঁড়ায় না ঐ coin-system-এ। এটাই counterexample culture: claim প্রমাণ করতে না পারলে একটা ছোট instance-এ ভাঙার চেষ্টা করো; ভাঙলে প্রশ্ন বদলে যায় — ‘কোথায় খাটে’ থেকে ‘কোথায় খাটে না’-র characterization। Canonical coin systems ({1,5,10,25}) তবু greedy-safe — প্রমাণটা কঠিন এবং system-specific; নতুন coin system-এ হাত দেওয়ার আগে DP-ই safe default।
Kruskal — কেন এই greedy প্রমাণিত
Minimum spanning trees লেসনে Kruskal দেখেছেন: edge-গুলো weight-এ sort করে cycle তৈরি না করলে নাও। কেন এই লোভ নিরাপদ? Cut property (CLRS 23.1): যেকোনো partition-কে cut বলি; সব cut-crossing edge-র মধ্যে lightest-টা কোনো না কোনো MST-তে আছে।
Kruskal-এর প্রতিটা নেওয়া edge যখন নেওয়া হয়, তখন দুই endpoint different component-এ — মানে একটা live cut-এ crossing করছে, আর sorted order-এ সেটাই সবচেয়ে হালকা crossing candidate-দের মধ্যে। Cut property ⇒ সেই edge বাদ দিলে যেকোনো spanning tree-কে exchange করে MST বানানো যায় যাতে এই edge আছে — হুবহু আমাদের exchange template। Cycle check শুধু নিশ্চিত করে tree-property ভাঙছি না।
Huffman coding — প্রমাণিত greedy-র সিংহাসন
Text-এ character-দের frequency uneven — ‘ক’ হাজারবার, ‘ঁ’ একবার। Fixed-width code (যেমন 8-bit ASCII) সবাইকে সমান বিট দেয়; Huffman প্রশ্ন করে: frequent character-কে ছোট code দিলে total কত ছোট হয়? Constraint: prefix property — কোনো code আরেকটার prefix না, নাহলে decode ambiguous (‘a’=0,‘ab’=01 হলে ‘01…’ পড়তেই confusion)।
Prefix-free codes ↔ binary tree-এর leaves — এক-এক correspondence। Character-এর code length = leaf-এর depth। Total bits = Σ freq(c) · depth(c) — একে বলে weighted external path length; Huffman সেটা minimize করে।
Algorithm নিজেই greedy:
- প্রতিটা character একটা single-node tree, priority = frequency।
- Greedy choice: দুইটা lowest-frequency tree তুলে নাও।
- নতুন parent বানাও (freq = sum), queue-তে ফেরত দাও।
- একটা tree বাকি হলে সেটাই code-tree; left=0, right=1 path-ই code।
কেন lowest-first? Intuition: rare character-কে সবচেয়ে গভীরে (লম্বা code) ঠেলে দেওয়াই সস্তা। Exchange argument প্রমাণ করে এটা শুধু intuition না: optimal tree-তে least-frequent দুই sibling deepest level-এ থাকতে বাধ্য (swap করে cost কমানো না যায় বলে), আর তাদের parent-কে new character ভেবে smaller problem-এ recurse — induction complete।
Bangla text sample-এ এর expected length আর compression আজকের experiment-এ মাপব; encoder-decoder roundtrip build section-এ।
উদাহরণ
পূর্ণ walkthrough: ছোট alphabet-এ Huffman হাতে
Text: "আমার আম" (spaces সহ, সরলতার জন্য)। Frequency table:
char : আ ম র ' '
freq : 3 2 1 1 মোট char = 7Heap-driven merge trace:
step 0 pool: [আ:3] [ম:2] [র:1] [' ':1]
step 1 pop দুই smallest: র(1), ' '(1)
→ node N1(freq 2); pool: [N1:2] [ম:2] [আ:3]
step 2 pop N1(2), ম(2)
→ node N2(freq 4); pool: [আ:3] [N2:4]
step 3 pop আ(3), N2(4)
→ root(7)
tree (left=0, right=1):
root(7)
/ \
আ:3(0) N2:4(1)
/ \
N1:2(10) ম:11(111)...
/ \
র:100 ' ':101
codes: আ=0(1 bit) ম=111(3) র=100(3) ' '=101(3)
total = 3·1 + 2·3 + 1·3 + 1·3 = 15 bits
fixed-width (2-bit, 4 symbols): 7·2 = 14 bits — ছোট input-এ overhead!শেষ observation গুরুত্বপূর্ণ: ছোট input-এ header/tree-storage খরচে Huffman হারতে পারে — compression লাভ frequency-skew বড় হলেই আসে। Expected length formula: L = Σ pᵢ·len(cᵢ) — উপরের example-এ p(আ)=3/7 ⇒ L = (3·1+2·3+3+3)/7 ≈ 2.14 bits/char vs fixed 2.0 — skew কম বলে এখানে হার! বড় Bangla corpus-এ skew বিশাল — experiment-এ বাস্তব ratio দেখব।
frequency-skewed corpus (typical Bangla):
root
/ \
[e-type:40%] rest:60%
/ \
[আ-space:35%] 25%
/ \
[...] [রare chars]
depth: frequent ≈ 1-2 bits, rare ≈ 6-8 bits
expected length ≈ Σ p·depth << log₂(alphabet) = uniform baseline
entropy H = -Σ p log₂ p ≤ L < H + 1 (Huffman-এর প্রমাণিত bound!)ছবিতে
Greedy-র তিন জগৎ আর decision flow
- Provably optimalinterval scheduling, Kruskal/Prim, Huffman — exchange/cut proof আছে
- Optimal under conditioncoin change — শুধু canonical system-এ; condition যাচাই ছাড়া ঝুঁকি
- Approximation onlyTSP NN (≤2× OPT w/ triangle ineq.), set cover (ln n) — bound আছে, exact নেই
- Pure heuristicgraph coloring, job-shop — কোনো guarantee নেই, practice-এ useful
সমস্যা পেলাম
│
├─ greedy choice চোখে পড়ল?
│ │
│ ▼
├─ small instances brute-force-এ verify — মিলল? (sanity)
│ │
│ ▼
├─ exchange/cut argument লিখতে পারি? ──হ্যাঁ──▶ greedy ship করো ✓
│ │না
│ ▼
├─ known structure? (canonical coins, matroid?) ──হ্যাঁ──▶ literature দেখো
│ │না
│ ▼
├─ approximation bound দরকার মাত্র? ──হ্যাঁ──▶ greedy + documented gap
│ │না
│ ▼
└─▶ DP / backtracking / exact method — correctness আগে, speed পরেনিজে চালিয়ে দেখুন
Greedy-র বিচার: interval scheduling ×১০০০ যাচাই, বাংলা text-এ Huffman-এর ratio
import heapq
import itertools
import random
# ---------- part ১: interval scheduling greedy vs brute force ----------
def max_activities_greedy(intervals):
chosen, last_end = [], float('-inf')
for s, f in sorted(intervals, key=lambda iv: iv[1]): # earliest finish
if s >= last_end:
chosen.append((s, f))
last_end = f
return len(chosen)
def max_activities_brute(intervals):
best = 0
n = len(intervals)
for mask in range(1 << n):
picked = [intervals[i] for i in range(n) if mask >> i & 1]
picked.sort()
ok = all(picked[i][1] <= picked[i + 1][0]
for i in range(len(picked) - 1))
if ok:
best = max(best, len(picked))
return best
rng = random.Random(42)
mismatch = 0
trials = 1000
for t in range(trials):
n = rng.randint(2, 12)
starts = sorted(rng.sample(range(30), n))
intervals = [(s, s + rng.randint(1, 8)) for s in starts]
g, b = max_activities_greedy(intervals), max_activities_brute(intervals)
assert g <= b, 'greedy beat optimum?! impossible'
if g != b:
mismatch += 1
print(f'interval scheduling: {trials} trials, mismatches={mismatch} '
f'(proof বলে 0 হতেই হবে)')
# ---------- part ২: huffman on bangla text ----------
def huffman_codes(text):
freq = {}
for ch in text:
freq[ch] = freq.get(ch, 0) + 1
if len(freq) == 1: # degenerate: একটাই symbol
return {next(iter(freq)): '0'}, freq
heap = [[f, i, [ch]] for i, (ch, f) in enumerate(sorted(freq.items()))]
heapq.heapify(heap)
tie = len(heap)
codes = {ch: '' for ch in freq}
while len(heap) > 1:
f1, _, syms1 = heapq.heappop(heap)
f2, _, syms2 = heapq.heappop(heap)
for ch in syms1:
codes[ch] = '0' + codes[ch]
for ch in syms2:
codes[ch] = '1' + codes[ch]
tie += 1
heapq.heappush(heap, [f1 + f2, tie, syms1 + syms2])
return codes, freq
sample = (
'বাংলা ভাষার জন্মকথা প্রাচীন ভারতীয় আর্যভাষার সঙ্গে সম্পর্কিত। '
'মৌর্যযুগে প্রাচীন বাংলার জনগণের ভাষা ছিল প্রাকৃত। '
'বাংলা ভাষার বিকাশে প্রাকৃত ও অপভ্রংশের গুরুত্বপূর্ণ ভূমিকা ছিল। '
) * 20
codes, freq = huffman_codes(sample)
bits_huffman = sum(freq[c] * len(codes[c]) for c in freq)
n_symbols = len(freq)
bits_fixed = len(sample) * 16 # UTF-16 baseline
bits_ascii_like = len(sample) * 8 # 8-bit fixed baseline
top = sorted(freq.items(), key=lambda kv: -kv[1])[:5]
print(f'\nsample: {len(sample)} chars, {n_symbols} unique')
for ch, f in top:
print(f' {ch!r} freq={f:<4} code={codes[ch]:<12} ({f*len(codes[ch])} bits)')
print(f'huffman total : {bits_huffman:>9,} bits '
f'({bits_huffman/len(sample):.2f} bits/char)')
print(f'8-bit fixed : {bits_ascii_like:>9,} bits '
f'ratio {bits_ascii_like/bits_huffman:.2f}x')
print(f'UTF-16 fixed : {bits_fixed:>9,} bits '
f'ratio {bits_fixed/bits_huffman:.2f}x')
# entropy comparison — theoretical floor
import math
H = -sum((f / len(sample)) * math.log2(f / len(sample)) for f in freq.values())
print(f'entropy floor : {H:.2f} bits/char (Huffman ≤ H+1 গ্যারান্টি)')সাধারণ ফলাফল:
interval scheduling: 1000 trials, mismatches=0 (proof বলে 0 হতেই হবে)
sample: 3540 chars, 36 unique
' ' freq=480 code=101 (1440 bits)
'া' freq=440 code=011 (1320 bits)
'র' freq=340 code=001 (1020 bits)
'্' freq=260 code=1100 (1040 bits)
'ভ' freq=140 code=11100 (700 bits)
huffman total : 16,220 bits (4.58 bits/char)
8-bit fixed : 28,320 bits ratio 1.75x
UTF-16 fixed : 56,640 bits ratio 3.49x
entropy floor : 4.55 bits/char (Huffman ≤ H+1 গ্যারান্টি)পড়ার বিষয়:
১. ×১০০০ mismatch=0 — প্রমাণ empirical নয়, কিন্তু brute-force cross-check implementation bug ধরার সস্তা safety net; production optimization-এও এই pattern ব্যবহার হয় (property-based testing)।
২. Huffman 4.58 bits/char — 8-bit baseline-এ ~৪৩% savings, UTF-16-এ ~৭১%; আর entropy floor-এর মাত্র 0.03 ওপরে — প্রমাণিত near-optimality-র হাতে-কলমে প্রমাণ।
৩. Frequent ‘া’/’ ’ 3-bit, rare ‘ভ’ 5-bit — frequency-depth inversion চোখে দেখা যাচ্ছে।
Earliest-finish greedy n≤12 random instances-এ brute-force optimum-এর সাথে ×১০০০ মিলে যায় (empirical support of proof), আর Huffman বাংলা text-এ fixed-width-এর চেয়ে বাস্তবে ছোট encoding দেয় — প্রমাণিত greedy-র দুই মুখ: correctness আর measurable win।
নিজে বানান
Huffman encoder + decoder — roundtrip পরীক্ষাসহ পূর্ণ pipeline
- Frequency map বানান
- Heap দিয়ে tree build করুন (tie-breaking stable রাখুন)
- Tree traverse করে code-map বের করুন — prefix property assert করুন
- Encode: text→bitstring; Decode: bitstring+tree→text
- Roundtrip test: encode→decode==original, edge cases (empty, single-symbol)
- Compression stats প্রিন্ট করুন — bits/char, ratio, entropy gap
লক্ষ্য: প্রমাণিত greedy-কে working codec-এ রূপ দেওয়া — encode আর decode উভয় দিক, কারণ real compression দুটোই চায়।
import heapq
import math
class Node:
__slots__ = ('freq', 'ch', 'left', 'right')
def __init__(self, freq, ch=None, left=None, right=None):
self.freq, self.ch = freq, ch
self.left, self.right = left, right
def build_tree(text: str) -> Node:
freq = {}
for ch in text:
freq[ch] = freq.get(ch, 0) + 1
items = sorted(freq.items())
if len(items) == 1: # single symbol → leaf-root guard
ch, f = items[0]
return Node(f, None, Node(f, ch), None)
heap = [(f, i, Node(f, ch)) for i, (ch, f) in enumerate(items)]
heapq.heapify(heap)
tie = len(heap)
while len(heap) > 1:
f1, _, n1 = heapq.heappop(heap)
f2, _, n2 = heapq.heappop(heap)
tie += 1
heapq.heappush(heap, (f1 + f2, tie, Node(f1 + f2, None, n1, n2)))
return heap[0][2]
def build_codes(root: Node) -> dict:
codes = {}
def walk(node, prefix):
if node is None: # single-symbol tree-র right arm
return
if node.ch is not None: # leaf
codes[node.ch] = prefix or '0' # single-symbol fallback
return
walk(node.left, prefix + '0')
walk(node.right, prefix + '1')
walk(root, '')
return codes
def encode(text: str, codes: dict) -> str:
return ''.join(codes[ch] for ch in text)
def decode(bits: str, root: Node) -> str:
out, node = [], root
for b in bits:
node = node.left if b == '0' else node.right
if node.ch is not None:
out.append(node.ch)
node = root
return ''.join(out)
def assert_prefix_free(codes: dict):
vals = list(codes.values())
for a in vals:
for b in vals:
assert not (a != b and b.startswith(a)), (a, b)
if __name__ == '__main__':
tests = [
'বাংলা ভাষা আমার মাতৃভাষা',
'aaaaaaaaaabbbbbcccccddddddddddeee',
'x', # single symbol
]
for text in tests:
root = build_tree(text)
codes = build_codes(root)
assert_prefix_free(codes)
bits = encode(text, codes)
assert decode(bits, root) == text, 'roundtrip FAILED'
avg = len(bits) / len(text)
freq = {c: text.count(c) for c in set(text)}
H = -sum((f / len(text)) * math.log2(f / len(text)) for f in freq.values())
print(f'{text[:18]!r:<22} {len(text):>3} chars → {len(bits):>4} bits '
f'({avg:.2f} bpc, entropy {H:.2f}) roundtrip ✓')
# adversarial: uniform distribution — Huffman লাভ কমে যায়
import string
uni = ''.join(string.ascii_lowercase[:16])
codes_u = build_codes(build_tree(uni * 100))
bits_u = len(encode(uni * 100, codes_u))
print(f'uniform 16-symbol: {bits_u/(1600):.2f} bits/char '
f'(log₂16=4.00 — প্রায় no-gain, prefix-free constraint-এর দাম)')প্রত্যাশিত output:
'বাংলা ভাষা আমার মা' 24 chars → 77 bits (3.21 bpc, entropy 3.14) roundtrip ✓
'aaaaaaaaaabbbbbccc' 33 chars → 74 bits (2.24 bpc, entropy 2.18) roundtrip ✓
'x' 1 chars → 1 bits (1.00 bpc, entropy -0.00) roundtrip ✓
uniform 16-symbol: 4.00 bits/char (log₂16=4.00 — প্রায় no-gain, prefix-free constraint-এর দাম)নিজে বাড়ান:
১. Bitstring-কে actual bytes-এ pack করুন (int(bits, 2).to_bytes) —
padding rules সহ; file I/O roundtrip।
২. Tree serialization যোগ করুন — decoder-কে tree পাঠানোর format
(header + payload), real .huffman file format।
৩. Canonical Huffman implement করুন — code lengths-ই যথেষ্ট, tree
পাঠানো লাগে না (DEFLATE/PNG-র পদ্ধতি)।
বাস্তব সিস্টেমে
Greedy বাস্তবে কোথায়
File compression। DEFLATE (zip/gzip/png) Huffman-ই dynamic distance/length alphabets-এ; JPEG DCT-coefficient coding-এও Huffman (canonical variant)। DEFLATE LZ77+Huffman hybrid — dictionary match (LZ) তারপর entropy stage (Huffman)।
Networking। TCP congestion window-র additive increase একটা greedy heuristic — AIMD convergence প্রমাণ আছে (fairness), throughput-optimality নেই; networking module-এ congestion control লেসনে এটা মূল theme।
Operating systems। Shortest-Job-First scheduling greedy — average waiting time-এ provably optimal (non-preemptive), starvation risk-এর সাথে; OS module-এর scheduling লেসনের trade-off।
Compilers। Register allocation-এর linear-scan heuristic greedy — optimal coloring NP-hard বলে JIT-এ speed-এর জন্য approximation accepted।
Bioinformatics। Greedy assembly heuristics short-read sequencing-এ first generation; overlap-layout-consensus pipeline-এর layout stage।
Approximation theory। Set cover greedy ln(n)-factor — প্রমাণিত যে polynomial-এ এর ভালো general bound অসম্ভব (unless P=NP)। Approximation ratio-ই honest deliverable যখন exact intractable।
যে ভুলগুলো সবাই করে
“Greedy-ও optimal দেয়, তাহলে DP-র দরকারই কী?”
Coin-set {1,3,4} amount 6-এ greedy ৩ দেয়, optimal ২ — গত লেসনের measurement। Greedy-র optimality প্রতি-problem property, universal নয়; প্রমাণ ছাড়া greedy ব্যবহার মানে unverified assumption shipping। DP slow-but-correct baseline রেখে greedy-র output validate করা সস্তা insurance।
“Exchange argument শুধু তত্ত্ব — engineering-এ কাজে লাগে না।”
উল্টো: interview-তে এবং design review-তে ‘এই greedy কেন ঠিক?’ — উত্তরটা exchange sketch ছাড়া দেওয়া যায় না। আর bug-hunting-এ counterexample search (ছোট instance brute-force diff, আজকের experiment-এর pattern) production greedy-র silent-wrong-answer ধরে — এটা সরাসরি exchange argument-এর inverse application।
“Huffman optimal মানে সেটা সবসময় best compressor।”
Huffman optimal prefix-code universe-এর মধ্যে, per-symbol independent coding ধরে। Arithmetic coding fractional-bit দিয়ে Huffman-কে ছাড়িয়ে যায়; context modeling (PPM, neural LM) conditional distribution-এ entirely different game; LZ-family repetition exploit করে যেটা per-symbol model দেখেই না। Compression stack-এ Huffman একটা layer — পুরো সমাধান নয়।
“Heuristic greedy-র ফলাফল অবিশ্বস্ত, তাই ব্যবহারই করা উচিত না।”
Approximation algorithm-এর documented bound (set cover ≤ ln n·OPT, TSP-NN ≤ 2×OPT metric space-এ) একটা guarantee — worst case-ও বাঁধা মধ্যে। NP-hard problem-এ exact polynomial না থাকলে bounded-approximation বনাম exponential-exact বাণিজ্য বাস্তব engineering সিদ্ধান্ত; dishonesty তখনই যখন bound না বলে optimal দাবি করা হয়।
বুঝেছেন কি না দেখুন
1Exchange argument-এর চার ধাপ লিখুন এবং interval scheduling-এ প্রতিটা ধাপ কোন বাক্যে fulfill হয় মেলান।
স্মরণ
ধাপ ১: G আর O ধরা — ‘f₁=min finish, O=optimal’। ধাপ ২: first divergence — O-এর earliest-finish member j। ধাপ ৩: exchange feasibility — f₁≤fⱼ বলে j-কে ১ দিয়ে replace করলে overlaps ভাঙে না, |O′|=|O|। ধাপ ৪: progress+repeat — O′ greedy-র প্রথম পছন্দ মানে, বাকি timeline-এ recursion; finite activities বলে termination।
2Activities: (1,4),(2,6),(4,7),(6,8),(3,5),(7,9)। Earliest-finish greedy কোনগুলো নেবে? Optimal size কত?
প্রয়োগ
Sort by finish: (1,4),(3,5),(2,6),(4,7),(6,8),(7,9)। Greedy নেয় (1,4) [end=4]; (3,5)-এর start 3 আগেই শেষ (4) হওয়ায় skip; (2,6),(4,7) skip; (6,8) start 6 ≥ 4 নেয় [end=8]; (7,9)-এর start 7-এর কম 8 হওয়ায় skip। ফল set (1,4),(6,8) — size 2। Optimal-ও 2 ভাবছিলাম? পুনর্বিবেচনা: (1,4),(4,7)? start=4 ≥ end=4 — touching valid! তাহলে (1,4),(4,7),(7,9) size 3। সংশোধন: ≥ convention-এ touching allowed, greedy-ও পাবে: after (1,4), (4,7) start 4≥4 নেয় [end=7], then (7,9) start 7≥7 নেয় — size 3 = optimal। Code-এ s >= last_end-ই এই convention।
3‘সবচেয়ে ছোট activity first’ greedy-র counterexample বানান — ৩টা
activity দিয়ে, যেখানে এটা ১ নেয় কিন্তু optimal ২।
যুক্তি
A=(10,11) length 1 — ছোটতম; B=(0,12), C=(12,24) দুইটা লম্বা কিন্তু A-এর সাথে overlap করে… B ও C disjoint। Shortest-first A=(10,11) নেয়; B,C-এর সাথে conflict (10-11 ∈ [0,12], [12,24]? C start 12 A-এর end 11-এর পরে — no conflict!)। ঠিক করি: B=(9,13), C=(13,17), A=(11.9,12.1)। Shortest=A নেয়; B-এর সাথে A overlap (11.9-12.1 ⊂ 9-13) তাই B আর C-তে A-পরে C নেওয়া যায় (13 ≥ 12.1) — total 2 (A+C) কিন্তু optimal B+C = 2-ও… সংক্ষেপে: A=(5,6) মাঝে, B=(0,10), C=(10,20): shortest-first নেয় A, বাদ B (overlap), নেয় C → 2। Optimal B+C = 2। Hmm equal। সঠিক counterexample: A=(5,6), B=(0,5.9), C=(5.9,12): shortest A নেয় → B বাদ (overlap), C-ও নেয় না (5.9-এর আগে A শেষ — overlap!) → মোট 1। Optimal: B+C = 2 (disjoint, 0-5.9, 5.9-12)। মূল নীতি: shortest middle piece দুই পাশের disjoint pair-কে ব্লক করে।
4আপনার team greedy scheduler লিখেছে job scheduling-এ (shortest-job-first); client দাবি করে deadline-missed jobs কমাতে হবে। Design response কী?
ডিজাইন
SJF average waiting minimize করে, deadlines ignore করে — objective mismatch, algorithm bug না। Options: (১) EDF (earliest-deadline-first) — single-machine preemptive-তে deadline misses provably optimal (exchange argument-ই প্রমাণ করে); (২) hybrid weighted objective — DP ছোট n-এ; (৩) পুরনো SJF রেখে simulation-based comparison দেখানো। সঠিক প্রথম প্রশ্ন: objective function-টা আসলে কী? Metric change মানে algorithm change — greedy-র ‘optimality’ সবসময় নির্দিষ্ট objective-র relative।
5Prefix-free constraint ছাড়া Huffman আরও efficient হতে পারত? কেন constraint অ-আলোচনাসাপেক্ষ?
স্মরণ
হতে পারত (shortest codes সবাইকে), কিন্তু undecodable: stream-এ ‘01’ পড়লে ‘a’+‘b’ না ‘ab’ — receiver reconstruction অসম্ভব, delimiter-এর extra bits লাগত (self-delimiting না হলে)। Prefix property মানে left-to-right instantaneous decoding — streaming-এ zero lookahead। Compression-এর মূল্য: correctness constraint; Huffman সেই constraint-এর মধ্যে provably best।
6Uniform distribution-এ (সব frequency সমান) Huffman-এর লাভ কী? Build-এর output-এর সাথে মিলিয়ে ব্যাখ্যা করুন।
যুক্তি
সমান frequency n symbol-এ: Huffman balanced tree বানায় — সব depth ⌈log₂n⌉ বা কাছাকাছি, মানে fixed-width-এর হুবহু সমান — gain ≈ 0 (build output: 4.00 bpc vs log₂16=4.00)। Huffman-এর লাভ frequency-variance-এর monotone ফাংশন; variance শূন্যে প্রযুক্তি শূন্য। তাই adaptive systems-এ distribution measure না করে Huffman apply করা বৃথা — measurement আগে।
এরপর কী
এরপর কী
Greedy আর DP — algorithm design-এর দুই মেরু — আমাদের toolkit-এ এখন আছে। পরের লেসন text-এর জগতে ফেরা: String Algorithms — naive O(nm) থেকে KMP-র no-backtrack magic, Rabin-Karp-র rolling hash, suffix array-এর genomics-scale indexing। Hash-tables লেসনের hash-function জ্ঞান আর আজকের Huffman-এর frequency-intuition দুটোই সেখানে কাজে লাগবে — এবং দেখব string matching-ও একটা DP-sibling (LPS array) দিয়ে elegant হয়।
আরও পড়ুন
- Introduction to Algorithms (CLRS), Chapters 15 ও 16 — Greedy Algorithms, Huffman Codes — Thomas H. Cormen et al. · Exchange argument আর Huffman-এর প্রামাণ্য উৎস — greedy-choice proof-এর canonical form
- Algorithm Design, Chapter 4 — Greedy Algorithms — Jon Kleinberg, Éva Tardos · Interval scheduling, exchange argument-এর pedagogical gold standard