সরাসরি পর্বে যান
পর্ব ৫১ · ৭২-এর মধ্যে · মডিউল ৭

Huffman coding — greedy দিয়ে ডেটা ছোট করা

Huffman Coding — Prefix-Free Codes, Building the Tree with a Min-Heap, Decoding, Proof of Optimality, Entropy & Canonical Codes

Taught in Bangla, this lesson explains why variable-length codes compress data, how prefix-free codes form a binary code tree, and how Huffman's greedy rule repeatedly merges the two rarest nodes using a min-heap in O(n log n), or O(n) with two queues on sorted frequencies. It covers decoding, average code length, the two-lemma induction proof of optimality, ties, canonical codes, entropy and real-world uses.

এই পর্বে যা শিখবেন

  • Fixed-length আর variable-length code-এর পার্থক্য, আর কেন prefix-free হওয়া জরুরি
  • Prefix-free code-কে binary tree হিসেবে দেখা, আর গড় code length (weighted path length) হিসাব করা
  • Min-heap দিয়ে Huffman tree হাতে বানানো, আর tree ধরে হেঁটে decode করা
  • Huffman কেন সর্বোত্তম — exchange argument আর induction দিয়ে প্রমাণ লেখা
  • Entropy-র সীমা, tie, canonical code আর zip/PNG/JPEG-এ Huffman-এর ব্যবহার ও সীমাবদ্ধতা বোঝা

১ · ডেটা ছোট করা কেন — SMS, ফাইল আর ধীর ইন্টারনেট

মোবাইলে একটি ইংরেজি SMS-এ ১৬০টি অক্ষর ধরে, কিন্তু বাংলায় লিখলে মাত্র ৭০টি। কারণ ইংরেজি SMS প্রতিটি অক্ষরে ৭ বিট খরচ করে, আর বাংলা অক্ষরের জন্য ফোন ১৬ বিটের Unicode ব্যবহার করে। জায়গা একই — ১৪০ byte — শুধু প্রতি অক্ষরে কত বিট লাগছে, সেটাই পার্থক্য গড়ে দেয়। একই প্রশ্ন সবখানে: গ্রামে ধীর মোবাইল ইন্টারনেটে একটি ওয়েবপেজ কত দ্রুত খুলবে, অফিসের ব্যাকআপ কত জায়গা নেবে, ইমেইলে ছবি কত বড় যাবে।

তথ্য না হারিয়ে কম বিটে লেখার বিদ্যার নাম data compression (ডেটা সংকোচন)। এর দুটি ধরন আছে। Lossless সংকোচনে খুললে হুবহু আগের ডেটা ফেরত আসে — লেখা, প্রোগ্রাম, ব্যাংকের হিসাবে এটাই লাগে; একটি অঙ্ক বদলালেও সর্বনাশ। Lossy সংকোচনে চোখ-কানে ধরা পড়ে না এমন খুঁটিনাটি ফেলে দেওয়া হয় — ছবি, গান, ভিডিও। এই পর্বের Huffman coding একটি lossless পদ্ধতি, আর মজার ব্যাপার হলো lossy JPEG-এর ভেতরেও শেষ ধাপে এটি বসে থাকে।

মূল পর্যবেক্ষণটি সহজ: লেখায় সব অক্ষর সমানভাবে আসে না। ইংরেজিতে e, t, a অনেক বেশি; z, q খুব কম। বাজারের ফর্দে “চাল”, “ডাল” বারবার আসে, “জাফরান” কালেভদ্রে। তাহলে প্রতিটি অক্ষরে সমান জায়গা দেব কেন? বেশি আসা অক্ষরকে ছোট নাম দিন, কম আসাকে লম্বা — গড়ে খরচ কমবে। প্রশ্ন শুধু দুটি: (১) পাঠক কীভাবে বুঝবে কোথায় এক অক্ষর শেষ আর পরেরটা শুরু, আর (২) কোন অক্ষরকে ঠিক কত লম্বা নাম দিলে মোট খরচ সবচেয়ে কম? প্রথম প্রশ্নের উত্তর prefix-free code (অংশ ৩), দ্বিতীয়টির উত্তর Huffman coding — পর্ব ৫০-এর greedy কৌশলের সবচেয়ে বিখ্যাত সফল উদাহরণগুলোর একটি।

২ · Fixed-length বনাম variable-length code — Morse-এর বুদ্ধি

কম্পিউটারে সাধারণ ইংরেজি লেখা থাকে ASCII-তে: প্রতিটি অক্ষর ৮ বিটের একটি byte. এটি একটি fixed-length code — সব অক্ষরের code সমান লম্বা। সুবিধা হলো পড়া সহজ: প্রতি ৮ বিট কেটে নিলেই একটি অক্ষর। কিন্তু একটি বার্তায় যদি মাত্র k টি আলাদা অক্ষর থাকে, তাহলে ৮ বিটের দরকারই নেই — ⌈log₂ k⌉ বিটই যথেষ্ট। ৬টি অক্ষরের জন্য ৩ বিট (2³ = 8 ≥ 6), ১৪টির জন্য ৪ বিট।

এই পর্বের চলমান উদাহরণ: একটি ৪০ অক্ষরের বার্তায় ছয়টি অক্ষর আছে, এই হারে —

অক্ষরfrequency ffixed-length (৩ বিট)variable-length (Huffman)f × দৈর্ঘ্য
A140001128
N80010116
T70100014
K601110118
S3100100112
H210110008
মোট4040 × 3 = 120 বিট96 বিট

ASCII-তে লাগত 40 × 8 = 320 বিট, ৩-বিটের fixed code-এ 120, আর ডান কলামের variable-length code-এ 96 — fixed code-এর চেয়েও ২০% কম। A সবচেয়ে বেশি আসে, তাই তার code মাত্র দুই বিট; H সবচেয়ে কম, তাই চার বিট। এই টেবিলের code কোথা থেকে এল, অংশ ৫-এ নিজেই বানাবেন।

Morse code-এর বুদ্ধি। ১৮৩০–৪০-এর দশকে Samuel Morse আর Alfred Vail টেলিগ্রাফের জন্য যে Morse code বানান, তাতে ইংরেজির সবচেয়ে বেশি আসা অক্ষর E মাত্র একটি ডট (·), T একটি ড্যাশ (−), আর কম আসা Q হলো − − · −. টেলিগ্রাফ অপারেটরের হাতের পরিশ্রম কমাতে এই নকশা — Huffman-এর এক শতাব্দীরও বেশি আগে একই অন্তর্দৃষ্টি। তবে Morse-এর একটি দুর্বলতা আছে, যা পরের অংশে ধরা পড়বে।

৩ · Prefix-free code আর code tree

Variable-length code-এর বিপদ দেখুন। ধরুন a = 0, b = 01, c = 10. বিটের স্রোত 010 পেলেন। এটি কি 0 | 10 = “ac”, নাকি 01 | 0 = “ba”? দুটোই বৈধ — বার্তা দ্ব্যর্থক। সমস্যার মূল: a-এর code 0 হলো b-এর code 01-এর শুরু (prefix). Morse code-এও একই সমস্যা: E (·) হলো I (··)-এর শুরু। তাই Morse-এ অক্ষরের মাঝে একটি বিরতি দিতে হয় — আসলে বিরতিটিই তৃতীয় একটি চিহ্ন। কম্পিউটারের বিটে তো শুধু 0 আর 1; আলাদা বিরতির চিহ্ন রাখলে সেটিও বিট খায়।

সমাধান: এমন code নিন যেখানে কোনো code অন্য কোনো code-এর শুরুর অংশ নয়. একে বলে prefix-free code (অনেক বই সংক্ষেপে “prefix code” বলে — নামটি বিভ্রান্তিকর, কিন্তু অর্থ একই)। ওপরের টেবিলের code দেখুন: 11, 01, 00, 101, 1001, 1000 — কোনোটি অন্যটির শুরু নয়। তাই বিটের স্রোত বাঁ থেকে পড়তে পড়তে যেই একটি পূর্ণ code মিলে যায়, সেখানেই নিশ্চিন্তে কাটা যায় — সামনে তাকানোর দরকার নেই। ফোন নম্বরও এভাবে কাজ করে: 999 ডায়াল করলেই কল চলে যায়, কারণ কোনো সাধারণ নম্বর 999 দিয়ে শুরু হয় না।

প্রতিটি prefix-free code একটি binary tree

একটি binary tree আঁকুন: root থেকে বাঁ দিকের edge-এর নাম 0, ডানের নাম 1. প্রতিটি অক্ষর একটি leaf-এ বসে, আর root থেকে সেই leaf পর্যন্ত পথের 0/1 পড়লেই তার code. একে বলে code tree. অক্ষর শুধু leaf-এ থাকে বলেই code prefix-free: কোনো অক্ষরের পথ অন্য অক্ষরে পৌঁছানোর আগে থামে না। উল্টোটাও সত্য — যেকোনো prefix-free code থেকে এমন tree আঁকা যায়। তাই “সেরা prefix-free code খোঁজা” আর “সেরা code tree খোঁজা” একই সমস্যা।

একটি code-এর দৈর্ঘ্য = সেই leaf-এর depth. আরেকটি জরুরি তথ্য: সর্বোত্তম tree সবসময় full binary tree — প্রতিটি internal node-এর ঠিক দুটি সন্তান। কারণ কোনো node-এর একটিই সন্তান থাকলে সেই এক-সন্তানের পুরো subtree এক ধাপ ওপরে তুলে আনা যায়: ওই subtree-র সব code এক বিট ছোট হয়, তবু prefix-free থাকে। তাই n টি অক্ষরের সর্বোত্তম tree-তে n টি leaf আর ঠিক n − 1 টি internal node.

Prefix-free না হলেই কি অচল? সবসময় নয় — যেমন {0, 01} prefix-free নয়, কিন্তু পুরো বার্তা হাতে থাকলে পেছন থেকে পড়ে একভাবেই ভাঙা যায় (একে বলে uniquely decodable code). তবে McMillan-এর একটি উপপাদ্য (১৯৫৬) দেখায়, এমন code দিয়ে prefix-free code-এর চেয়ে কম বিটে লেখা সম্ভব নয়। তাই কিছু না হারিয়ে আমরা prefix-free code-এ সীমাবদ্ধ থাকতে পারি — আর সেটি পড়াও সহজ।

৪ · খরচ মাপা — গড় code length

একটি code tree T-এর খরচ হলো বার্তা লিখতে মোট কত বিট লাগে। অক্ষর c যদি f(c) বার আসে আর তার depth d(c) হয়, তাহলে

B(T) = Σc f(c) · d(c)     গড় দৈর্ঘ্য L = B(T) / Σ f(c)

এর নাম weighted path length — প্রতিটি leaf-এর পথের দৈর্ঘ্যকে তার ওজন (frequency) দিয়ে গুণ করে যোগ। f-কে সম্ভাবনা p(c) = f(c)/মোট ধরলে L হলো প্রতি অক্ষরে প্রত্যাশিত বিট (expected code length). উদাহরণে B = 96, L = 96/40 = 2.4 বিট — fixed code-এর ৩ বিটের চেয়ে কম।

একটি সুন্দর অভেদ. প্রতিটি internal node-এর ওজন ধরুন তার নিচের সব leaf-এর frequency-র যোগফল। তাহলে B(T) = সব internal node-এর ওজনের যোগফল। কেন? একটি leaf c-এর frequency ঠিক ততবার যোগ হয়, যতগুলো internal node তার পূর্বপুরুষ — অর্থাৎ d(c) বার। ফলে Huffman-এ প্রতিটি জোড়ার যোগফল লিখে রাখলেই খরচ পাওয়া যায়; উদাহরণে 5 + 11 + 15 + 25 + 40 = 96. ইন্টারভিউর “দড়ি জোড়ার সর্বনিম্ন খরচ” (দুটি দড়ি জুড়তে খরচ = দুটির দৈর্ঘ্যের যোগ) প্রশ্নটি আসলে হুবহু এই হিসাব।

সমস্যাটি এখন স্পষ্ট: n টি অক্ষর আর তাদের frequency দেওয়া; এমন full binary tree খুঁজুন যার leaf-গুলো এই অক্ষর আর B(T) সবচেয়ে কম। সম্ভাব্য tree-এর সংখ্যা n-এর সাথে বিস্ফোরকভাবে বাড়ে (Catalan সংখ্যা গুণ n! রকম লেবেল), তাই সব দেখে বাছাই অসম্ভব। দরকার একটি চতুর নিয়ম।

৫ · Huffman-এর অ্যালগরিদম — সবচেয়ে বিরল দুটিকে জোড়া

স্বাভাবিক ভাবনা হলো ওপর থেকে শুরু করা: অক্ষরগুলোকে প্রায় সমান ওজনের দুই দলে ভাগ করুন, এক দল 0, অন্যটি 1, তারপর প্রতিটি দলে আবার একই কাজ। এটিই Shannon-Fano পদ্ধতি — কিন্তু এটি সবসময় সর্বোত্তম নয় (ভাবনার প্রশ্ন ১)। David Huffman উল্টো দিক থেকে ভাবলেন: নিচ থেকে ওপরে. সবচেয়ে কম আসা দুটি অক্ষর নিশ্চয়ই সবচেয়ে লম্বা code পাবে — তাই তাদের সবচেয়ে গভীরে ভাইবোন বানিয়ে দিন। জোড়া দুটিকে এখন একটি “নতুন অক্ষর” হিসেবে দেখুন, যার frequency দুটির যোগফল। বাকি কাজ একই সমস্যা, শুধু একটি অক্ষর কম।

প্রতিবার “সবচেয়ে কম frequency-র দুটি” বের করা আর নতুনটি ঢোকানো — এটি ঠিক পর্ব ২৩-এর min-heap বা priority queue-এর কাজ।

Pseudocode — Huffman (C = n symbols, f = frequencies; n >= 2)

Huffman(C):
    Q = min-heap of leaves, key = f            // ties: smaller node id first
    repeat n - 1 times:
        x = Q.extractMin()                     // least frequent
        y = Q.extractMin()                     // second least frequent
        z = new node: left = x, right = y, f(z) = f(x) + f(y)
        Q.insert(z)
    return Q.extractMin()                      // the root of the code tree
// code of a symbol = labels on the path from the root: left edge 0, right edge 1

উদাহরণের ছয়টি অক্ষরে চালিয়ে দেখুন। লাল/সবুজ রঙের সংখ্যাটি এই ধাপে বানানো নতুন node:

Tree-গুলোর বন (forest)রঙিন = এই ধাপের নতুন node

            
প্রতিটি ধাপের পর
heap-এ (ছোট থেকে)
বের করা দুটি
নতুন node
merge-এর যোগফল = খরচ
  1. শুরু. প্রতিটি অক্ষর একটি leaf, তার frequency-ই key. ছয়টিই min-heap-এ। মোট অক্ষর ৪০টি।
  2. প্রথম merge. সবচেয়ে ছোট দুটি H2 আর S3. এদের জুড়ে node 5 — আগে বের হওয়া H বাঁয়ে (0), S ডানে (1). 5 heap-এ ফিরে গেল। খরচ এ পর্যন্ত 5.
  3. দ্বিতীয় merge. এখন সবচেয়ে ছোট 5 আর K6 → node 11. লক্ষ করুন, নতুন node-ও সাধারণ অক্ষরের মতোই প্রতিযোগিতা করে। খরচ 5 + 11 = 16.
  4. তৃতীয় merge. heap-এ 7 8 11 14 — ছোট দুটি T7 আর N8 → node 15. এবার দুটি আলাদা tree তৈরি হলো। খরচ 31.
  5. চতুর্থ merge. heap-এ 11 14 15 — ছোট দুটি 11 আর A14 → node 25. A সবচেয়ে বেশি আসে, তাই সবার শেষে জোড়া পেল — ফলে সে root-এর কাছে থাকবে। খরচ 56.
  6. শেষ. 15 আর 25 জুড়ে root 40 = মোট অক্ষর। পথ পড়ে code: T = 00, N = 01, A = 11, K = 101, H = 1000, S = 1001. খরচ 96 বিট — টেবিলের সাথে মেলে। ৬টি অক্ষরে merge হলো ঠিক ৫টি (n − 1).

এবার একই অ্যালগরিদম আসল লেখায়। জাতীয় সংগীতের প্রথম লাইন, ইংরেজি অক্ষরে: amar sonar bangla ami tomay bhalobashi. প্রোগ্রামটি অক্ষর গোনে, heap দিয়ে tree বানায়, প্রতিটি অক্ষরের code ছাপে, পুরো লেখা encode করে, তারপর tree ধরে decode করে মিলিয়ে দেখে। Tie ভাঙার নিয়ম স্থির রাখা হয়েছে — heap-এর key (frequency, node id), আর leaf-এর id অক্ষরের ক্রমে, নতুন node-এর id তার পরে — তাই C++ আর Python হুবহু এক উত্তর দেয়।

#include <cstdio>
#include <functional>
#include <map>
#include <queue>
#include <string>
#include <utility>
#include <vector>
using namespace std;
int main() {
    const string text = "amar sonar bangla ami tomay bhalobashi";
    map<char, int> freq;                          // leaves sorted by character
    for (char c : text) ++freq[c];
    vector<char> sym; vector<int> f, L, R;        // node arrays, leaves first
    for (auto [c, k] : freq) { sym.push_back(c); f.push_back(k); L.push_back(-1); R.push_back(-1); }
    using P = pair<int, int>;                     // (frequency, node id): id breaks ties
    priority_queue<P, vector<P>, greater<P>> pq;
    for (int i = 0; i < (int)f.size(); ++i) pq.push({f[i], i});
    while (pq.size() > 1) {
        auto [fa, a] = pq.top(); pq.pop();        // the two least frequent nodes
        auto [fb, b] = pq.top(); pq.pop();
        sym.push_back(0); f.push_back(fa + fb); L.push_back(a); R.push_back(b);
        pq.push({fa + fb, (int)f.size() - 1});
    }
    const int root = pq.top().second;
    map<char, string> code;
    function<void(int, const string&)> walk = [&](int v, const string& s) {
        if (L[v] < 0) { code[sym[v]] = s.empty() ? "0" : s; return; }
        walk(L[v], s + "0"); walk(R[v], s + "1");  // left edge 0, right edge 1
    };
    walk(root, "");
    for (auto [c, k] : freq) printf("'%c' freq %2d code %s\n", c, k, code[c].c_str());
    string bits, out;
    for (char c : text) bits += code[c];      // encode
    for (int v = root; char b : bits) {           // decode: walk down from the root
        v = (b == '0') ? L[v] : R[v];
        if (L[v] < 0) { out += sym[v]; v = root; }
    }
    printf("first 24 bits: %s\n", bits.substr(0, 24).c_str());
    printf("fixed 8-bit: %zu bits, huffman: %zu bits\n", 8 * text.size(), bits.size());
    printf("average: %.3f bits per char\n", (double)bits.size() / text.size());
    printf("decoded matches: %s\n", out == text ? "True" : "False");
}

আউটপুট:

' ' freq  5 code 101
'a' freq  9 code 01
'b' freq  3 code 1100
'g' freq  1 code 10010
'h' freq  2 code 11111
'i' freq  2 code 0000
'l' freq  2 code 0001
'm' freq  3 code 1101
'n' freq  2 code 0010
'o' freq  3 code 1110
'r' freq  2 code 0011
's' freq  2 code 1000
't' freq  1 code 10011
'y' freq  1 code 11110
first 24 bits: 011101010011101100011100
fixed 8-bit: 304 bits, huffman: 134 bits
average: 3.526 bits per char
decoded matches: True

Code বের করা হয়েছে recursion দিয়ে (walk), আর decode-এর লুপে C++20-এর range-for-এর শুরুতে int v = root; বসানো। ব্যাখ্যা নিচে, দুই ভাষার জন্য একসাথে।

import heapq
from collections import Counter

text = "amar sonar bangla ami tomay bhalobashi"
freq = sorted(Counter(text).items())            # leaves sorted by character
sym = [c for c, _ in freq]
f = [k for _, k in freq]
L, R = [-1] * len(f), [-1] * len(f)
pq = [(k, i) for i, k in enumerate(f)]          # (frequency, node id): id breaks ties
heapq.heapify(pq)
while len(pq) > 1:
    fa, a = heapq.heappop(pq)                   # the two least frequent nodes
    fb, b = heapq.heappop(pq)
    sym.append(""); f.append(fa + fb); L.append(a); R.append(b)
    heapq.heappush(pq, (fa + fb, len(f) - 1))
root = pq[0][1]
code = {}
def walk(v, s):                                 # left edge = 0, right edge = 1
    if L[v] < 0:
        code[sym[v]] = s or "0"
        return
    walk(L[v], s + "0"); walk(R[v], s + "1")
walk(root, "")
for c, k in freq:
    print(f"'{c}' freq {k:2d} code {code[c]}")
bits = "".join(code[c] for c in text)
out, v = [], root
for b in bits:                                  # decode: walk down from the root
    v = L[v] if b == "0" else R[v]
    if L[v] < 0:
        out.append(sym[v]); v = root
print("first 24 bits:", bits[:24])
print(f"fixed 8-bit: {8 * len(text)} bits, huffman: {len(bits)} bits")
print(f"average: {len(bits) / len(text):.3f} bits per char")
print("decoded matches:", "".join(out) == text)

আউটপুট C++-এর সাথে হুবহু এক:

' ' freq  5 code 101
'a' freq  9 code 01
'b' freq  3 code 1100
'g' freq  1 code 10010
'h' freq  2 code 11111
'i' freq  2 code 0000
'l' freq  2 code 0001
'm' freq  3 code 1101
'n' freq  2 code 0010
'o' freq  3 code 1110
'r' freq  2 code 0011
's' freq  2 code 1000
't' freq  1 code 10011
'y' freq  1 code 11110
first 24 bits: 011101010011101100011100
fixed 8-bit: 304 bits, huffman: 134 bits
average: 3.526 bits per char
decoded matches: True

sorted(Counter(text).items()) অক্ষরের ক্রমে leaf দেয়, আর heapq tuple-কে বাঁ থেকে তুলনা করে — তাই (frequency, id) জোড়াই tie ভাঙে।

ফলাফল কী বলছে:

১৪টি আলাদা অক্ষর, ৩৮টি অক্ষরের লেখা। সবচেয়ে বেশি আসা a (৯ বার) পেয়েছে মাত্র ২ বিট, ফাঁকা জায়গা (৫ বার) ৩ বিট, আর একবার আসা g, t, y ৫ বিট। মোট 134 বিট — ASCII-র 304 বিটের অর্ধেকেরও কম, আর ১৪ অক্ষরের জন্য সেরা fixed code (৪ বিট, মোট 152)-এর চেয়েও কম। শেষ লাইন প্রমাণ করে decode করে হুবহু আগের লেখা ফিরেছে — lossless.

কোডের দুটি সতর্কতা। (১) এখানে বিটগুলো শেখার সুবিধায় '0'/'1' অক্ষরের string-এ রাখা — প্রতিটি “বিট” আসলে ৮ বিটের একটি অক্ষর! আসল compressor বিটগুলো byte-এ গেঁথে (bit packing) লেখে, আর সাথে tree বা code table-ও ফাইলে রাখে, নইলে অপর প্রান্ত decode করতে পারবে না। (২) লেখায় একটিই আলাদা অক্ষর থাকলে root নিজেই leaf, code ফাঁকা হতো — তাই "0" বসানো হয়েছে; তবে তখন decode-এর লুপ আলাদা করে সামলাতে হয়। Tree-তে n টি leaf আর n − 1 টি internal node — অ্যারে দিয়ে রাখা, pointer লাগেনি।

৬ · Decoding — tree ধরে হাঁটা

Encode করা সহজ: প্রতিটি অক্ষরের code পাশাপাশি বসান। Decode-এ বিটের স্রোত আর tree হাতে নিয়ে root থেকে হাঁটুন — 0 পেলে বাঁয়ে, 1 পেলে ডানে। Leaf-এ পৌঁছালেই একটি অক্ষর লিখুন আর root-এ ফিরে যান। Prefix-free বলেই leaf-এ পৌঁছানো মাত্র সিদ্ধান্ত নেওয়া যায়। উদাহরণের code দিয়ে বাংলা শব্দ “নাটক”-এর ইংরেজি বানান NATAK encode করলে পাই 01110011101 — মাত্র ১১ বিট (ASCII-তে ৪০)। এবার উল্টো পথে:

Code tree আর বিটের স্রোতরঙিন = এই অক্ষরের পথ

            
প্রতিটি অক্ষরের পর
এই অক্ষরে পড়া বিট
পথ
আউটপুট
  1. শুরু. হাতে ১১টি বিট আর অংশ ৫-এর tree. root 40-এ দাঁড়িয়ে আছি।
  2. প্রথম অক্ষর. 0 → বাঁয়ে 15-এ; এখনো internal node, থামা যাবে না। 1 → ডানে N8 — leaf! N লিখে root-এ ফিরলাম।
  3. দ্বিতীয়. 1 → 25, 1 → A14, leaf. A লিখলাম। মাত্র দুই বিটে সবচেয়ে বেশি আসা অক্ষর।
  4. তৃতীয়. 0 → 15, 0 → T7. T লিখলাম।
  5. চতুর্থ. 1 → 25, 1 → A14. আবার A.
  6. শেষ. 1 → 25, 0 → 11 (internal, চলুন), 1 → K6. K লিখলাম, বিট শেষ — ফল NATAK. প্রতিটি বিটে tree-তে ঠিক এক ধাপ, তাই মোট B বিট decode করতে O(B) সময়।

বিট শেষ হলে আমরা root-এ ফিরে আছি — এটিই বৈধ বার্তার চিহ্ন। মাঝপথে বিট ফুরালে (কোনো internal node-এ আটকে থাকলে) বার্তা কাটা পড়েছে। বাস্তবের দ্রুত decoder বিট ধরে হাঁটে না; তারা একসাথে কয়েকটি বিট নিয়ে একটি lookup টেবিলে দেখে নেয় কোন অক্ষর আর কত বিট খরচ হলো — একই ফল, অনেক কম ধাপে।

নিজে চালিয়ে দেখুন — আপনার লেখার Huffman code

নিচের টুলে “চালিয়ে দেখুন” চাপুন, তারপর বক্সে যেকোনো লেখা দিন — ইংরেজি বা বাংলা। টুলটি প্রতিটি অক্ষরের frequency আর code দেখাবে, আর তিনটি সংখ্যা তুলনা করবে: Huffman-এ মোট বিট, প্রতি অক্ষরে ৮ বিট ধরলে, আর UTF-8-এ আসলে কত বিট। বাংলা লিখে দেখুন — UTF-8-এ প্রতিটি বাংলা অক্ষর ৩ byte = ২৪ বিট, তাই সাশ্রয় আরও নাটকীয়।

const $ = id => document.getElementById(id);
const esc = s => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/ /g, "space");
function huffman(text) {
  const cnt = new Map();
  for (const ch of text) cnt.set(ch, (cnt.get(ch) || 0) + 1);       // code points, so Bangla works
  const sym = [...cnt.keys()].sort(), f = sym.map(s => cnt.get(s));
  const L = sym.map(() => -1), R = sym.map(() => -1);
  const pq = f.map((k, i) => [k, i]);                                   // (frequency, node id)
  while (pq.length > 1) {
    pq.sort((a, b) => a[0] - b[0] || a[1] - b[1]);                      // simple stand-in for a min-heap
    const [[fa, a], [fb, b]] = pq.splice(0, 2);
    f.push(fa + fb); L.push(a); R.push(b); sym.push("");
    pq.push([fa + fb, f.length - 1]);
  }
  const code = new Map();
  const walk = (v, s) => { if (L[v] < 0) code.set(sym[v], s || "0"); else { walk(L[v], s + "0"); walk(R[v], s + "1"); } };
  if (pq.length) walk(pq[0][1], "");
  return { cnt, code };
}
function show() {
  const t = $("tx").value, { cnt, code } = huffman(t), n = [...t].length;
  let bits = 0, rows = "<tr><th>char</th><th>freq</th><th>code</th></tr>";
  for (const c of [...cnt.keys()].sort()) {
    bits += cnt.get(c) * code.get(c).length;
    rows += `<tr><td>${esc(c)}</td><td>${cnt.get(c)}</td><td>${code.get(c)}</td></tr>`;
  }
  const utf8 = new TextEncoder().encode(t).length * 8;
  const msg = `${n} chars, ${cnt.size} distinct: huffman ${bits} bits, 8-bit ${8 * n}, UTF-8 ${utf8}`;
  $("hm").textContent = msg; $("ht").innerHTML = rows; console.log(msg);
}
$("go").onclick = show; $("tx").oninput = show; show();

শুরুতে টুলটি নিজেই জাতীয় সংগীতের লাইনটি নেয়; আউটপুট:

38 chars, 14 distinct: huffman 134 bits, 8-bit 304, UTF-8 304

134 বিট — C++ আর Python প্রোগ্রামের সাথে মেলে, কারণ tie ভাঙার নিয়ম একই। ইংরেজি অক্ষর UTF-8-এ ১ byte, তাই শেষ দুটি সংখ্যা সমান। এবার “আমার সোনার বাংলা” লিখে দেখুন: অক্ষর কম হলেও UTF-8-এর বিট অনেক বেশি, কারণ প্রতিটি বাংলা code point ৩ byte (কার-চিহ্ন যেমন া, ো আলাদা code point হিসেবে গোনা হয়)। এখানে heap-এর বদলে প্রতিবার ছোট অ্যারে সাজানো হয়েছে — কয়েক ডজন অক্ষরে এটি যথেষ্ট, কিন্তু অনেক আলাদা চিহ্ন হলে heap লাগবে। আরেকটি পরীক্ষা: একই অক্ষর বারবার লিখুন (যেমন aaaa) — একটিমাত্র চিহ্ন, code "0", প্রতি অক্ষরে ১ বিট।

৭ · কেন সর্বোত্তম — দুটি lemma আর induction

Greedy অ্যালগরিদম প্রায়ই ভুল উত্তর দেয় (পর্ব ৫০), তাই প্রমাণ ছাড়া Huffman-কে বিশ্বাস করার কারণ নেই। প্রমাণের দুটি ধাপ — পর্ব ৫০-এর exchange argument আর পর্ব ০৩-এর induction.

Lemma ১ (greedy পছন্দ নিরাপদ). x আর y সবচেয়ে কম frequency-র দুটি অক্ষর হলে এমন একটি সর্বোত্তম tree আছে, যেখানে x আর y ভাইবোন এবং সবচেয়ে গভীর স্তরে।

প্রমাণ. যেকোনো সর্বোত্তম tree T নিন। এটি full, তাই সবচেয়ে গভীর স্তরে অন্তত দুটি ভাইবোন leaf আছে, ধরুন a আর b. x-কে a-এর সাথে জায়গা বদল করুন। খরচের পরিবর্তন

Δ = f(x)·d(a) + f(a)·d(x) − f(x)·d(x) − f(a)·d(a) = (f(a) − f(x)) · (d(x) − d(a)) ≤ 0

কারণ f(a) ≥ f(x) (x সবচেয়ে বিরল) আর d(x) ≤ d(a) (a সবচেয়ে গভীরে)। খরচ বাড়েনি, তাই নতুন tree-ও সর্বোত্তম। একইভাবে y-কে b-এর সাথে বদলান। এখন x, y সবচেয়ে গভীরে ভাইবোন। ∎

Lemma ২ (জোড়া দিলে সমস্যা ছোট হয়). C থেকে x, y সরিয়ে একটি নতুন অক্ষর z বসান, f(z) = f(x) + f(y); নতুন বর্ণমালা C′. C′-এর যেকোনো tree T′-এ z-এর leaf ভেঙে দুই সন্তান x, y বসালে C-এর একটি tree T পাই, আর B(T) = B(T′) + f(x) + f(y) — কারণ x আর y z-এর চেয়ে এক স্তর গভীরে, বাকিরা অপরিবর্তিত।

উপপাদ্য. Huffman সর্বোত্তম tree দেয়। Induction অক্ষরের সংখ্যা n-এর ওপর। n = 2 হলে একমাত্র full tree — দুই leaf, প্রত্যেকে এক বিট। ধরুন n − 1 অক্ষরে সত্য। n অক্ষরে Huffman প্রথমে x, y জোড়ে, তারপর C′-তে Huffman চালায় — induction অনুযায়ী সেই T′ C′-এর জন্য সর্বোত্তম। বিপরীতে ধরুন C-এর কোনো tree U আছে যার B(U) < B(T). Lemma ১ অনুযায়ী U-কে এমনভাবে নিতে পারি যেখানে x, y ভাইবোন। তাদের জুড়ে z বানালে C′-এর tree U′, আর B(U′) = B(U) − f(x) − f(y) < B(T) − f(x) − f(y) = B(T′) — T′ সর্বোত্তম, তাই এটি অসম্ভব। ∎

উদাহরণে প্রমাণটি সংখ্যা দিয়ে দেখুন। শুরু একটি ইচ্ছাকৃত খারাপ tree থেকে — Huffman tree-তে শুধু A আর H-এর জায়গা উল্টে দেওয়া:

প্রতিটি অক্ষরের code, depth আর খরচরঙিন = এই ধাপে যা দেখছি

            
  1. খারাপ tree. সবচেয়ে বেশি আসা A আছে depth 4-এ, আর সবচেয়ে বিরল H depth 2-এ। Code এখনো prefix-free, কিন্তু খরচ 120 বিট।
  2. কাকে দেখছি. সবচেয়ে গভীর ভাইবোন A (1000) আর S (1001). সবচেয়ে বিরল দুটি H (2) আর S (3). S ইতিমধ্যে গভীরে; H নেই।
  3. Exchange. A আর H-এর জায়গা বদল। সূত্র: (f(A) − f(H)) × (d(H) − d(A)) = (14 − 2) × (2 − 4) = −24. খরচ 96 — Lemma ১-এর মতোই কমল (বা অন্তত বাড়ল না)।
  4. Merge. H আর S এখন গভীরতম ভাইবোন (1000, 1001). তাদের এক অক্ষর z (f = 5) বানালে z-এর code 100, আর খরচ 96 − 5 = 91 — Lemma ২.
  5. Induction. পাঁচ অক্ষরের সমস্যা C′-তে Huffman-এর খরচ 91, যা induction অনুযায়ী সর্বোত্তম। তাই ছয় অক্ষরে Huffman-এর 91 + 5 = 96-এর চেয়ে কম কোনো prefix-free code নেই।
মূল ধারণা

Greedy প্রমাণের দুটি স্তম্ভ এখানে স্পষ্ট: greedy পছন্দ (সবচেয়ে বিরল দুটিকে ভাই বানানো) কোনো সর্বোত্তম সমাধানকে নষ্ট করে না, আর পছন্দের পর বাকি সমস্যা একই ধরনের ছোট সমস্যা, যার সর্বোত্তম উত্তর পুরো সমস্যার সর্বোত্তম উত্তর দেয় (optimal substructure). পর্ব ৫২-এর dynamic programming-এও দ্বিতীয় স্তম্ভটি লাগবে, কিন্তু সেখানে প্রথমটি থাকে না।

৮ · Complexity — heap দিয়ে O(n log n), সাজানো থাকলে O(n)

ধরুন লেখার দৈর্ঘ্য m আর আলাদা অক্ষর n টি। খরচ ধাপে ধাপে:

  • গোনা: প্রতিটি অক্ষর একবার দেখা — O(m); ASCII-তে ২৫৬ ঘরের একটি অ্যারেই যথেষ্ট।
  • Tree বানানো: heapify O(n), তারপর n − 1 বার merge — প্রতিবার দুটি extract আর একটি insert, প্রতিটি O(log n). মোট O(n log n).
  • Code পড়া: tree-র ২n − 1 টি node একবার করে দেখা — O(n) (প্রতিটি code string বানালে মোট code দৈর্ঘ্যের সমান বাড়তি)।
  • Encode আর decode: আউটপুটের বিট সংখ্যা B ধরে O(m + B); decode-এ প্রতি বিটে এক ধাপ।
  • জায়গা: tree আর heap O(n), এর বাইরে ইনপুট ও আউটপুট।

সাধারণত n ছোট (byte হলে সর্বোচ্চ ২৫৬), তাই আসল খরচ লেখা পড়া আর বিট লেখায়। তবে অনেক অক্ষরের বর্ণমালায় (যেমন শব্দভিত্তিক coding) tree বানানোর অংশ গুরুত্বপূর্ণ।

দুটি queue-এর কৌশল — O(n)

frequency যদি আগে থেকেই ছোট থেকে বড় সাজানো থাকে, heap-ও লাগে না। দুটি সাধারণ queue নিন: Q1-এ সাজানো leaf-গুলো, Q2 শুরুতে খালি — নতুন বানানো node এখানে পেছনে যোগ হবে। প্রতিবার দুই queue-এর সামনের মধ্যে ছোটটি নিন, দুবার; জোড়া দিয়ে Q2-এর পেছনে রাখুন। চাবিকাঠি হলো: নতুন node-গুলো নিজেরাই বাড়তে থাকা ক্রমে তৈরি হয়, কারণ প্রতিবার আমরা অবশিষ্টদের মধ্যে সবচেয়ে ছোট দুটি নিই, আর অবশিষ্টরা আগের বাছাইয়ের চেয়ে ছোট হতে পারে না। তাই Q2 সবসময় সাজানো, আর “সবচেয়ে ছোট” মানে শুধু দুটি সামনের ঘর তুলনা — O(1). মোট O(n). উদাহরণে Q1 = 2 3 6 7 8 14; Q2-তে ক্রমে আসে 5, 11, 15, 25, 40 — সত্যিই বাড়তে থাকা। (এই কৌশলের কৃতিত্ব সাধারণত Jan van Leeuwen-কে দেওয়া হয়, ১৯৭৬.) সাজানো না থাকলে প্রথমে সাজাতে O(n log n) — তাই সার্বিক নিম্নসীমা বদলায় না।

৯ · Tie, canonical code, entropy আর বাস্তবের Huffman

Tie — একাধিক সঠিক উত্তর

দুটি node-এর frequency সমান হলে কাকে আগে নেবেন? যেকোনোটি নিলেই Huffman-এর খরচ সর্বোত্তম থাকে — প্রমাণে কোথাও “একমাত্র” দরকার হয়নি। কিন্তু tree-র আকার, এমনকি code-এর দৈর্ঘ্যও বদলাতে পারে। যেমন frequency 1, 1, 2, 2: একভাবে দৈর্ঘ্য 2, 2, 2, 2 (খরচ 12), আরেকভাবে 3, 3, 2, 1 (খরচ 1·3 + 1·3 + 2·2 + 2·1 = 12). মোট সমান, আকার আলাদা। তার ওপর যেকোনো internal node-এর বাঁ-ডান (0/1) অদলবদল করলে code বদলায়, খরচ নয়। তাই পরীক্ষার খাতায় আপনার code বইয়ের সাথে না মিললেও ভুল নয় — মোট বিট মিলিয়ে দেখুন। এই কারণেই এই পর্বের প্রোগ্রামে tie-breaking নিয়ম স্থির করা: (frequency, id). আমাদের নিয়মে পুরোনো node আগে বের হয়; এতে code-এর দৈর্ঘ্যের পার্থক্য কম থাকে, যা বাস্তবে সুবিধাজনক।

Canonical Huffman code — শুধু দৈর্ঘ্য পাঠালেই চলে

Decoder-কে code জানাতে হয়। পুরো tree পাঠানো ব্যয়বহুল আর জটিল। পর্যবেক্ষণ: খরচ নির্ভর করে শুধু প্রতিটি অক্ষরের code দৈর্ঘ্যের ওপর, আসল 0/1-এর ওপর নয়। তাই একটি নিয়ম ঠিক করুন যা দৈর্ঘ্য থেকেই code বানায় — canonical Huffman code: অক্ষরগুলোকে (দৈর্ঘ্য, অক্ষর) ক্রমে সাজান, প্রথমটিকে সব-শূন্য code দিন, পরেরটি আগেরটির +1, আর দৈর্ঘ্য বাড়লে ডানে 0 যোগ (বাঁয়ে shift).

(দৈর্ঘ্য, অক্ষর) ক্রমে সাজানোরঙিন = এই ধাপে বসানো code

            
  1. শুরু. Huffman tree থেকে শুধু দৈর্ঘ্য রাখলাম: A, N, T দুই বিট, K তিন, H আর S চার। সমান দৈর্ঘ্যে অক্ষরের ক্রম — তাই A N T K H S.
  2. দৈর্ঘ্য ২. প্রথম code 00 (A), তারপর +1: 01 (N), 10 (T).
  3. দৈর্ঘ্য ৩. পরের মান 10 + 1 = 11; দৈর্ঘ্য এক বাড়ল, তাই ডানে 0 যোগ: K = 110.
  4. দৈর্ঘ্য ৪. 110 + 1 = 111, দৈর্ঘ্য আবার বাড়ল: H = 1110.
  5. শেষ. S = 1110 + 1 = 1111. Code বদলেছে (A ছিল 11, এখন 00), কিন্তু প্রতিটি দৈর্ঘ্য একই, তাই মোট বিট 96 — আর code এখনো prefix-free.

এখন encoder শুধু প্রতিটি অক্ষরের দৈর্ঘ্য পাঠায় (অক্ষরের ক্রমে: A2 H4 K3 N2 S4 T2), decoder একই নিয়মে হুবহু একই code বানিয়ে নেয়। DEFLATE (zip, gzip, PNG) ঠিক এভাবেই কাজ করে, আর দৈর্ঘ্যের তালিকাটিকেও আবার Huffman দিয়ে ছোট করে। দৈর্ঘ্য বৈধ কি না, তা একটি সরল শর্তে ধরা যায় — Kraft inequality: Σ 2−l ≤ 1. এখানে 3·¼ + ⅛ + 2·1/16 = 1 — tree পুরো ভরা (full).

Entropy — কত ছোট করা সম্ভব

Huffman সেরা prefix-free code দেয় — কিন্তু সেই সেরাটি কত ভালো? ১৯৪৮ সালে Claude Shannon তাঁর “A Mathematical Theory of Communication” প্রবন্ধে উত্তর দেন। অক্ষরগুলো p1, …, pn সম্ভাবনায় এলে উৎসটির entropy

H = − Σ pi log₂ pi    বিট প্রতি অক্ষর

— অর্থাৎ বার্তায় আসলে কতটা “অনিশ্চয়তা” বা নতুন তথ্য আছে। যে অক্ষর প্রায় নিশ্চিত, সে প্রায় কোনো তথ্য দেয় না; বিরল অক্ষর অনেক তথ্য দেয় (log₂(1/p) বিট)। Shannon-এর source coding theorem বলে, অক্ষর-ধরে লেখা যেকোনো lossless code-এর গড় দৈর্ঘ্য L ≥ H, আর Huffman-এর জন্য

H ≤ LHuffman < H + 1

উদাহরণে H ≈ 2.341, L = 2.4. জাতীয় সংগীতের লাইনে H ≈ 3.500, L = 134/38 ≈ 3.526 — সীমার একদম কাছে। সব p যদি ½-এর ঘাত হয় (½, ¼, ⅛, …), তাহলে L = H হুবহু।

আরও গভীরে: নিচের সীমা আসে Kraft inequality আর Gibbs-এর অসমতা থেকে: Σ 2−li ≤ 1 হলে Σ pi li ≥ H. ওপরের সীমার জন্য Shannon-এর দৈর্ঘ্য li = ⌈log₂(1/pi)⌉ নিন — এগুলো Kraft মানে, আর গড় < H + 1; Huffman সর্বোত্তম বলে তার চেয়েও ভালো। “+1”-এর ফাঁক বড় হতে পারে: দুই অক্ষর 0.9 আর 0.1 সম্ভাবনায় এলে H ≈ 0.469, কিন্তু Huffman প্রতি অক্ষরে ১ বিটের কম দিতে পারে না। সমাধান: k টি অক্ষরের দলকে এক “অক্ষর” ধরলে প্রতি অক্ষরে ফাঁক < 1/k (বর্ণমালা অবশ্য সূচকীয়ভাবে বাড়ে) — অথবা arithmetic coding আর ANS, যারা ভগ্নাংশ বিটও খরচ করতে পারে।

বাস্তবে Huffman — zip থেকে JPEG, আর তার সীমা

zip, gzip, PNG

সবাই DEFLATE ব্যবহার করে: প্রথমে LZ77 আগের অংশের পুনরাবৃত্তি খুঁজে “পেছনে এত দূরে, এত লম্বা” রূপে লেখে, তারপর সেই চিহ্নগুলো canonical Huffman দিয়ে। Code-এর সর্বোচ্চ দৈর্ঘ্য ১৫ বিটে সীমিত।

JPEG আর MP3

ছবি বা শব্দ থেকে চোখ-কানে অদৃশ্য খুঁটিনাটি ফেলার (lossy) পর বাকি সংখ্যাগুলো lossless ভাবে লেখা হয় — বেসলাইন JPEG আর MP3 এই শেষ ধাপে Huffman ব্যবহার করে।

ওয়েব

HTTP/2-এর header সংকোচন (HPACK) একটি স্থির, আগে থেকে ঠিক করা Huffman code ব্যবহার করে — প্রতিবার tree পাঠাতে হয় না। Brotli-ও Huffman-ভিত্তিক।

সীমা আর উত্তরসূরি

Arithmetic coding পুরো বার্তাকে একটি ভগ্নাংশে লেখে, প্রায় entropy-তে পৌঁছায়। Jarek Duda-র ANS একই মানের সংকোচন দ্রুত দেয় — Zstandard আর Apple-এর LZFSE এটি ব্যবহার করে।

Huffman-এর সীমাগুলো মনে রাখুন। (১) প্রতিটি অক্ষরে পূর্ণসংখ্যা বিট — অতি সাধারণ অক্ষরে অপচয় (ওপরের 0.9/0.1 উদাহরণ)। (২) Frequency আগে জানতে হয়: হয় পুরো ফাইল দুবার পড়া (একবার গোনা, একবার লেখা), নয় চলতে চলতে tree বদলানো — adaptive Huffman. (৩) শুধু প্রতিটি অক্ষরের হার দেখে, পাশাপাশি অক্ষরের সম্পর্ক দেখে না: “ক্ষ”-এর পরে কী আসে, বা একই শব্দ বারবার — এগুলো ধরে LZ77-এর মতো পদ্ধতি বা context model. তাই বাস্তব compressor Huffman-কে একা নয়, অন্য কৌশলের সাথে জুড়ে ব্যবহার করে। (৪) Code table পাঠাতেও বিট লাগে; খুব ছোট বার্তায় সেটিই লাভ খেয়ে ফেলতে পারে।

১৯৫১ সালে MIT-তে Robert M. Fano-র information theory কোর্সে ছাত্রদের সামনে দুটি পথ ছিল: শেষের লিখিত পরীক্ষা দেওয়া, অথবা একটি term paper লেখা। Fano-র দেওয়া একটি term paper-এর বিষয় ছিল সবচেয়ে দক্ষ binary code খোঁজা — যে সমস্যা Fano নিজে আর Shannon তখনো পুরোপুরি সমাধান করতে পারেননি; তাঁদের Shannon-Fano পদ্ধতি ভালো, কিন্তু সবসময় সর্বোত্তম নয়। স্নাতকোত্তর ছাত্র David A. Huffman মাসের পর মাস চেষ্টা করে প্রায় হাল ছেড়ে পরীক্ষার পড়া শুরু করতে যাচ্ছিলেন — তখনই নিচ থেকে ওপরে tree বানানোর ধারণাটি তাঁর মাথায় আসে, আর তিনি সেটি সর্বোত্তম বলে প্রমাণও করেন। ফল প্রকাশিত হয় ১৯৫২ সালে Proceedings of the IRE-তে, “A Method for the Construction of Minimum-Redundancy Codes” নামে।

পরীক্ষা ও ইন্টারভিউতে:

  • Prefix-free code কেন দরকার? — অক্ষরের মাঝে কোনো বিভাজক ছাড়াই বিটের স্রোত একভাবে ভাঙা যায়; leaf-এ পৌঁছালেই অক্ষর নিশ্চিত।
  • Huffman-এর time complexity? — n টি আলাদা অক্ষরে binary heap দিয়ে O(n log n); frequency সাজানো থাকলে দুটি queue দিয়ে O(n).
  • Frequency 5, 9, 12, 13, 16, 45 হলে মোট বিট? — merge 14, 25, 30, 55, 100; যোগফল 224.
  • Huffman code কি অদ্বিতীয়? — না; tie আর 0/1 অদলবদলে code বদলায়, কিন্তু মোট বিট একই সর্বনিম্ন।
  • n টি দড়ি জোড়ার সর্বনিম্ন খরচ? — Huffman-এর মতোই: min-heap থেকে সবচেয়ে ছোট দুটি জুড়ে ফেরত রাখুন, যোগফলগুলো জমান — O(n log n).

১০ · AI-এর যুগে Huffman — tree ঠিক, খরচ ভুল

“Huffman coding-এর কোড লিখে দাও” — AI মুহূর্তেই লিখবে, আর বেশিরভাগ সময় tree ঠিকই বানাবে। ভুলগুলো লুকিয়ে থাকে কিনারায়: একটিমাত্র অক্ষরের লেখা, tie-এর কারণে প্রতিবার ভিন্ন code, বিটকে অক্ষর হিসেবে রেখে “সংকোচন” দাবি, বা code table না রেখেই ফাইল লেখা। আর ব্যাখ্যায় AI প্রায়ই নিজের code-কে “একমাত্র সঠিক” বলে, বা Shannon-Fano-র মতো ওপর থেকে ভাগ করা পদ্ধতিকে Huffman বলে চালিয়ে দেয়।

Huffman-এর কোডে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকীভাবে ধরবেন
একটিমাত্র আলাদা অক্ষরে code ফাঁকা string, ফলে encode-এ ০ বিট আর decode-এ অসীম লুপ বা ভুল"aaaa" আর খালি string দিয়ে চালান
Tie-এর নিয়ম নেই — Python-এ সমান frequency-তে node object তুলনা করে TypeError, C++-এ চালানোভেদে ভিন্ন code(frequency, counter) জোড়া দিন; সমান frequency-র ইনপুটে দুবার চালিয়ে মেলান
“O(n) সময়” বা “O(m log m)” — n আর m গুলিয়ে ফেলাকোনটি আলাদা অক্ষর (n), কোনটি লেখার দৈর্ঘ্য (m) — নিজে আলাদা করে লিখুন
বিট রাখা হয়েছে '0'/'1' অক্ষরে, তবু ফাইলের আকার কমেছে বলে দাবিআউটপুট ফাইলের byte গুনুন; bit packing আর code table-এর আকার হিসাবে আনুন
একবার সাজিয়ে র‍্যাংক অনুযায়ী code দেওয়া, বা ওপর থেকে সমান ভাগ (Shannon-Fano) — “Huffman” নামে15, 7, 6, 6, 5 দিয়ে চালান: সঠিক Huffman-এ মোট 87 বিট, Shannon-Fano-তে 89
“আপনার code ভুল, আমারটা সঠিক” — শুধু code মিলিয়ে বিচারমোট বিট Σ f·d তুলনা করুন আর prefix-free কি না দেখুন

AI-কে সমাধান লিখতে না বলে আপনার trace আর যুক্তি যাচাই করতে বলুন:

আমি DSA শিখছি, এখন Huffman coding. নিচে frequency, আমার হাতে বানানো Huffman tree-র ধাপ আর আমার কোড দিলাম: [এখানে frequency, ধাপগুলো আর কোড পেস্ট করুন] পুরো সমাধান নতুন করে লিখে দিও না। শুধু এগুলো করো: ১. প্রতিটি merge-এ heap-এ কী ছিল আর কোন দুটি বের হলো দেখাও, আমার ধাপের সাথে কোথায় অমিল বলো। ২. আমার code আর তোমার code আলাদা হলে, দুটির মোট বিট Σ f·d হিসাব করে দেখাও — শুধু code দেখে ভুল বলো না। ৩. একটিমাত্র অক্ষর, সব অক্ষর সমান frequency, আর খালি ইনপুটে আমার কোড কী করে? ৪. Time complexity কত — n (আলাদা অক্ষর) আর m (লেখার দৈর্ঘ্য) আলাদা রেখে ধাপে ধাপে যুক্তি দাও।

Huffman-এর কোড যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Shannon আর Fano-র মতো বিখ্যাত গবেষকরা যা পারেননি, একজন ছাত্র কীভাবে পারলেন — ওপর থেকে ভাগ আর নিচ থেকে জোড়ার পার্থক্যটি কোথায়?

১৯৪৮ সালের প্রবন্ধে Shannon দেখিয়েছিলেন, গড় code দৈর্ঘ্য entropy-র নিচে নামতে পারে না, আর entropy-র এক বিটের মধ্যে পৌঁছানো যায়। কিন্তু প্রতিটি নির্দিষ্ট frequency তালিকার জন্য ঠিক সর্বোত্তম code কীভাবে বানাতে হয়, সেটি খোলা প্রশ্ন ছিল। Shannon আর Fano আলাদাভাবে প্রায় একই পদ্ধতি দেন, যা আজ Shannon-Fano coding নামে পরিচিত: অক্ষরগুলো frequency অনুযায়ী সাজিয়ে এমন জায়গায় কাটুন যাতে দুই দলের মোট ওজন যতটা সম্ভব সমান হয়; বাঁ দলের সবার code 0 দিয়ে শুরু, ডানের 1 দিয়ে; তারপর প্রতিটি দলে আবার একই কাজ। এটি ওপর থেকে নিচে tree বানায় — অনেকটা পর্ব ৪৯-এর divide and conquer-এর মতো।

সমস্যা হলো, ওপরের স্তরে “সমান ভাগ” করার সিদ্ধান্ত নিচের স্তরের পরিণতি দেখে না। frequency 15, 7, 6, 6, 5 (মোট ৩৯) নিন। সবচেয়ে সমান ভাগ {15, 7} = 22 আর {6, 6, 5} = 17. তারপর {6 | 6, 5}. দৈর্ঘ্য হয় 2, 2, 2, 3, 3 — খরচ 30 + 14 + 12 + 18 + 15 = 89 বিট। Huffman নিচ থেকে শুরু করে: 5 + 6 = 11, 6 + 7 = 13, 11 + 13 = 24, 15 + 24 = 39. সবচেয়ে বেশি আসা অক্ষর পায় এক বিট, বাকি চারটি তিন বিট — খরচ 15 + 3 × 24 = 87 বিট। Shannon-Fano-র প্রথম ভাগেই 15-কে 7-এর সাথে জুড়ে দেওয়ায় 15 আর এক বিটে নামতে পারেনি।

Huffman-এর অন্তর্দৃষ্টি ছিল প্রশ্নটি উল্টে দেওয়া। ওপরের স্তরে কী হবে তা অনিশ্চিত, কিন্তু নিচের স্তর সম্পর্কে একটি কথা নিশ্চিত: সবচেয়ে বিরল দুটি অক্ষর সবচেয়ে গভীরে ভাইবোন হতে পারে (Lemma ১)। এই একটি নিশ্চিত সিদ্ধান্ত নিয়ে সমস্যাটি এক অক্ষর ছোট করা যায় — আর বাকিটা একই প্রশ্ন। নিশ্চিত তথ্য থেকে ধাপে ধাপে এগোনো, অনিশ্চিত অনুমান থেকে নয় — ভালো greedy নকশার এটিই রহস্য। গল্পটি প্রায়ই বলা হয় এভাবে: Huffman বহু মাস চেষ্টার পর হাল ছাড়তে যাচ্ছিলেন, আর ঠিক তখনই উল্টো দিক থেকে ভাবার ধারণাটি আসে। পরে তিনি বলেছিলেন বলে প্রচলিত, সমস্যাটি যে বিখ্যাত গবেষকদেরও আটকে রেখেছিল তা জানলে হয়তো চেষ্টাই করতেন না।

মূল উপলব্ধি: একই সমস্যা ওপর থেকে আর নিচ থেকে ভাবলে ভিন্ন অ্যালগরিদম আসে। Greedy সফল হয় তখনই, যখন প্রতিটি পছন্দের পক্ষে একটি প্রমাণযোগ্য নিশ্চয়তা থাকে — আর সেটি প্রায়ই সমস্যার “নিচের প্রান্তে” খুঁজে পাওয়া যায়।

প্র ০২Morse code variable-length হলেও prefix-free নয় — তবু টেলিগ্রাফ কীভাবে চলত, আর Huffman-এর সাথে তুলনায় এর নকশা কতটা ভালো?

১৮৪৪ সালে Washington থেকে Baltimore-এ পাঠানো “What hath God wrought” বার্তাটি টেলিগ্রাফের যুগের সূচনার প্রতীক। Samuel Morse আর তাঁর সহযোগী Alfred Vail-এর code-এ অক্ষরগুলো ডট (ছোট সংকেত) আর ড্যাশ (লম্বা সংকেত) দিয়ে লেখা। প্রচলিত গল্প অনুযায়ী Vail একটি ছাপাখানায় গিয়ে দেখেছিলেন কোন অক্ষরের সীসার টাইপ কতগুলো রাখা থাকে — যেটির টাইপ বেশি, সেটি ইংরেজিতে বেশি আসে। সেই অনুযায়ী E পেল একটি ডট, T একটি ড্যাশ, আর বিরল অক্ষর পেল লম্বা নকশা। এটি আসলে frequency দেখে variable-length code বানানো — Huffman-এর একশো বছরেরও বেশি আগে।

কিন্তু Morse prefix-free নয়: E (·) হলো I (··), S (···), H (····)-এর শুরু। তাহলে “····” মানে কি H, নাকি EEEE, নাকি IS? সমাধান হলো সময়ের বিরতি। Morse-এ আসলে তিন বা তার বেশি চিহ্ন: ডট, ড্যাশ, অক্ষরের মাঝের বিরতি, শব্দের মাঝের লম্বা বিরতি। প্রথাগত নিয়মে ড্যাশ তিন ডটের সমান লম্বা, অক্ষরের মাঝের বিরতি তিন ডট, শব্দের মাঝে সাত। অর্থাৎ প্রতিটি অক্ষরের শেষে একটি বিভাজক বসছে, যার নিজের খরচ আছে। Huffman-এর ভাষায়, বিরতি-সহ Morse একটি prefix-free code, যার বর্ণমালা দুই চিহ্নের নয় এবং যার চিহ্নগুলোর সময়-খরচ অসমান।

অসমান খরচের চিহ্ন দিয়ে সর্বোত্তম code খোঁজা আরও কঠিন সমস্যা — সাধারণ Huffman সেখানে সরাসরি খাটে না, আর এর জন্য আলাদা অ্যালগরিদম নিয়ে গবেষণা হয়েছে। তাছাড়া Morse-এর নকশা মানুষের কানের জন্য — অপারেটরকে শুনে চিনতে হয়, তাই নকশাগুলো মনে রাখার মতো হওয়াও জরুরি ছিল। কম্পিউটারের বিটে এসব প্রশ্ন নেই: 0 আর 1-এর খরচ সমান, আর বিরতির আলাদা চিহ্ন নেই। ঠিক এই সরল জগতেই Huffman পুরোপুরি সর্বোত্তম উত্তর দেয়।

মূল উপলব্ধি: “বেশি আসা জিনিসে ছোট নাম” ধারণাটি পুরোনো ও স্বাভাবিক। Huffman-এর অবদান ধারণাটি নয়, বরং নির্দিষ্ট মডেলে (সমান খরচের দুই চিহ্ন, prefix-free) সর্বোত্তম উত্তর বানানোর একটি পদ্ধতি আর তার প্রমাণ। মডেল বদলালে প্রশ্নও বদলায়।

প্র ০৩Huffman “সর্বোত্তম”, তবু আধুনিক compressor কেন arithmetic coding বা ANS-এর দিকে যাচ্ছে?

Huffman-এর সর্বোত্তমতা একটি নির্দিষ্ট খেলার নিয়মে: প্রতিটি অক্ষরকে আলাদা করে একটি পূর্ণসংখ্যা-দৈর্ঘ্যের বিট-নকশা দিতে হবে। এই নিয়মেই ক্ষতি। কোনো অক্ষরের সম্ভাবনা p হলে তার “আদর্শ” দৈর্ঘ্য log₂(1/p) বিট — p = 0.9 হলে মাত্র 0.152 বিট। কিন্তু Huffman তাকে অন্তত ১ বিট দিতে বাধ্য। সাদা-কালো স্ক্যান করা কাগজে প্রায় সব পিক্সেল সাদা; সাদা পিক্সেলের আদর্শ খরচ যেখানে ১ বিটের ছোট একটি ভগ্নাংশ, সেখানে প্রতিটিতে পুরো ১ বিট দেওয়া মানে কয়েক গুণ অপচয়।

Arithmetic coding এই নিয়মটি ভাঙে। পুরো বার্তাকে এটি 0 থেকে 1-এর মধ্যে একটি ক্রমশ সরু হতে থাকা ব্যবধান দিয়ে প্রকাশ করে: প্রতিটি অক্ষর ব্যবধানটিকে তার সম্ভাবনার অনুপাতে ছোট করে। শেষে ব্যবধানের ভেতরের একটি সংখ্যা লিখলেই চলে, আর মোট বিট প্রায় সব log₂(1/p)-এর যোগফল — entropy-র খুব কাছে, প্রতি অক্ষরে ভগ্নাংশ বিটও সম্ভব। ধারণাটি ১৯৭০-এর দশকে Jorma Rissanen প্রমুখের কাজে বাস্তব রূপ পায়। দুর্বলতা ছিল গতি (প্রতিটি অক্ষরে গুণ-ভাগ) আর দীর্ঘদিন পেটেন্টের জটিলতা — JPEG মানে arithmetic coding-এর বিকল্প থাকলেও বেশিরভাগ সফটওয়্যার Huffman-ই ব্যবহার করত।

২০০০-এর দশকের শেষে পোল্যান্ডের গবেষক Jarek Duda প্রস্তাব করেন Asymmetric Numeral Systems (ANS): পুরো অবস্থাকে একটি পূর্ণসংখ্যায় রেখে arithmetic coding-এর মতো সংকোচন, কিন্তু টেবিল দেখে Huffman-এর কাছাকাছি গতিতে। Duda এটি মুক্তভাবে প্রকাশ করেন। Yann Collet-এর Zstandard (২০১৬ সালে Facebook প্রকাশ করে) আর Apple-এর LZFSE এর একটি রূপ ব্যবহার করে। লক্ষ করুন, Zstandard-ও কিছু অংশে (literal byte-এ) Huffman রেখে দিয়েছে — কারণ সেখানে এটি দ্রুত আর যথেষ্ট ভালো।

আরেকটি বড় পার্থক্য মডেলে। এই পর্বের Huffman ধরে নেয় প্রতিটি অক্ষর স্বাধীন। বাস্তব লেখায় “q”-এর পরে প্রায় সবসময় “u”, বাংলায় হসন্তের পরে ব্যঞ্জন। এই প্রসঙ্গ (context) ধরে সম্ভাবনা বদলাতে পারলে entropy আরও কমে। Arithmetic coding আর ANS প্রতিটি অক্ষরে আলাদা সম্ভাবনা নিতে পারে সহজেই; Huffman-এ প্রতিটি প্রসঙ্গের জন্য আলাদা tree লাগে।

মূল উপলব্ধি: “সর্বোত্তম” সবসময় কোনো মডেলের ভেতরে। মডেলের সীমা (পূর্ণসংখ্যা বিট, স্বাধীন অক্ষর) ভাঙলেই আরও ভালো পদ্ধতি সম্ভব — Huffman তাই শেষ কথা নয়, বরং একটি পরিষ্কার, দ্রুত আর প্রমাণিত ভিত্তি।

প্র ০৪একটি zip ফাইলকে আবার zip করলে বা JPEG ছবিকে zip করলে প্রায় ছোট হয় না কেন — কোনো compressor কি সব ফাইল ছোট করতে পারে?

DEFLATE-এর দুটি ধাপ — LZ77 আর Huffman — দুটিই ডেটার “অপ্রয়োজনীয়তা” (redundancy) খায়। LZ77 (Abraham Lempel আর Jacob Ziv, ১৯৭৭) পুনরাবৃত্ত অংশকে “পেছনে এত দূরে, এত লম্বা” দিয়ে বদলায়; Huffman অসমান frequency-র সুবিধা নেয়। একবার সংকোচনের পর আউটপুটের byte-গুলো প্রায় সমান হারে আসে আর প্রায় কোনো পুনরাবৃত্তি থাকে না — দেখতে প্রায় এলোমেলো। এমন ডেটার entropy প্রতি byte-এ প্রায় ৮ বিট, তাই অংশ ৯-এর সীমা অনুযায়ী আর কমানো যায় না। JPEG নিজেই শেষ ধাপে Huffman করে রেখেছে, তাই zip প্রায় কিছুই পায় না; উল্টো header আর table যোগ হয়ে ফাইল সামান্য বড়ও হতে পারে। এজন্য zip প্রোগ্রাম এমন ফাইল “stored” — সংকোচন ছাড়া — রাখার বিকল্প রাখে।

আরও মৌলিক প্রশ্ন: এমন কোনো lossless compressor কি সম্ভব, যা প্রতিটি ফাইল অন্তত এক বিট ছোট করে? না — পর্ব ০৬-এর pigeonhole principle দিয়েই প্রমাণ। ঠিক n বিটের ফাইল আছে 2n টি। n বিটের চেয়ে ছোট সব ফাইল মিলিয়ে আছে 20 + 21 + … + 2n−1 = 2n − 1 টি। Lossless মানে দুটি আলাদা ইনপুট একই আউটপুটে যেতে পারবে না, অথচ 2n টি কবুতরের জন্য খোপ মাত্র 2n − 1 টি। তাই কোনো না কোনো ফাইল ছোট হবেই না। প্রতিটি compressor আসলে একটি বাজি: “বাস্তবের ফাইলে কাঠামো আছে” — লেখায় অসমান অক্ষর, ছবিতে পাশাপাশি পিক্সেলের মিল। এলোমেলো ডেটায় বাজি হারে।

এই যুক্তির একটি ব্যবহারিক দিক আছে। “যেকোনো ফাইল ১০ গুণ ছোট করে” — এমন বিজ্ঞাপন দেখলে নিশ্চিত জানবেন, এটি মিথ্যা বা lossy. আরেকটি দিক নিরাপত্তায়: এনক্রিপ্ট করা ডেটা এলোমেলো দেখায়, তাই সংকোচন সবসময় এনক্রিপশনের আগে করতে হয়।

মূল উপলব্ধি: সংকোচন কাঠামো খুঁজে বের করার খেলা। একবার কাঠামো ব্যবহার হয়ে গেলে আর কিছু বাকি থাকে না, আর কাঠামোহীন ডেটা কোনো অ্যালগরিদমই ছোট করতে পারে না — এটি গণনার সীমা, প্রকৌশলের দুর্বলতা নয়।

প্র ০৫বাংলা লেখায় Huffman কেমন কাজ করবে — আর frequency আগে না জানলে (যেমন লাইভ চ্যাটে) কী করা হয়?

UTF-8-এ প্রতিটি বাংলা code point (অক্ষর, কার-চিহ্ন, হসন্ত) ৩ byte, অর্থাৎ ২৪ বিট নেয়। অথচ সাধারণ বাংলা লেখায় ব্যবহৃত আলাদা code point-এর সংখ্যা একশোর কাছাকাছি বা কম — যার জন্য fixed-length code-এই ৭ বিট যথেষ্ট। তার ওপর া, ি, ে, র, ক-এর মতো চিহ্ন অনেক বেশি আসে, ঋ, ঞ, ঢ় অনেক কম — Huffman-এর জন্য আদর্শ অবস্থা। তাই বাংলা লেখায় শুধু অক্ষরভিত্তিক Huffman-ও UTF-8-এর তুলনায় বড় সাশ্রয় দেয়। এই পর্বের টুলে নিজের লেখা দিয়ে পরীক্ষা করুন। বাস্তবে gzip বা Brotli বাংলা ওয়েবপেজ byte ধরে সংকোচন করে; UTF-8-এর প্রথম byte-টি সব বাংলা অক্ষরে প্রায় একই হওয়ায় সেটিও সহজে ছোট হয়।

দ্বিতীয় প্রশ্নটি বেশি কৌশলী। Static Huffman-এ প্রথমে পুরো লেখা পড়ে গুনতে হয়, তারপর code table পাঠাতে হয়। লাইভ চ্যাট বা নেটওয়ার্কের স্রোতে পুরো ডেটা আগে পাওয়া যায় না। তিনটি পথ আছে। (১) আগে থেকে ঠিক করা স্থির code — HTTP/2-এর HPACK অনেক header-এর নমুনা থেকে একটি Huffman table বানিয়ে মানদণ্ডে লিখে দিয়েছে, দুই প্রান্তই তা জানে। (২) টুকরো ধরে কাজ — DEFLATE ডেটাকে block-এ ভাগ করে প্রতিটির জন্য নতুন table বানায়, ফলে ফাইলের ভিন্ন অংশের ভিন্ন চরিত্রও ধরা পড়ে। (৩) Adaptive Huffman — encoder আর decoder দুজনেই এ পর্যন্ত দেখা অক্ষর গুনে একই tree রাখে, আর প্রতিটি অক্ষরের পর tree একটু হালনাগাদ করে; কোনো table পাঠাতে হয় না। Newton Faller (১৯৭৩), Robert Gallager (১৯৭৮) আর Donald Knuth (১৯৮৫)-এর কাজ থেকে FGK পদ্ধতি, আর Jeffrey Vitter-এর ১৯৮৭ সালের উন্নত রূপ এর পরিচিত উদাহরণ। Unix-এর পুরোনো compact প্রোগ্রাম adaptive Huffman ব্যবহার করত।

Adaptive পদ্ধতির খরচ হলো প্রতিটি অক্ষরে tree হালনাগাদের কাজ, আর শুরুতে যখন গোনা কম, তখন code দুর্বল। তাই আজকের বেশিরভাগ ব্যবস্থা block-ভিত্তিক বা স্থির table পছন্দ করে, আর যেখানে মানিয়ে নেওয়া জরুরি সেখানে adaptive arithmetic coding বা ANS.

মূল উপলব্ধি: Huffman-এর tree শুধু অ্যালগরিদম নয়, একটি চুক্তি — encoder আর decoder-কে একই tree জানতে হয়। সেই চুক্তি কীভাবে ভাগ হবে (পাঠিয়ে, আগে থেকে ঠিক করে, নাকি দুজনে একসাথে শিখে), সেটিই বাস্তব নকশার বড় প্রশ্ন।

অনুশীলন

খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।

  1. হাতে trace করুন: frequency a = 26, e = 11, i = 9, o = 6, u = 5, y = 3. Huffman tree বানান (tie হলে আগে থাকা node আগে), প্রতিটি অক্ষরের code-এর দৈর্ঘ্য আর মোট বিট বলুন। ৩-বিটের fixed code-এর সাথে তুলনা করুন।

    Merge: y3 + u5 = 8; {6, 8, 9, 11, 26}-এ o6 + 8 = 14; {9, 11, 14, 26}-এ i9 + e11 = 20; {14, 20, 26}-এ 14 + 20 = 34; শেষে 26 + 34 = 60. দৈর্ঘ্য: a = 1; e, i, o = 3; u, y = 4.

    মোট বিট = 8 + 14 + 20 + 34 + 60 = 136 (যাচাই: 26·1 + 3·(11 + 9 + 6) + 4·(5 + 3) = 26 + 78 + 32 = 136). Fixed code-এ 60 × 3 = 180, তাই প্রায় ২৪% সাশ্রয়। এখানে কোনো tie আসেনি।

  2. ভেবে বলুন: code {a = 0, b = 01, c = 10}-এ 010-এর দুটি ভিন্ন অর্থ বের করুন। তারপর মাত্র একটি code বদলে একে prefix-free বানান, যাতে কোনো code-এর দৈর্ঘ্য ৩ ছাড়ায় না।

    0|10 = “ac” আর 01|0 = “ba”. সমস্যা: 0 হলো 01-এর শুরু। একটি সমাধান a = 00 নিলে {00, 01, 10} — কেউ কারও শুরু নয়। (a = 11 নিলেও চলে।)

    Tree-তে দেখলে: আগে a ছিল root-এর বাঁ সন্তানে, অথচ সেই node-এর নিচে b-ও ছিল — অক্ষর internal node-এ বসেছিল। prefix-free code-এ অক্ষর শুধু leaf-এ থাকে।

  3. ভেবে বলুন: frequency 1, 1, 2, 3, 5, 8 (Fibonacci সংখ্যা) হলে Huffman tree-র আকার কেমন হয়, আর সবচেয়ে লম্বা code কত বিট? সাধারণভাবে n টি অক্ষরে Huffman code সর্বোচ্চ কত লম্বা হতে পারে?

    1 + 1 = 2; তারপর 2 + 2 = 4 (tie: পুরোনো leaf 2 আর নতুন node 2); 3 + 4 = 7; 5 + 7 = 12; 8 + 12 = 20. প্রতিবার নতুন node পরের অক্ষরের সাথে জোড়া পায় — tree একটি লম্বা শিকল। সবচেয়ে বিরল দুটির code ৫ বিট = n − 1.

    n টি leaf-এর full binary tree-তে depth n − 1-এর বেশি হতে পারে না, তাই সর্বোচ্চ দৈর্ঘ্য n − 1. Fibonacci-ধাঁচের frequency ঠিক এই খারাপ অবস্থা আনে। এজন্যই DEFLATE-এর মতো ফরম্যাট দৈর্ঘ্য ১৫ বিটে সীমিত রাখে আর প্রয়োজনে “length-limited” Huffman (যেমন package-merge পদ্ধতি) ব্যবহার করে।

  4. কোড লিখুন: n টি frequency দেওয়া (সাজানো নয়)। Tree না বানিয়েই শুধু Huffman-এর মোট বিট (= merge-গুলোর যোগফল) ফেরত দেয় এমন ফাংশন লিখুন, আর time ও space complexity বলুন। frequency সাজানো থাকলে কীভাবে O(n) করবেন?

    Min-heap থেকে সবচেয়ে ছোট দুটি তুলে যোগফল জমান আর যোগফল heap-এ ফেরত দিন — এটিই “দড়ি জোড়ার সর্বনিম্ন খরচ” সমস্যার সমাধান।

    long long huffmanCost(const vector<long long>& freq) {
        priority_queue<long long, vector<long long>, greater<>> pq(freq.begin(), freq.end());
        long long cost = 0;                     // n = 1: no merge, cost 0 (code "0" would cost freq[0])
        while (pq.size() > 1) {
            long long a = pq.top(); pq.pop();
            long long b = pq.top(); pq.pop();
            cost += a + b;                      // every merge adds one bit to all leaves below it
            pq.push(a + b);
        }
        return cost;
    }

    Range থেকে priority_queue বানানো O(n) (ভেতরে build-heap), তারপর n − 1 টি merge, প্রতিটি O(log n) — মোট O(n log n), জায়গা O(n). long long নেওয়া হয়েছে কারণ বড় ইনপুটে যোগফল int ছাড়াতে পারে। সাজানো থাকলে অংশ ৮-এর দুটি queue: একটি index দিয়ে সাজানো অ্যারে পড়ুন, আরেকটি অ্যারেতে নতুন যোগফলগুলো পেছনে যোগ করুন; প্রতিবার দুই সামনের মধ্যে ছোটটি নিন — O(n). {5, 9, 12, 13, 16, 45}-এ উত্তর 224 আসা উচিত।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি লেখার Huffman code বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. লেখায় প্রতিটি আলাদা অক্ষর কতবার এসেছে গোনা
  2. প্রতিটি অক্ষরকে একটি leaf বানিয়ে frequency-কে key ধরে min-heap-এ রাখা
  3. Heap থেকে সবচেয়ে কম frequency-র দুটি node বের করা
  4. দুটিকে সন্তান করে নতুন node বানানো, যার frequency দুটির যোগফল, আর heap-এ ফেরত রাখা
  5. Heap-এ একটি node থাকা পর্যন্ত আগের দুই ধাপ চালিয়ে, root থেকে বাঁয়ে 0 আর ডানে 1 পড়ে code লেখা

নিচের কোন code-সমষ্টিটি prefix-free?

  • {0, 01, 11}
  • {0, 10, 110, 111}
  • {1, 10, 100}
  • {00, 01, 0}

চারটি অক্ষরের frequency 1, 1, 2, 4. Huffman code-এ পুরো বার্তা লিখতে মোট কত বিট লাগে?

  • 14
  • 16
  • 12
  • 8

n টি আলাদা অক্ষরের frequency থেকে binary min-heap দিয়ে Huffman tree বানাতে worst case time complexity কত?

  • O(n)
  • O(n²)
  • O(n log n)
  • O(log n)

H যদি অক্ষরের সম্ভাবনা থেকে পাওয়া entropy হয় আর L Huffman code-এর গড় দৈর্ঘ্য (বিট প্রতি অক্ষর), তাহলে কোনটি সবসময় সত্য?

  • L = H
  • L < H
  • H + 1 ≤ L
  • H ≤ L < H + 1

n টি অক্ষরের (n ≥ 2) Huffman code-এ একটি code-এর দৈর্ঘ্য সর্বোচ্চ কত হতে পারে?

  • ⌈log₂ n⌉
  • n − 1
  • n
  • কোনো সীমা নেই

দুজন একই frequency তালিকা থেকে সঠিকভাবে Huffman tree বানিয়ে আলাদা code পেলেন। কোন বক্তব্যটি সঠিক?

  • অন্তত একজন নিশ্চয়ই ভুল করেছেন
  • দুজনের প্রতিটি অক্ষরের code-এর দৈর্ঘ্য অবশ্যই এক
  • দুজনের মোট বিট (Σ f·d) সমান
  • একটি code prefix-free না-ও হতে পারে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. বেশি আসা অক্ষরে ছোট code, কম আসায় লম্বা — prefix-free হলে বিভাজক ছাড়াই একভাবে decode হয়; প্রতিটি এমন code একটি code tree.
  2. খরচ B(T) = Σ f·d = সব internal node-এর ওজনের যোগফল = Huffman-এর merge-গুলোর যোগফল।
  3. Huffman: min-heap থেকে সবচেয়ে বিরল দুটি জুড়ে ফেরত রাখুন, n − 1 বার — O(n log n); সাজানো frequency-তে দুটি queue দিয়ে O(n).
  4. সর্বোত্তমতার প্রমাণ: বিরলতম দুটি গভীরতম ভাইবোন হতে পারে (exchange) + জোড়া দিলে B কমে f(x) + f(y) (induction).
  5. H ≤ L < H + 1; tie-তে code বদলায়, খরচ নয়; canonical code-এ শুধু দৈর্ঘ্য পাঠালেই চলে — DEFLATE, JPEG-এ এভাবেই।

পরের পর্বে: যেখানে কোনো নিরাপদ greedy পছন্দ নেই, সেখানে কী করবেন? সব উপ-সমস্যার উত্তর একবার করে হিসাব করে মনে রাখা — dynamic programming-এর ভিত্তি: memoization আর tabulation.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান