Foundationপ্রথম নীতি থেকে
LEVEL 6লেসন ২১/২৪অ্যাডভান্সড৫৫ মিনিট

Greedy — local সিদ্ধান্তে global জয়, কখন সম্ভব

Greedy Algorithms and the Exchange Argument

Greedy প্রতিটা ধাপে locally best নেয় — global optimum তখনই নিশ্চিত যখন exchange argument-এ প্রমাণ আছে; Huffman সেই প্রমাণের সবচেয়ে সুন্দর জয়।

এই লেসন শেষে আপনি পারবেন

  • Greedy choice property আর optimal substructure যাচাই করে বলতে পারবেন কোনো সমস্যায় greedy সঠিক উত্তর দেওয়ার entitlement আছে কি না
  • Exchange argument-এর step-by-step প্রমাণ-কৌশল interval scheduling-এ প্রয়োগ করে earliest-finish rule-এর optimality নিজে প্রমাণ করতে পারবেন
  • Coin system {1,3,4}-জাতীয় counterexample বানিয়ে দেখাতে পারবেন greedy কোথায় ভাঙে আর DP কীভাবে সেখানে জেতে
  • Huffman coding-এর পূর্ণ walkthrough দিতে পারবেন — heap-built tree, prefix property, expected length হিসাব আর measured compression ratio
  • Kruskal-এর cut property recap করে বলতে পারবেন কেন ওই greedy provably correct, আর heuristic-কে approximation হিসেবে মেনে নেওয়ার honesty প্রদর্শন করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনে 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-রও দুইটা পূর্বশর্ত আছে:

  1. Greedy choice property: একটা locally-best সিদ্ধান্ত নেওয়ার পর বাকি সমস্যা এখনো সমাধানযোগ্য থাকে এমনভাবে যে সম্পূর্ণ solution optimal হতে পারে — অর্থাৎ প্রথম সিদ্ধান্তটা কখনো regret হয় না। DP-র সাথে পার্থক্য: greedy-র সিদ্ধান্ত আটকে যায় (subproblem আর পরিবর্তন হয় না), DP-র সিদ্ধান্ত সব option খোলা রাখে।
  2. Optimal substructure: গত লেসনেরই শর্ত — পুরো সমাধান optimal হলে তার অংশগুলোও নিজেদের subproblem-এ optimal।

দুই শর্ত মিললে প্রমাণের মানক কাঠামো: induction — প্রতিটা ধাপে greedy choice optimal solution-এর সাথে compatible দেখাও, exchange argument দিয়ে।

Exchange argument — প্রমাণের recipe

Recipe চার ধাপে:

  1. ধরো G = greedy-র solution, O = কোনো optimal solution।
  2. যদি G = O, কাজ শেষ। নাহলে প্রথম স্থান খুঁজো যেখানে দুটো আলাদা।
  3. Exchange: O-কে হাত না ভাঙে greedy-র choice-টা ঢোকাও — দেখাও নতুন O′ এখনো valid এবং মান খারাপ হয়নি।
  4. 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:

  1. প্রতিটা character একটা single-node tree, priority = frequency।
  2. Greedy choice: দুইটা lowest-frequency tree তুলে নাও।
  3. নতুন parent বানাও (freq = sum), queue-তে ফেরত দাও।
  4. একটা 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 = 7

Heap-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!)
Huffman tree-তে depth = code length। Frequent symbol উপরে (ছোট code), rare নিচে — weighted path length minimize হয় greedy merges-এ।

ছবিতে

Greedy-র তিন জগৎ আর decision flow

Greedy-র classification — কোথায় কী দাবি করা নিরাপদ
  1. Provably optimalinterval scheduling, Kruskal/Prim, Huffman — exchange/cut proof আছে
  2. Optimal under conditioncoin change — শুধু canonical system-এ; condition যাচাই ছাড়া ঝুঁকি
  3. Approximation onlyTSP NN (≤2× OPT w/ triangle ineq.), set cover (ln n) — bound আছে, exact নেই
  4. 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 পরে
Decision flow — নতুন সমস্যায় greedy বিবেচনার শৃঙ্খলিত পথ। 'প্রমাণ আছে?' ঘরে 'না' মানেই DP/backtracking-এ ফেরা, আশায় নয়।

নিজে চালিয়ে দেখুন

EXPERIMENT

Greedy-র বিচার: interval scheduling ×১০০০ যাচাই, বাংলা text-এ Huffman-এর ratio

macOS / Linux (Python 3.10+)· ২৫ মিনিট
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।

নিজে বানান

BUILD IT

Huffman encoder + decoder — roundtrip পরীক্ষাসহ পূর্ণ pipeline

Python · ●●●○○
  1. Frequency map বানান
  2. Heap দিয়ে tree build করুন (tie-breaking stable রাখুন)
  3. Tree traverse করে code-map বের করুন — prefix property assert করুন
  4. Encode: text→bitstring; Decode: bitstring+tree→text
  5. Roundtrip test: encode→decode==original, edge cases (empty, single-symbol)
  6. 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 দাবি করা হয়।

বুঝেছেন কি না দেখুন

1

Exchange 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।

2

Activities: (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।

5

Prefix-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।

6

Uniform 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