সরাসরি পর্বে যান
পর্ব ৬৮ · ৮৩-এর মধ্যে · মডিউল ১১

Prefix code আর Huffman coding

Prefix Codes and Huffman Coding — Kraft–McMillan Inequality, Optimality Proof and Entropy Bounds

Taught in Bangla, this lesson shows prefix codes as leaves of a binary tree, proves the Kraft inequality with its converse and McMillan's extension to uniquely decodable codes, and builds Huffman codes by merging the two rarest symbols. Optimality is proved with two lemmas and induction, ties are discussed, and entropy gives H ≤ L < H + 1. For six order states the page compares 3-bit fixed codes, a 2.38-bit Huffman average and 2.31-bit entropy. In-page Python builds codes from lengths and checks Huffman optimality.

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

  • Code, codeword, fixed আর variable length, unique decodability আর prefix code-এর নির্ভুল সংজ্ঞা
  • Prefix code আর binary tree-র মিল, Kraft-এর অসমতা আর তার বিপরীতের নির্মাণসহ প্রমাণ, McMillan-এর উপপাদ্য
  • Huffman-এর algorithm হাতে চালানো, আর তার সর্বোত্তমতার সম্পূর্ণ প্রমাণ (দুটি lemma আর induction)
  • Tie থাকলে আলাদা Huffman tree, কিন্তু একই গড় দৈর্ঘ্য কেন
  • Entropy, Gibbs-এর অসমতা আর H ≤ L < H + 1-এর প্রমাণ, আর বাস্তবে Huffman কোথায় বসে

১ · Code কী — fixed, variable আর unique decodability

বাজারঘরের সার্ভার প্রতিদিন লাখো অর্ডার-অবস্থা লগে লেখে: নতুন, প্যাক হয়েছে, পথে, পৌঁছেছে, বাতিল, ফেরত। প্রতিটির জন্য পুরো শব্দ না লিখে ছোট একটি সংকেত লিখলেই চলে। প্রশ্ন হলো সংকেত কত ছোট করা যায়, আর ছোট করতে গিয়ে পড়ার সময় কোনো বিভ্রান্তি তৈরি হয় কি না। মোবাইলে নম্বর ডায়াল করার কথা ভাবুন: “999” ডায়াল করলেই ফোন জরুরি নম্বর ধরে নেয়, কারণ কোনো সাধারণ নম্বর “999” দিয়ে শুরু হয় না। এটাই এই পর্বের মূল ধারণা।

লগের ছয়টি অবস্থার সংক্ষিপ্ত নাম ধরি: N (নতুন), P (প্যাক), S (পথে, shipped), D (পৌঁছেছে, delivered), C (বাতিল, cancelled), R (ফেরত, returned). প্রতি ১০০টি লগ-লাইনে এদের গড় সংখ্যা (কাল্পনিক, উদাহরণের জন্য) D 36, S 24, P 15, N 12, C 8, R 5.

সংজ্ঞা ১.১ (code আর codeword)

A একটি সসীম alphabet, অর্থাৎ চিহ্নের সেট (এখানে {N, P, S, D, C, R})। একটি binary code হলো একটি ফাংশন c: A → {0, 1}+, যা প্রতিটি চিহ্নকে একটি অশূন্য bit string দেয়; c(a)-কে a-এর codeword বলে, আর তার দৈর্ঘ্য l(a). একটি বার্তা a1a2⋯ak-এর encoding হলো codeword-গুলো পাশাপাশি বসানো: c(a1)c(a2)⋯c(ak) (concatenation, পাশাপাশি জোড়া)। এই বর্ধিত ফাংশনকে code-এর extension c* বলে।

সব codeword সমান দৈর্ঘ্যের হলে তা fixed-length code. ছয়টি চিহ্নের জন্য অন্তত ⌈log2 6⌉ = 3 bit লাগে, কারণ 2 bit দিয়ে মাত্র 22 = 4টি আলাদা string হয় (পর্ব ৩১-এর গুণের নিয়ম)। দৈর্ঘ্য আলাদা হতে পারলে তা variable-length code; লাভের আশা এখানে: ঘন ঘন আসা D-কে ছোট codeword দিলে মোট বিট কমে। টেলিগ্রাফের Morse code এই বুদ্ধিতেই বেশি ব্যবহৃত E-কে একটি ডট দেয়।

কিন্তু variable length-এর একটি ঝুঁকি আছে। ধরুন c(a) = 0, c(b) = 1, c(c) = 01. তাহলে “01” মানে “ab” না “c”? দুটোই হতে পারে, অর্থাৎ বার্তা হারিয়ে যায়।

সংজ্ঞা ১.২ (uniquely decodable আর prefix code)

Code c uniquely decodable (UD), যদি তার extension c* one-to-one হয়: দুটি আলাদা বার্তার encoding কখনো একই bit string হয় না। c একটি prefix code (prefix-free code), যদি কোনো codeword অন্য কোনো codeword-এর prefix (শুরুর অংশ) না হয়।

তিনটি ছোট উদাহরণ সম্পর্কগুলো স্পষ্ট করে:

CodePrefix?UD?কারণ
{0, 1, 01}নানা“01” দুইভাবে পড়া যায়
{0, 01, 11}নাহ্যাঁ0 হলো 01-এর prefix, তবু উল্টো দিক থেকে পড়লে একভাবেই ভাঙে
{0, 10, 110, 111}হ্যাঁহ্যাঁকোনো codeword অন্যটির শুরু নয়

দ্বিতীয় সারির code-টি UD, কারণ কোনো codeword অন্যটির শেষাংশ (suffix) নয়: বার্তাটি শেষ থেকে পড়লে প্রতিবার একটি codeword নিশ্চিতভাবে চেনা যায়। কিন্তু সামনে থেকে পড়তে গেলে “0” দেখেই বলা যায় না সেটি একা 0 না 01-এর শুরু; অপেক্ষা করতে হয়, কখনো পুরো বার্তার শেষ পর্যন্ত। Prefix code-এ এই অপেক্ষা নেই: একটি codeword শেষ হওয়ামাত্র চেনা যায়। তাই prefix code-কে instantaneous code-ও বলে।

উপপাদ্য ১.৩

প্রতিটি prefix code uniquely decodable.

  1. ধরুন দুটি আলাদা বার্তা u1⋯uj আর v1⋯vk-এর encoding একই string w. প্রথম যে স্থানে চিহ্ন আলাদা, তার আগের অংশ বাদ দিলেও encoding সমান থাকে, তাই ধরে নেওয়া যায় u1 ≠ v1 (একটি বার্তা অন্যটির শুরু হলে বাকি অংশের encoding ফাঁকা string হতো, যা অসম্ভব, কারণ codeword অশূন্য)। — সাধারণ অংশ ছাঁটা
  2. w শুরু হয় c(u1) দিয়ে, আবার c(v1) দিয়েও। দুটির যেটি ছোট (বা সমান), সেটি অন্যটির prefix. — একই string-এর দুটি শুরু
  3. u1 ≠ v1 বলে দুটি আলাদা codeword, আর একটি অন্যটির prefix: prefix code-এর সংজ্ঞার বিরোধ। ∎

উল্টোটা সত্য নয়, টেবিলের দ্বিতীয় সারিই প্রমাণ। তবু §৩-এ দেখবেন, codeword-এর দৈর্ঘ্যের দিক থেকে UD code prefix code-এর চেয়ে বেশি কিছু দিতে পারে না।

২ · Prefix code আর binary tree — leaf মানে codeword

একটি binary tree নিন, যার প্রতিটি node-এর বাঁ দিকের edge-এ 0 আর ডান দিকের edge-এ 1 লেখা। Root থেকে কোনো node পর্যন্ত পথের লেবেলগুলো পরপর পড়লে একটি bit string পাওয়া যায়, যার দৈর্ঘ্য সেই node-এর গভীরতা; ইংরেজিতে একে depth বলে। নিচের ছবিটি বাজারঘরের লগের জন্য §৪-এ বানানো Huffman code-এর code tree.

বাজারঘরের অর্ডার-অবস্থার code tree

বৃত্তে internal node আর তার নিচের সব leaf-এর মোট গণনা; সবুজ বাক্সে চিহ্ন আর গণনা; বাক্সের নিচে codeword (root থেকে পথের লেবেল)।

Huffman code tree for six order statuses: P 00, S 01, D 11, N 100, R 1010, C 1011 01 01 01 01 01 1003961 2513 P 15S 24D 36 N 12R 5C 8 000111 10010101011
উপপাদ্য ২.১ (prefix code ⇔ leaf-এর সেট)

Codeword-এর একটি সেট prefix-free ঠিক তখন, যখন একটি binary tree আছে যার leaf-গুলো ঠিক সেই codeword-গুলো (আর কোনো codeword internal node-এ বসে না)। এই tree-তে codeword-এর দৈর্ঘ্য = leaf-এর গভীরতা।

  1. (⇒) প্রতিটি codeword-এর সব prefix নিয়ে একটি tree বানান: root ফাঁকা string, আর string x-এর সন্তান x0 ও x1, তবে কেবল সেগুলোই রাখুন যেগুলো কোনো codeword-এর prefix. কোনো codeword w যদি internal node হতো, তবে w-এর একটি সন্তান অন্য কোনো codeword-এর prefix, অর্থাৎ w নিজেই সেই codeword-এর prefix, যা নিষেধ। তাই প্রতিটি codeword leaf, আর প্রতিটি leaf কোনো codeword (কারণ leaf নিজেই কোনো codeword-এর prefix, আর আর বাড়ে না)। — prefix-এর tree
  2. (⇐) দুটি leaf u, v-এর একটি অন্যটির prefix হলে tree-তে u থেকে v-তে নামার পথ থাকত, তখন u-এর সন্তান থাকত, অর্থাৎ u leaf নয়। বিরোধ। ∎ — leaf-এর সংজ্ঞা

Decoding এই ছবিতে হাঁটার মতো: root থেকে শুরু করে প্রতিটি বিট পড়ে বাঁয়ে বা ডানে যান; leaf-এ পৌঁছালে সেই চিহ্ন লিখুন আর root-এ ফিরুন। Tree-র height h হলে প্রতিটি চিহ্নে বড়জোর h ধাপ। একটি পর্যবেক্ষণ পরে কাজে লাগবে।

Lemma ২.২ (সর্বোত্তম tree full)

প্রতিটি চিহ্নের সম্ভাবনা ধনাত্মক হলে, গড় দৈর্ঘ্য সবচেয়ে কম এমন prefix code-এর tree একটি full binary tree: প্রতিটি internal node-এর ঠিক দুটি সন্তান।

প্রমাণ: কোনো internal node v-এর একটিমাত্র সন্তান থাকলে সেই সন্তানের subtree-টিকে তুলে v-এর জায়গায় বসান। v-এর নিচের প্রতিটি leaf-এর গভীরতা এক কমে, বাকিরা বদলায় না, আর leaf-গুলো তখনো prefix-free. ধনাত্মক সম্ভাবনার অন্তত একটি leaf v-এর নিচে আছে, তাই গড় দৈর্ঘ্য কঠোরভাবে কমে; সর্বোত্তম code-এ তাই এমন v থাকতে পারে না। ∎ Full binary tree-র recursive সংজ্ঞা আর “leaf = internal + 1”-এর ধরনের হিসাব পর্ব ১২-এ দেখেছেন; rooted আর m-ary tree-র গণনা পর্ব ৬৫ “Tree — ধর্ম, rooted tree আর m-ary tree-এর হিসাব”-এ।

৩ · Kraft-এর অসমতা, তার বিপরীত আর McMillan

কোন দৈর্ঘ্যগুলো একসঙ্গে সম্ভব? চারটি চিহ্নকে দৈর্ঘ্য 1, 1, 1, 2 দেওয়া যায় না: দৈর্ঘ্য 1-এর string মাত্র দুটি, 0 আর 1. এই “জায়গার হিসাব” একটি সুন্দর অসমতায় ধরা পড়ে।

উপপাদ্য ৩.১ (Kraft-এর অসমতা)

দৈর্ঘ্য l1, …, ln-এর একটি binary prefix code থাকলে

∑i=1..n 2−li ≤ 1.

উল্টোভাবে, যেকোনো ধনাত্মক পূর্ণসংখ্যা l1, …, ln এই অসমতা মানলে ঠিক এই দৈর্ঘ্যের একটি prefix code আছে।

প্রতিটি পদ 2−l-কে ভাবুন codeword-টি সব সম্ভাব্য দীর্ঘ string-এর কত ভাগ “দখল” করে: দৈর্ঘ্য 1-এর codeword অর্ধেক, দৈর্ঘ্য 2-এর এক-চতুর্থাংশ। মোট দখল এক-এর বেশি হতে পারে না।

প্রমাণ (prefix ⇒ অসমতা): ধরুন m = max li.

  1. দৈর্ঘ্য li-এর codeword wi-কে prefix হিসেবে রাখে এমন দৈর্ঘ্য m-এর string ঠিক 2m−liটি: বাকি m − liটি বিট যেকোনো। — গুণের নিয়ম
  2. আলাদা i আর j-এর এই দুই সেট disjoint: একটি string x দুটিতেই থাকলে wi আর wj দুটিই x-এর prefix, তাই ছোটটি বড়টির prefix, যা নিষেধ। — prefix-free
  3. সব সেট দৈর্ঘ্য m-এর 2mটি string-এর ভেতরে, তাই ∑ 2m−li ≤ 2m. দুই দিককে 2m দিয়ে ভাগ করলে অসমতা। ∎ — disjoint সেটের যোগ

প্রমাণ (বিপরীত, নির্মাণসহ): দৈর্ঘ্যগুলো সাজান l1 ≤ l2 ≤ ⋯ ≤ ln, আর aj = ∑i<j 2−li (a1 = 0 ধরে)। Codeword wj হলো aj-এর binary বিস্তারের দশমিক-বিন্দুর পরের প্রথম ljটি বিট।

  1. i < j হলে li ≤ lj, তাই aj হলো 2−lj-এর পূর্ণ গুণিতক; ফলে প্রথম ljটি বিটই aj-কে হুবহু ধরে, কিছু ছাঁটা পড়ে না। — ছোট ভগ্নাংশ বড় ঘাতের গুণিতক
  2. aj ≤ 1 − 2−lj < 1, কারণ মোট যোগ ≤ 1 আর aj-এ j-তম পদটি নেই। তাই ljটি বিটে জায়গা হয়। — Kraft-এর শর্ত
  3. wi দিয়ে শুরু হয় এমন সব বিস্তারের সংখ্যা ঠিক ব্যবধি [ai, ai + 2−li)-এ পড়ে। — binary বিস্তার
  4. i < j হলে aj ≥ ai + 2−li, তাই aj সেই ব্যবধির বাইরে: wi হলো wj-এর prefix নয়। আর lj ≥ li বলে wj-ও wi-এর prefix হতে পারে না (সমান দৈর্ঘ্যে দুটি আলাদা)। ∎ — ব্যবধি পরপর, ওভারল্যাপ নেই
1lengths = 1, 2, 3, 32j = 1: a = 03j = 2: a = 1/24j = 3: a = 3/45j = 4: a = 7/86sum = 1: tree full
aj, তার binary রূপ আর codeword
a (binary)
codeword
ব্যবধি
যোগফল
  1. শর্ত পরীক্ষা। দৈর্ঘ্য সাজানো আছে: 1, 2, 3, 3. Kraft যোগ 1/2 + 1/4 + 1/8 + 1/8 = 1 ≤ 1, তাই উপপাদ্য ৩.১ বলে এই দৈর্ঘ্যের prefix code আছে। এখন সেটি বানাই।
  2. প্রথম শব্দ। a1 = 0, binary-তে 0.000…; দৈর্ঘ্য 1, তাই প্রথম 1টি বিট: 0. এটি [0, 1/2) ব্যবধি দখল করে, অর্থাৎ 0 দিয়ে শুরু সব string.
  3. দ্বিতীয় শব্দ। a2 = 1/2 = 0.1 (binary রূপে)। দৈর্ঘ্য 2, তাই প্রথম 2টি বিট: 10. ব্যবধি [1/2, 3/4); প্রথম ব্যবধির সঙ্গে ওভারল্যাপ নেই।
  4. তৃতীয় শব্দ। a3 = 1/2 + 1/4 = 3/4 = 0.11. দৈর্ঘ্য 3: 110, ব্যবধি [3/4, 7/8).
  5. চতুর্থ শব্দ। a4 = 7/8 = 0.111. দৈর্ঘ্য 3: 111, ব্যবধি [7/8, 1). Kraft-এর প্রমাণের ধাপ ২ অনুযায়ী a4 = 1 − 1/8 < 1, তাই জায়গা হলো।
  6. ফল। Code {0, 10, 110, 111}: কোনো codeword অন্যটির শুরু নয়। যোগফল ঠিক 1, তাই ব্যবধিগুলো [0, 1) পুরো ঢেকে ফেলে আর tree full: আর কোনো codeword যোগ করা যায় না, কোনোটি ছোট করাও যায় না। যোগফল 1-এর কম হলে tree-তে ফাঁকা ডাল থাকত। (নিচের কোডে একই ফল।)

Prefix code-এ সীমাবদ্ধ থাকলে কি আমরা কিছু হারাই? UD code-এর দৈর্ঘ্য কি Kraft ভেঙে আরও ছোট হতে পারে? না, এবং প্রমাণটি গণনার একটি চমৎকার কৌশল।

উপপাদ্য ৩.২ (McMillan-এর অসমতা)

যেকোনো uniquely decodable binary code-এর দৈর্ঘ্য l1, …, ln হলে ∑ 2−li ≤ 1.

  1. S = ∑ 2−li, m = max li, আর k যেকোনো ধনাত্মক পূর্ণসংখ্যা। Sk গুণ করে খুললে প্রতিটি পদ এক একটি k-চিহ্নের বার্তার জন্য 2−(বার্তার encoding-এর দৈর্ঘ্য). — বিস্তার
  2. দৈর্ঘ্য অনুযায়ী জড়ো করুন: Sk = ∑t=k..km At 2−t, যেখানে At = যেসব k-চিহ্নের বার্তার encoding দৈর্ঘ্য t, তাদের সংখ্যা। — পদ জড়ো করা
  3. Code UD, তাই আলাদা বার্তার encoding আলাদা string; দৈর্ঘ্য t-এর string মাত্র 2tটি, তাই At ≤ 2t. — one-to-one, পর্ব ৩২-এর যুক্তি
  4. অতএব Sk ≤ ∑t=k..km 1 ≤ km, সব k-এর জন্য। — যোগ
  5. S > 1 হলে Sk ঘাতের হারে বাড়ে আর km শুধু রৈখিক হারে, তাই বড় k-তে Sk > km. বিরোধ, তাই S ≤ 1. ∎ — ঘাত বনাম রৈখিক

দুই উপপাদ্য মিলিয়ে: যেকোনো UD code-এর দৈর্ঘ্য দিয়ে একটি prefix code বানানো যায় (Kraft-এর বিপরীত)। তাই গড় দৈর্ঘ্য কমানোর প্রশ্নে prefix code-ই যথেষ্ট। এই জুটিকে Kraft–McMillan অসমতা বলে। নিচের ব্লক এর তিনটি কাজ করে: Kraft যোগ গোনে, বিপরীত নির্মাণ চালায়, আর বাজারঘরের code-এর tree ধরে একটি bit string decode করে। প্রথমবার চালালে ব্রাউজারে Python (Pyodide) নামতে কয়েক সেকেন্ড লাগে।

from fractions import Fraction
def kraft(lengths):
    return sum(Fraction(1, 2 ** l) for l in lengths)
def code_from_lengths(lengths):          # converse of Kraft: build a prefix code
    words, acc = [], Fraction(0)
    for l in sorted(lengths):
        words.append(format(int(acc * 2 ** l), f"0{l}b"))  # first l bits of acc
        acc += Fraction(1, 2 ** l)
    return words
for ls in [(1, 2, 3, 3), (2, 2, 2, 3), (1, 2, 2, 3)]:
    k = kraft(ls)
    print(ls, "Kraft sum", k, "->", code_from_lengths(ls) if k <= 1 else "impossible")
def build_tree(codes):                   # binary trie as nested dicts
    root = {}
    for sym, word in codes.items():
        node = root
        for bit in word[:-1]:
            node = node.setdefault(bit, {})
        node[word[-1]] = sym             # the leaf holds the symbol
    return root
def decode(bits, root):
    out, node = [], root
    for bit in bits:
        node = node[bit]
        if isinstance(node, str):        # reached a leaf: emit, restart at root
            out.append(node); node = root
    assert node is root, "bits ended inside a codeword"
    return "".join(out)
codes = {"P": "00", "S": "01", "D": "11", "N": "100", "R": "1010", "C": "1011"}
tree = build_tree(codes)
print(decode("1000111101011", tree))
print(decode("1111001010", tree))

আসল আউটপুট (ব্রাউজারের Python):

(1, 2, 3, 3) Kraft sum 1 -> ['0', '10', '110', '111']
(2, 2, 2, 3) Kraft sum 7/8 -> ['00', '01', '10', '110']
(1, 2, 2, 3) Kraft sum 9/8 -> impossible
NSDRD
DDPR

প্রথম লাইন stepper-এর code হুবহু দেয়। দ্বিতীয় লাইনে যোগফল 7/8 < 1: code বানানো গেল, কিন্তু tree-তে একটি ফাঁকা ডাল (111) রয়ে গেছে, আর 110-কে 11 করে ছোট করা যেত; এমন code কখনো সর্বোত্তম হয় না (Lemma ২.২)। তৃতীয় লাইনে যোগফল 9/8 > 1, তাই কোনো prefix code নেই, McMillan অনুযায়ী কোনো UD code-ও নেই। Fraction ব্যবহারের কারণ: float-এ 1/8-এর মতো সংখ্যা নিখুঁত থাকলেও যোগে ভুল জমতে পারে, অথচ Kraft-এর “ঠিক 1” আর “1-এর একটু বেশি” আলাদা করা জরুরি। শেষ দুই লাইন §৪-এর code দিয়ে decode: 1000111101011 থেকে NSDRD, আর 1111001010 থেকে DDPR. Decoder কখনো পেছনে তাকায় না; leaf-এ পৌঁছানোমাত্র চিহ্ন লেখে, এটাই prefix code-এর “instantaneous” গুণ।

৪ · গড় দৈর্ঘ্য আর Huffman-এর algorithm

চিহ্ন ai-এর সম্ভাবনা pi (এখানে গণনা/১০০) আর codeword-দৈর্ঘ্য li হলে প্রতি চিহ্নে গড় বিট

L(c) = ∑i pi li,

যা আসলে random চিহ্নের codeword-দৈর্ঘ্যের expected value (পর্ব ৪০)। একে expected codeword length বলে। সমস্যাটি তাহলে: Kraft মানে এমন ধনাত্মক পূর্ণসংখ্যা li বেছে ∑ pi li সবচেয়ে কম করা। পূর্ণসংখ্যার এই অপ্টিমাইজেশন দেখতে কঠিন, কিন্তু David Huffman ১৯৫২ সালে একটি সরল greedy উত্তর দেন।

Algorithm ৪.১ (Huffman)

প্রতিটি চিহ্ন দিয়ে একটি একক-node tree বানান, ওজন = তার সম্ভাবনা বা গণনা। যতক্ষণ একাধিক tree আছে: সবচেয়ে কম ওজনের দুটি tree T1 আর T2 তুলে নিন, একটি নতুন root-এর বাঁ আর ডান সন্তান করুন, নতুন tree-র ওজন = দুই ওজনের যোগ। শেষে যে একটি tree থাকে, তাতে বাঁয়ে 0 আর ডানে 1 লিখে codeword পড়ুন।

“সবচেয়ে কম ওজনের দুটি” বারবার তোলার কাজ একটি priority queue (min-heap) দিয়ে O(n log n) সময়ে হয়; বাস্তবায়ন আর জটিলতার বিস্তারিত DSA কোর্সের পর্ব ৫১-এ আর heap DSA কোর্সের পর্ব ২৩-এ। আমরা এখানে হাতে চালাই, তারপর প্রমাণ করি কেন এটি সেরা।

1R5 C8 N12 P15 S24 D362R5 + C8 → 133N12 + 13 → 254P15 + S24 → 39525 + D36 → 61639 + 61 → 100 (root)7বাঁয়ে 0, ডানে 1: পড়া
Heap-এর ওজন আর জমা খরচ
heap
জোড়ার যোগ
  1. শুরু। ছয়টি একক-node tree, ওজন প্রতি ১০০ লাইনে গণনা: R 5, C 8, N 12, P 15, S 24, D 36. মোট 100.
  2. প্রথম জোড়া। সবচেয়ে হালকা দুটি R (5) আর C (8). নতুন node-এর ওজন 13. Heap-এ এখন 12, 13, 15, 24, 36.
  3. দ্বিতীয় জোড়া। সবচেয়ে হালকা N (12) আর নতুন node 13: যোগ 25. খেয়াল করুন, জোড়া tree-ও সাধারণ চিহ্নের মতোই প্রতিযোগিতায় নামে।
  4. তৃতীয় জোড়া। এখন সবচেয়ে হালকা P (15) আর S (24): যোগ 39. Heap: 25, 36, 39.
  5. চতুর্থ জোড়া। 25 আর D (36): যোগ 61. Heap-এ বাকি 39 আর 61.
  6. শেষ জোড়া। 39 + 61 = 100, root. পাঁচটি জোড়ার ওজনের যোগ 13 + 25 + 39 + 61 + 100 = 238; §৫-এ উপপাদ্য ৫.৩-এর পরের সূত্র বলে এটাই ∑ (গণনা × দৈর্ঘ্য), তাই প্রতি চিহ্নে L = 238/100 = 2.38 bit.
  7. Codeword পড়া। প্রতিটি জোড়ায় প্রথম তোলা (হালকা) tree বাঁয়ে যায়, তার edge-এ 0. Root থেকে: P 00, S 01, D 11, N 100, R 1010, C 1011. যাচাই: 36·2 + 24·2 + 15·2 + 12·3 + 8·4 + 5·4 = 238. Fixed-length 3 bit হলে 300, অর্থাৎ প্রায় ২০.৭% সাশ্রয়।

নিচের ব্লক heapq দিয়ে একই কাজ করে, codeword ছাপে, আর গড় দৈর্ঘ্যকে entropy (§৬) আর fixed-length-এর সঙ্গে তুলনা করে।

import heapq, math
from fractions import Fraction
freq = {"D": 36, "S": 24, "P": 15, "N": 12, "C": 8, "R": 5}   # per 100 statuses
heap = [(w, i, s) for i, (s, w) in enumerate(freq.items())]
heapq.heapify(heap); nxt = len(heap)
while len(heap) > 1:
    a = heapq.heappop(heap)              # least frequent tree
    b = heapq.heappop(heap)              # second least
    print("merge", a[0], "+", b[0], "=", a[0] + b[0])
    heapq.heappush(heap, (a[0] + b[0], nxt, (a[2], b[2]))); nxt += 1
codes = {}
def walk(t, path):                       # left edge 0, right edge 1
    if isinstance(t, str):
        codes[t] = path
    else:
        walk(t[0], path + "0"); walk(t[1], path + "1")
walk(heap[0][2], "")
print(sorted(codes.items(), key=lambda kv: (len(kv[1]), kv[1])))
total = sum(freq.values())
L = sum(freq[s] * len(codes[s]) for s in freq) / total
H = -sum(w / total * math.log2(w / total) for w in freq.values())
print(f"Huffman L = {L:.2f} bits, entropy H = {H:.4f}, fixed = 3 bits")
print("Kraft sum:", sum(Fraction(1, 2 ** len(c)) for c in codes.values()))
words = list(codes.values())
print("prefix-free:", not any(u != v and v.startswith(u) for u in words for v in words))
print("1000 statuses:", round(L * 1000), "bits instead of", 3 * 1000)

আসল আউটপুট (ব্রাউজারের Python):

merge 5 + 8 = 13
merge 12 + 13 = 25
merge 15 + 24 = 39
merge 25 + 36 = 61
merge 39 + 61 = 100
[('P', '00'), ('S', '01'), ('D', '11'), ('N', '100'), ('R', '1010'), ('C', '1011')]
Huffman L = 2.38 bits, entropy H = 2.3100, fixed = 3 bits
Kraft sum: 1
prefix-free: True
1000 statuses: 2380 bits instead of 3000

পাঁচটি merge লাইন stepper-এর পাঁচ ধাপ হুবহু। Heap-এর প্রতিটি উপাদানে (ওজন, ক্রমিক নম্বর, tree) রাখা হয়েছে, যাতে সমান ওজনে Python tree-গুলো তুলনা করতে না যায়; ক্রমিক নম্বরই tie ভাঙে। Kraft যোগ ঠিক 1: Huffman tree সবসময় full (প্রতিটি নতুন node-এর ঠিক দুটি সন্তান)। L = 2.38 entropy 2.31-এর চেয়ে সামান্য বেশি, যা §৬-এর সীমা H ≤ L < H + 1 মানে। ১০০০টি লগ-লাইনে ৩০০০-এর বদলে ২৩৮০ bit, তবে decoder-কেও code table জানতে হয়; বাস্তব ফাইলে সেই খরচও যোগ হয় (§৭)।

একটি বার্তা encode আর decode করে পুরো চক্রটি দেখা যাক। বার্তা: N, S, D, R, D (একটি অর্ডারের নতুন থেকে ফেরত পর্যন্ত কয়েকটি লগ-লাইন)।

1N 100 · S 01 · D 11 · R 10102encode: 10001111010113decode 1, 0, 0 → N4decode 0, 1 → S5decode 1, 1 → D6decode 1, 0, 1, 0 → R7decode 1, 1 → D, শেষ
Decoder-এর অবস্থা
পড়া বিট
পথের node
বাকি বিট
আউটপুট
  1. Code-এর তালিকা। §৪-এর Huffman code: N 100, S 01, D 11, R 1010 (আর P 00, C 1011, এই বার্তায় লাগবে না)।
  2. Encode করা। Codeword পাশাপাশি: 100 · 01 · 11 · 1010 · 11 = 1000111101011, মোট 3 + 2 + 2 + 4 + 2 = 13 bit. Fixed-length 3 bit হলে 5 × 3 = 15 bit. মাঝে কোনো বিভাজক নেই।
  3. প্রথম চিহ্ন। Root (100) থেকে বিট 1: ডানে 61; বিট 0: বাঁয়ে 25; বিট 0: বাঁয়ে leaf N. N লিখে root-এ ফিরি। তিনটি বিট খরচ হলো।
  4. দ্বিতীয় চিহ্ন। বিট 0: বাঁয়ে 39; বিট 1: ডানে leaf S. আউটপুট NS.
  5. তৃতীয় চিহ্ন। বিট 1: ডানে 61; বিট 1: ডানে leaf D. আউটপুট NSD.
  6. চতুর্থ চিহ্ন। 1 → 61, 0 → 25, 1 → 13, 0 → leaf R. সবচেয়ে গভীর leaf-এ চার ধাপ লাগল। আউটপুট NSDR.
  7. শেষ। 1 → 61, 1 → leaf D. সব বিট শেষ আর decoder ঠিক root-এ, তাই বার্তা পূর্ণ: NSDRD. শেষে বিট বাকি থাকলে বা decoder মাঝপথে থাকলে বার্তাটি ভাঙা বলে ধরা পড়ত (কোডের assert সেটাই পরীক্ষা করে)।

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

Greedy পদ্ধতি সবসময় সেরা উত্তর দেয় না (DSA কোর্সের পর্ব ৫০-এ coin change-এর উদাহরণ)। তাই Huffman-এর জন্য প্রমাণ লাগবে। ধরে নিন n ≥ 2, প্রতিটি pi > 0, আর x, y দুটি সবচেয়ে কম সম্ভাবনার চিহ্ন: px ≤ py ≤ বাকি সবার সম্ভাবনা। সর্বোত্তম code অবশ্যই আছে: Lemma ২.২ অনুযায়ী সর্বোত্তম tree full, আর n leaf-এর full binary tree সসীম সংখ্যক।

Lemma ৫.১ (বিরল দুটি সবচেয়ে গভীরে ভাই)

এমন একটি সর্বোত্তম prefix code আছে, যার tree-তে x আর y sibling (একই parent-এর দুই সন্তান), আর দুজনেই সবচেয়ে বেশি গভীরতায়।

  1. যেকোনো সর্বোত্তম tree T নিন; সবচেয়ে বেশি গভীরতা d. গভীরতা d-এর একটি leaf a নিন। T full বলে a-এর parent-এর আরেকটি সন্তান b আছে, আর b-ও leaf, কারণ b-এর নিচে কিছু থাকলে গভীরতা d ছাড়াত। — Lemma ২.২
  2. নাম এমনভাবে দিন যাতে pa ≤ pb. x আর y সবচেয়ে বিরল, তাই px ≤ pa আর py ≤ pb. — x, y-এর সংজ্ঞা
  3. T-তে x আর a-এর জায়গা বদল করুন। গড় দৈর্ঘ্যের পরিবর্তন = pxd + palx − pxlx − pad = −(pa − px)(d − lx) ≤ 0, কারণ pa ≥ px আর d ≥ lx. — exchange argument
  4. একইভাবে y আর b বদল করুন; পরিবর্তন −(pb − py)(d − ly) ≤ 0. (x = b বা y = a হলে বিশেষ ক্ষেত্র: তখন একটি বদলেই x, y ওই দুই জায়গায় চলে আসে, বা কোনো বদলই লাগে না।) — একই হিসাব
  5. নতুন tree-র গড় দৈর্ঘ্য T-এর চেয়ে বেশি নয়, আর T সর্বোত্তম, তাই নতুনটিও সর্বোত্তম; তাতে x, y গভীরতা d-তে ভাই। ∎
Lemma ৫.২ (জোড়া দিলে সমস্যা ছোট হয়)

A′ হলো A থেকে x আর y বাদ দিয়ে একটি নতুন চিহ্ন z যোগ করা alphabet, pz = px + py. A′-এর যেকোনো prefix code tree T′-এ leaf z-কে একটি internal node বানিয়ে তার দুই সন্তান x আর y বসালে A-এর একটি tree T পাওয়া যায়, আর L(T) = L(T′) + px + py. উল্টোভাবে, A-এর যে tree-তে x, y ভাই, তাদের parent-কে leaf z বানালে উল্টো সম্পর্কটি পাওয়া যায়।

প্রমাণ: z-এর গভীরতা lz হলে x আর y-এর গভীরতা lz + 1, বাকি সবার অপরিবর্তিত। তাই L(T) − L(T′) = px(lz + 1) + py(lz + 1) − (px + py)lz = px + py. ∎

উপপাদ্য ৫.৩ (Huffman-এর সর্বোত্তমতা)

সব ধনাত্মক সম্ভাবনার যেকোনো alphabet-এ (n ≥ 2) Huffman-এর code সব prefix code-এর মধ্যে, আর তাই (McMillan অনুযায়ী) সব UD code-এর মধ্যে, সবচেয়ে কম গড় দৈর্ঘ্যের।

প্রমাণ n-এর ওপর induction দিয়ে।

  1. ভিত্তি n = 2: প্রতিটি codeword অন্তত 1 bit, তাই L ≥ p1 + p2 = 1; Huffman দেয় 0 আর 1, L = 1. — base case
  2. Hypothesis: n − 1টি চিহ্নের প্রতিটি alphabet-এ Huffman সর্বোত্তম। — inductive hypothesis
  3. n চিহ্নে Huffman প্রথমে x আর y জোড়া দেয়, তারপর বাকি কাজটি ঠিক A′-এর ওপর Huffman চালানোর মতো, যেখানে z-এর ওজন px + py. সেই tree H′; শেষে z খুলে x, y বসালে Huffman-এর tree H. Lemma ৫.২: L(H) = L(H′) + px + py. — algorithm-এর গঠন
  4. Lemma ৫.১ থেকে একটি সর্বোত্তম tree T, যাতে x, y ভাই। তাদের parent-কে leaf z বানালে A′-এর একটি tree T′, আর L(T′) = L(T) − px − py. — Lemma ৫.১ আর ৫.২
  5. Hypothesis অনুযায়ী L(H′) ≤ L(T′). তাই L(H) = L(H′) + px + py ≤ L(T′) + px + py = L(T). T সর্বোত্তম, তাই H-ও সর্বোত্তম। ∎ — তুলনা

প্রমাণ থেকে একটি দরকারি সূত্রও আসে। Lemma ৫.২ বারবার প্রয়োগ করলে, প্রতিটি জোড়ায় নতুন node-এর ওজন একবার করে L-এ যোগ হয়:

Huffman-এর মোট খরচ ∑ wi li = সব জোড়ায় তৈরি node-এর ওজনের যোগ।

বাজারঘরে 13 + 25 + 39 + 61 + 100 = 238, যা stepper-এ দেখেছেন। হাতে অঙ্ক করার সময় codeword না পড়েই L বের করার এটি দ্রুত পথ।

Tie আর একাধিক Huffman tree

দুটি ওজন সমান হলে কোনটি আগে তুলবেন, algorithm তা বলে না। ওজন a 1, b 1, c 2, d 2 (মোট 6) নিন। প্রথমে a + b = 2. তখন তিনটি 2: c, d আর নতুন node. যদি c আর d জোড়া দেন, সব codeword-এর দৈর্ঘ্য 2: দৈর্ঘ্য-ভেক্টর (2, 2, 2, 2). যদি নতুন node আর c জোড়া দেন, তবে দৈর্ঘ্য a 3, b 3, c 2, d 1. দুটি tree আলাদা, দৈর্ঘ্যও আলাদা, এমনকি height-ও (2 বনাম 3), অথচ খরচ একই: 1·2 + 1·2 + 2·2 + 2·2 = 12 আর 1·3 + 1·3 + 2·2 + 2·1 = 12. এটি কাকতালীয় নয়: উপপাদ্য ৫.৩-এর প্রমাণে কোনো tie-ভাঙার নিয়ম ব্যবহার হয়নি, তাই যেকোনো পছন্দেই ফল সর্বোত্তম, আর সব সর্বোত্তমের খরচ সমান। তাই Huffman tree “অনন্য” নয়, কিন্তু তার গড় দৈর্ঘ্য অনন্য। বাস্তবের ফরম্যাটে encoder আর decoder-কে একই tree মানতে হয়, তাই canonical Huffman code শুধু দৈর্ঘ্য থেকে একটি নির্দিষ্ট নিয়মে codeword বানায়, ঠিক Kraft-এর নির্মাণের মতো।

প্রমাণে বিশ্বাস আছে, তবু যাচাই করা যাক। নিচের ব্লক ১৫০টি এলোমেলো ছোট alphabet-এ (২ থেকে ৬টি চিহ্ন) Kraft মানে এমন প্রতিটি দৈর্ঘ্য-ভেক্টর পরীক্ষা করে সবচেয়ে কম খরচ বের করে, আর Huffman-এর খরচের সঙ্গে মেলায়। দৈর্ঘ্য 1 থেকে n − 1 পর্যন্ত দেখাই যথেষ্ট, কারণ n leaf-এর full tree-র কোনো leaf n − 1-এর বেশি গভীর নয়।

import heapq, math, random
from itertools import product
def huffman_cost(w):                     # sum of all merged weights = sum w*l
    h = list(w); heapq.heapify(h); cost = 0
    while len(h) > 1:
        a, b = heapq.heappop(h), heapq.heappop(h)
        cost += a + b; heapq.heappush(h, a + b)
    return cost
def best_by_kraft(w):                    # try every length vector, Kraft <= 1
    n = len(w); m = n - 1; best = None
    for ls in product(range(1, n), repeat=n):
        if sum(2 ** (m - l) for l in ls) <= 2 ** m:
            c = sum(x * l for x, l in zip(w, ls))
            if best is None or c < best:
                best = c
    return best
random.seed(68); gap = 0; trials = 150
for t in range(trials):
    w = [random.randint(1, 20) for _ in range(random.randint(2, 6))]
    cost = huffman_cost(w)
    assert cost == best_by_kraft(w)      # Huffman is optimal
    L = cost / sum(w)
    H = -sum(x / sum(w) * math.log2(x / sum(w)) for x in w)
    assert H <= L < H + 1                # source coding bounds
    gap = max(gap, L - H)
print(trials, "random alphabets: Huffman = brute-force optimum")
print("H <= L < H + 1 held; largest L - H:", round(gap, 4))
print("weights 1,1,2,2: Huffman cost", huffman_cost([1, 1, 2, 2]))
print("lengths (2,2,2,2) cost", 1*2 + 1*2 + 2*2 + 2*2,
      "| lengths (3,3,2,1) cost", 1*3 + 1*3 + 2*2 + 2*1)

আসল আউটপুট (ব্রাউজারের Python):

150 random alphabets: Huffman = brute-force optimum
H <= L < H + 1 held; largest L - H: 0.7136
weights 1,1,2,2: Huffman cost 12
lengths (2,2,2,2) cost 12 | lengths (3,3,2,1) cost 12

huffman_cost কোনো codeword বানায় না, শুধু জোড়ার ওজন যোগ করে; উপপাদ্য ৫.৩-এর পরের সূত্রটি এখানে কাজ করছে। best_by_kraft ছয় চিহ্নে 56 = 15625টি দৈর্ঘ্য-ভেক্টর দেখে, আর Kraft-এর বিপরীত বলে প্রতিটি টিকে থাকা ভেক্টরের একটি prefix code আছে, তাই এটি সত্যিই সব prefix code-এর সেরা। Kraft পরীক্ষাটি পূর্ণসংখ্যায় (2m দিয়ে গুণ করে), তাই float-এর ভুল নেই। ১৫০টি ক্ষেত্রেই দুই সংখ্যা সমান, আর H ≤ L < H + 1 সব ক্ষেত্রে টিকেছে; সবচেয়ে বড় ফারাক 0.7136 bit. শেষ দুই লাইন tie-এর উদাহরণ: দুটি আলাদা দৈর্ঘ্য-ভেক্টর, খরচ দুটিতেই 12. Brute force চিহ্ন বাড়লে দ্রুত অচল হয় ((n − 1)nটি ভেক্টর), Huffman-এর লাগে n − 1টি জোড়া।

৬ · Entropy — কোনো code যার নিচে নামতে পারে না

Huffman সর্বোত্তম, কিন্তু সেই সর্বোত্তম মান কত? একটি সরল সূত্র প্রায় ঠিক উত্তর দেয়। সম্ভাবনা p-এর একটি ঘটনা ঘটলে তার information content −log2 p bit: ঘটনা যত বিরল, তত বেশি “খবর”। গড় নিলে

H(p) = −∑i pi log2 pi

যাকে Shannon-এর entropy বলে (log ভিত্তি 2, তাই একক bit ধরা হয়)। বাজারঘরে D-এর −log2 0.36 ≈ 1.474 আর R-এর −log2 0.05 ≈ 4.322, আর H ≈ 2.3100. Entropy-র ধারণা আর অনুভব AI Foundations কোর্সের পর্ব ৩৮-এ আছে; এখানে আমরা code-এর সঙ্গে তার সম্পর্ক প্রমাণ করি।

Lemma ৬.১ (Gibbs-এর অসমতা)

p1, …, pn ধনাত্মক আর যোগফল 1; q1, …, qn ধনাত্মক আর ∑ qi ≤ 1. তাহলে −∑ pi log2 pi ≤ −∑ pi log2 qi, আর সমতা ঠিক তখন, যখন প্রতিটি i-তে qi = pi.

  1. সব x > 0-এর জন্য ln x ≤ x − 1, সমতা শুধু x = 1-এ (y = ln x বক্ররেখা x = 1-এ তার স্পর্শক y = x − 1-এর নিচে থাকে)। — ln-এর concavity
  2. x = qi/pi বসিয়ে pi দিয়ে গুণ করে যোগ করুন: ∑ pi ln(qi/pi) ≤ ∑ (qi − pi) = ∑ qi − 1 ≤ 0. — যোগ
  3. বাঁ দিক = ∑ pi ln qi − ∑ pi ln pi; ln 2 দিয়ে ভাগ করে সাজালে দাবিটি আসে। সমতার জন্য ধাপ ১-এ প্রতিটি qi/pi = 1 লাগে। ∎
উপপাদ্য ৬.২ (source coding-এর সীমা, অক্ষর-ধরে code)

সব ধনাত্মক সম্ভাবনার জন্য: (ক) যেকোনো uniquely decodable code-এ L ≥ H; সমতা ঠিক তখন, যখন প্রতিটি pi = 2−li. (খ) এমন একটি prefix code আছে যাতে L < H + 1; তাই Huffman-এর LHuff < H + 1.

  1. (ক) qi = 2−li নিন। McMillan অনুযায়ী ∑ qi ≤ 1, তাই Gibbs থেকে H ≤ −∑ pi log2 2−li = ∑ pi li = L. সমতা ⇔ pi = qi. — Lemma ৬.১
  2. (খ) li = ⌈−log2 pi⌉ নিন (Shannon-এর দৈর্ঘ্য)। তাহলে li ≥ −log2 pi, তাই 2−li ≤ pi, আর ∑ 2−li ≤ ∑ pi = 1. Kraft-এর বিপরীত এই দৈর্ঘ্যের prefix code দেয়। — উপপাদ্য ৩.১
  3. ⌈t⌉ < t + 1, তাই L = ∑ pi li < ∑ pi(−log2 pi + 1) = H + 1. Huffman সর্বোত্তম, তাই LHuff ≤ এই L < H + 1. ∎ — উপপাদ্য ৫.৩

বাজারঘরে Shannon-এর দৈর্ঘ্য D 2, S 3, P 3, N 4, C 4, R 5, আর তার L = 2.94: সীমার ভেতরে, কিন্তু Huffman-এর 2.38-এর চেয়ে অনেক খারাপ। তাই Shannon-এর দৈর্ঘ্য প্রমাণের হাতিয়ার, সেরা code নয়। সমতার শর্তটি দেখার মতো: সম্ভাবনা (1/2, 1/4, 1/8, 1/8) হলে Huffman দেয় 0, 10, 110, 111, আর L = H = 1.75 ঠিক সমান। সম্ভাবনা 2-এর ঘাত না হলে কিছু ফাঁক থাকবেই।

সতর্কতা: “H ≤ L” শুধু uniquely decodable code-এর জন্য। Codeword আলাদা কিন্তু UD নয় এমন code-এ L entropy-র নিচে নামতে পারে (নিচের অনুশীলন ৪), কিন্তু তখন বার্তা ফেরত পাওয়ার নিশ্চয়তা থাকে না, অর্থাৎ সেটি আসলে সংকোচন নয়।

৭ · বাস্তবে Huffman — block, সীমা আর ফরম্যাট

L < H + 1-এর “+1” বিরল-ঘটনা-ভরা উৎসে বড় সমস্যা। ধরুন বাজারঘরের একটি লগ-কলামে 90% লাইনে x (“ঠিক আছে”) আর 10% লাইনে y (“সমস্যা”)। H ≈ 0.4690 bit, কিন্তু অক্ষর-ধরে যেকোনো code-এ প্রতিটি চিহ্ন অন্তত 1 bit, তাই L = 1, entropy-র দ্বিগুণের বেশি। প্রতিকার block coding: দুটি চিহ্ন একসঙ্গে একটি নতুন “চিহ্ন” ধরুন (xx, xy, yx, yy; সম্ভাবনা 0.81, 0.09, 0.09, 0.01) আর তার ওপর Huffman চালান। scratch-এ হিসাব: জোড়ায় প্রতি মূল চিহ্নে 0.645 bit, তিনটির block-এ 0.5327 bit. স্বাধীন চিহ্নের উৎসে k-এর block-এ উপপাদ্য ৬.২ দেয় kH ≤ Lk < kH + 1, তাই প্রতি চিহ্নে

H ≤ Lk/k < H + 1/k,

আর k বাড়লে গড় দৈর্ঘ্য entropy-র যত কাছে চান তত কাছে আসে। এটাই Claude Shannon-এর ১৯৪৮ সালের “A Mathematical Theory of Communication” প্রবন্ধের source coding উপপাদ্যের সরল রূপ: entropy হলো lossless সংকোচনের মৌলিক সীমা। দাম হলো code table-এর আকার, যা k-এর সঙ্গে ঘাতের হারে বাড়ে (দুই চিহ্নের উৎসে 2kটি সম্ভাব্য block থাকে)। এই কারণে arithmetic coding আর ANS-এর মতো পদ্ধতি পুরো বার্তাকে একসঙ্গে code করে, বিশাল table ছাড়াই ভগ্নাংশ-বিটের দক্ষতা পায়।

আরেকটি সীমা গণনার। পর্ব ৩২-এ প্রমাণ করেছেন: কোনো lossless compression প্রতিটি ফাইল ছোট করতে পারে না। Huffman এর ব্যতিক্রম নয়: সব চিহ্ন সমান সম্ভাবনার হলে (যেমন ইতিমধ্যে সংকুচিত বা এলোমেলো ডেটা) কোনো লাভ নেই, আর code table পাঠানোর খরচে ফাইল বড়ও হতে পারে। Huffman লাভ দেয় শুধু তখন, যখন চিহ্নের সম্ভাবনা অসমান।

বাস্তবের ফরম্যাটে Huffman প্রায়ই অন্য কৌশলের সঙ্গে বসে। ব্যাপকভাবে নথিভুক্ত উদাহরণ DEFLATE (RFC 1951, ১৯৯৬), যা zip, gzip আর PNG-এর সংকোচন: আগে LZ77 দিয়ে পুনরাবৃত্ত অংশকে “কত পেছনে, কত লম্বা” দিয়ে বদলায়, তারপর ফলাফলকে canonical Huffman code-এ লেখে, আর ফাইলে শুধু codeword-দৈর্ঘ্য পাঠায়। ওই নির্দেশিকায় codeword-দৈর্ঘ্য সর্বোচ্চ 15 bit-এ সীমিত, তাই encoder-কে কখনো “দৈর্ঘ্য-সীমিত” code বানাতে হয়, যা খাঁটি Huffman-এর চেয়ে সামান্য খারাপ হতে পারে। প্রচলিত JPEG ছবির baseline রূপেও শেষ ধাপে Huffman coding ব্যবহৃত হয় বলে মানদণ্ডে বর্ণিত (arithmetic coding সেখানে ঐচ্ছিক বিকল্প)। বিস্তারিত বাস্তবায়ন DSA কোর্সের পর্ব ৫১-এ।

৮ · পরীক্ষার অঙ্ক আর একটি ভুল প্রমাণ

অঙ্ক ১: হাতে Huffman

পাঁচটি চিহ্নের গণনা a 10, b 15, c 30, d 16, e 29, মোট 100. Huffman code, গড় দৈর্ঘ্য, আর fixed-length-এর তুলনায় সাশ্রয় বের করুন।

  1. জোড়া: a 10 + b 15 = 25; d 16 + 25 = 41; e 29 + c 30 = 59; 41 + 59 = 100.
  2. দৈর্ঘ্য: d, e, c → 2; a, b → 3. একটি code: d 00, a 010, b 011, e 10, c 11 (Kraft: 3/4 + 2/8 = 1).
  3. খরচ: জোড়ার যোগ 25 + 41 + 59 + 100 = 225, তাই L = 2.25 bit. Fixed-length-এ ⌈log2 5⌉ = 3 bit, সাশ্রয় 25%. Entropy H ≈ 2.2047, সীমা মেলে (scratch-এ heapq দিয়ে মেলানো)।

অঙ্ক ২: Kraft দিয়ে সম্ভব-অসম্ভব

(ক) দৈর্ঘ্য 1, 2, 3, 3, 3-এর binary prefix code আছে কি? (খ) 1, 3, 3, 3, 3?

  1. (ক) 1/2 + 1/4 + 3/8 = 9/8 > 1, তাই নেই; McMillan অনুযায়ী এমন UD code-ও নেই।
  2. (খ) 1/2 + 4/8 = 1, তাই আছে: Kraft-এর নির্মাণে 0, 100, 101, 110, 111 (scratch-এ code_from_lengths দিয়ে মেলানো)।

অঙ্ক ৩: সমতার ক্ষেত্র

সম্ভাবনা 1/2, 1/4, 1/8, 1/16, 1/32, 1/32. Huffman-এর দৈর্ঘ্য 1, 2, 3, 4, 5, 5, আর L = 1/2 + 2/4 + 3/8 + 4/16 + 5/32 + 5/32 = 1.9375 = H, কারণ প্রতিটি সম্ভাবনা 2-এর ঋণাত্মক ঘাত (উপপাদ্য ৬.২-এর সমতা)।

প্রমাণের ভুল ধরুন: “Kraft মানলেই prefix code”

একজন লিখলেন: “Code {0, 01}-এর দৈর্ঘ্য 1 আর 2, Kraft যোগ 1/2 + 1/4 = 3/4 ≤ 1. Kraft-এর উপপাদ্য অনুযায়ী তাই এটি একটি prefix code.”

ভুল কোথায়: Kraft-এর বিপরীত বলে, এই দৈর্ঘ্যের কোনো একটি prefix code আছে (যেমন {0, 10}); প্রদত্ত নির্দিষ্ট code-টি prefix কি না, সে বিষয়ে কিছু বলে না। এখানে 0 হলো 01-এর prefix, তাই {0, 01} prefix code নয় (যদিও UD, কারণ কোনো codeword অন্যটির শেষাংশ নয়)। অসমতা দৈর্ঘ্যের শর্ত, codeword-এর নয়; “অস্তিত্ব” আর “এটিই সেটি” গুলিয়ে ফেলা একটি পরিচিত যুক্তির ভুল।

আরও গভীরে: সর্বোচ্চ দৈর্ঘ্যের সীমাসহ সর্বোত্তম prefix code-এর জন্য Larmore আর Hirschberg-এর ১৯৯০ সালের package-merge algorithm পরিচিত বলে উল্লেখ করা হয়। Codeword-কে চিহ্নের বর্ণানুক্রম মানতে হলে (alphabetic code) Hu–Tucker-এর পদ্ধতি লাগে, আর অক্ষরের “দাম” অসমান হলে (যেমন Morse-এর ডট আর ড্যাশের সময় আলাদা) সর্বোত্তম code খোঁজা অনেক কঠিন সমস্যা। আর অক্ষরের সম্ভাবনা আগে না জানলে adaptive Huffman বা universal code (যেমন Lempel–Ziv-এর পরিবার) উৎসের পরিসংখ্যান চলতে চলতে শিখে নেয়। Error ধরা আর ঠিক করার জন্য code-এ ইচ্ছা করে বাড়তি বিট বসানোর উল্টো প্রশ্নটি পর্ব ৭২ “Error-correcting code — parity, Hamming code আর Hamming distance”-এ আসবে।

পরীক্ষা ও ইন্টারভিউতে: বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন প্রায়ই আসে:

  • Prefix code কেন দরকার? বিভাজক ছাড়াই বিটের স্রোত একভাবে আর সঙ্গে সঙ্গে decode হয়; codeword-গুলো binary tree-র leaf.
  • দৈর্ঘ্য (1, 2, 2, 3)-এর prefix code আছে কি? না: Kraft যোগ 9/8 > 1.
  • Huffman-এর গড় দৈর্ঘ্য দ্রুত বের করার উপায়? সব জোড়ায় তৈরি node-এর ওজন যোগ করে মোট দিয়ে ভাগ।
  • Huffman কেন সর্বোত্তম? বিরল দুটি সবচেয়ে গভীরে ভাই হতে পারে (exchange), আর জোড়া দিলে খরচ ঠিক px + py বাড়ে; n-এর ওপর induction.
  • Entropy-র সঙ্গে সম্পর্ক? UD code-এ H ≤ L, Huffman-এ L < H + 1; সমতা শুধু 2-এর ঘাতের সম্ভাবনায়।

৯ · AI-এর যুগে — Huffman-এর হিসাব আর প্রমাণ যাচাই

AI সহকারী Huffman-এর ধাপ সাধারণত ঠিক বর্ণনা করে, কিন্তু হাতের হিসাবে, শর্তে আর প্রমাণের দিকনির্ণয়ে ভুল করে। কয়েকটি নিয়মিত ভুল আর ধরার উপায়:

AI-এর ভুলকীভাবে ধরবেন
জোড়ার পর নতুন node-কে প্রতিযোগিতা থেকে বাদ দিয়ে শুধু মূল চিহ্ন জোড়া দেওয়াপ্রতিটি ধাপে heap-এর পুরো তালিকা লিখে মেলান; জোড়ার যোগ = ∑ wi li কি না দেখুন
Kraft-কে “এই code prefix” বলে প্রয়োগ, বা UD-এর বদলে যেকোনো code-এ H ≤ L দাবি{0, 01} আর {0, 1, 00}-এর মতো ছোট পাল্টা-উদাহরণ পরীক্ষা করুন
“বিরল দুটি প্রতিটি সর্বোত্তম tree-তে ভাই” বলাTie-এর উদাহরণ (1, 1, 2, 2) দিয়ে সব সর্বোত্তম tree গুনে দেখুন
Entropy-তে log-এর ভিত্তি গুলিয়ে ফেলা (ln দিয়ে bit বলা)সমান সম্ভাবনার দুই চিহ্নে H = 1 bit আসে কি না দেখুন
গড় দৈর্ঘ্যে ভগ্নাংশের যোগে ভুল, বা সাশ্রয়ের শতাংশ ভুল ভিত্তিতেFraction দিয়ে L গুনুন, fixed-length-এর সঙ্গে আলাদা করে তুলনা করুন

AI-এর কাছে Huffman বা code-এর প্রমাণ চাইলে এমন কাঠামো দিন, যাতে প্রতিটি ধাপ আলাদা করে যাচাই করা যায়:

নিচের code-সংক্রান্ত প্রশ্নের উত্তর ধাপে ধাপে দাও: প্রশ্ন: [যেমন "গণনা 7, 9, 12, 20, 22, 30-এর Huffman code আর গড় দৈর্ঘ্য"] ১. প্রতিটি জোড়ার ধাপে heap-এর পুরো তালিকা লেখো, কোন দুটি তুলছ আর কেন। ২. শেষে codeword-গুলো, প্রতিটির দৈর্ঘ্য, Kraft যোগ (ভগ্নাংশে) আর ∑ w·l লেখো; জোড়ার ওজনের যোগের সঙ্গে মিলিয়ে দেখাও। ৩. কোনো উপপাদ্য (Kraft, McMillan, Gibbs, Shannon) ব্যবহার করলে তার সব শর্ত লেখো: prefix না UD, সম্ভাবনা ধনাত্মক কি না, log-এর ভিত্তি কত। ৪. "যদি এবং কেবল যদি" বা অস্তিত্বের দাবিতে কোন দিক প্রমাণ করছ তা আলাদা করে বলো। ৫. ৬টি চিহ্ন পর্যন্ত সব দৈর্ঘ্য-ভেক্টর পরীক্ষা করে উত্তর যাচাইয়ের একটি Python কোড দাও (শুধু heapq, itertools, fractions), যাতে আমি নিজে চালাতে পারি। ৬. কোনো নাম, সাল বা মানদণ্ডের তথ্য নিয়ে নিশ্চিত না হলে তা স্পষ্ট বলো।

AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

Huffman-এর উত্তর যাচাই সহজ, কারণ তিনটি স্বাধীন সংখ্যা মিলতে হয়: ∑ w·l, জোড়ার ওজনের যোগ, আর brute force-এর সর্বনিম্ন। তিনটি না মিললে উত্তরে ভুল আছে।

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

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

প্র ০১একজন ছাত্রের term paper কীভাবে তাঁর শিক্ষকের পদ্ধতিকে ছাড়িয়ে গেল? Shannon–Fano থেকে Huffman-এর গল্প

১৯৪০-এর দশকের শেষে Claude Shannon আর MIT-এর Robert Fano স্বাধীনভাবে একটি “ওপর থেকে নিচে” পদ্ধতি দেন, যা আজ Shannon–Fano coding নামে পরিচিত: চিহ্নগুলোকে সম্ভাবনা অনুযায়ী সাজিয়ে এমন দুই দলে ভাগ করুন যাদের মোট সম্ভাবনা যত কাছাকাছি সম্ভব; এক দলকে 0, অন্যটিকে 1 দিন; প্রতিটি দলে একই কাজ চালিয়ে যান। পদ্ধতিটি সাধারণত ভালো ফল দেয়, কিন্তু সবসময় সর্বোত্তম নয়; কিছু সম্ভাবনায় এটি Huffman-এর চেয়ে বেশি গড় দৈর্ঘ্য দেয়।

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

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

গল্পটির শিক্ষা গবেষণার পদ্ধতি নিয়েও: একটি সমস্যা এক দিক থেকে আটকে গেলে তার “উল্টো” রূপ ভাবা প্রায়ই পথ খোলে। Huffman পরে কম্পিউটার বিজ্ঞানের শিক্ষক হিসেবে কাজ করেন; তাঁর code আজও পাঠ্যবইয়ের প্রথম greedy প্রমাণগুলোর একটি।

মূল উপলব্ধি: ওপর থেকে ভাগের Shannon–Fano ভালো কিন্তু সবসময় সর্বোত্তম নয়; নিচ থেকে জোড়ার Huffman প্রতিটি ধাপে নিরাপদ পছন্দ নেয়, আর সেই নিরাপত্তার প্রমাণই তার সর্বোত্তমতা।

প্র ০২Kraft আর McMillan-এর জুটি কেন code-নকশায় এত বড় স্বস্তি? “prefix code-ই যথেষ্ট” কথাটির মানে কী?

Leon Kraft তাঁর ১৯৪৯ সালের MIT master’s thesis-এ prefix code-এর অসমতাটি দেন বলে উল্লেখ করা হয়; Brockway McMillan ১৯৫৬ সালে দেখান যে একই অসমতা যেকোনো uniquely decodable code-এর জন্যও সত্য। প্রথম দেখায় দ্বিতীয় ফলটি অবাক করে। UD code অনেক বড় শ্রেণি: {0, 01, 11}-এর মতো code, যা সামনে থেকে পড়তে গেলে অপেক্ষা করতে হয়, তাও UD. মনে হতে পারে, এই বাড়তি স্বাধীনতায় হয়তো আরও ছোট codeword পাওয়া যায়।

McMillan বলে, না। §৩-এর প্রমাণের মূল কৌশলটি দেখুন: Sk গুনলে k-চিহ্নের বার্তার encoding-এর দৈর্ঘ্য-বণ্টন আসে, আর UD হওয়া মানে এগুলো দৈর্ঘ্য t-এর 2tটি string-এ one-to-one বসে। S > 1 হলে বার্তার সংখ্যা ঘাতের হারে বাড়ত আর জায়গা ফুরিয়ে যেত। এটি pigeonhole-এর একটি সূক্ষ্ম, “অসীম দূরে তাকানো” রূপ: একটি k-এ নয়, সব k-এর সীমা মিলিয়ে।

ব্যবহারিক ফল বিশাল। যেকোনো UD code-এর দৈর্ঘ্য নিয়ে Kraft-এর বিপরীত দিয়ে একই দৈর্ঘ্যের prefix code বানানো যায়, তাই গড় দৈর্ঘ্যে কিছুই হারায় না, অথচ decoding অনেক সহজ: অপেক্ষা নেই, লুকানো অবস্থা নেই, শুধু tree ধরে হাঁটা। এ কারণেই compression-এর প্রায় সব বাস্তব code prefix code, আর তত্ত্বে “সর্বোত্তম UD code” আর “সর্বোত্তম prefix code” একই প্রশ্ন।

আরেকটি দিক: Kraft-এর অসমতা code-নকশাকে একটি ক্রমাগত সমস্যায় নিয়ে আসে। দৈর্ঘ্যের বদলে qi = 2−li-কে একটি “সম্ভাবনা” হিসেবে ভাবা যায়, আর তখন code মানে একটি অনুমিত distribution. Gibbs-এর অসমতা বলে, সেরা অনুমান আসল distribution নিজেই; এভাবেই entropy সীমা আসে, আর machine learning-এর cross-entropy loss-এর সঙ্গে code-দৈর্ঘ্যের সম্পর্কও এখান থেকে।

মূল উপলব্ধি: McMillan দেখায় UD code দৈর্ঘ্যে prefix code-এর চেয়ে কিছু বেশি দিতে পারে না, আর Kraft-এর বিপরীত সেই দৈর্ঘ্যে prefix code বানায়; তাই সহজে decode করা যায় এমন prefix code-এ কাজ করা কোনো ছাড় নয়।

প্র ০৩Shannon ১৯৪৮ সালে entropy-কে সংকোচনের সীমা বললেন; এই সীমা কতটা কঠোর, আর কোথায় তা আরও সূক্ষ্ম?

Claude Shannon-এর ১৯৪৮ সালের দুই কিস্তির প্রবন্ধ “A Mathematical Theory of Communication” (Bell System Technical Journal) তথ্যকে মাপার একটি গাণিতিক কাঠামো দেয়। Source coding উপপাদ্যের মূল কথা: একটি স্বাধীন, একই বণ্টনের উৎস থেকে আসা চিহ্নকে lossless-ভাবে লিখতে গড়ে প্রতি চিহ্নে H bit-এর কম লাগে না, আর বড় block নিলে H-এর যত কাছে চান যাওয়া যায়। এই পর্বের উপপাদ্য ৬.২ আর block-এর হিসাব ঠিক এই ফলের অক্ষর-ধরে রূপ।

সীমাটি কঠোর অর্থে: H ≤ L-এর প্রমাণ কোনো বিশেষ algorithm-এর ওপর নির্ভর করে না, যেকোনো UD code-এর জন্য সত্য। কিন্তু তিনটি সূক্ষ্মতা আছে। প্রথমত, H হলো অনুমিত মডেলের entropy. বাংলা লেখায় অক্ষরগুলো স্বাধীন নয়; আগের অক্ষর জানলে পরেরটি সম্পর্কে অনিশ্চয়তা কমে। তাই অক্ষর-ধরে মডেলের entropy-র চেয়ে ভালো সংকোচন সম্ভব, যদি মডেল প্রসঙ্গ ব্যবহার করে। আধুনিক compressor-এর বেশিরভাগ বুদ্ধি আসলে মডেলিং-এ, code-এ নয়।

দ্বিতীয়ত, entropy গড়ের সীমা, প্রতিটি ফাইলের নয়। পর্ব ৩২-এর pigeonhole যুক্তি বলে কিছু ফাইল বড় হবেই; source coding উপপাদ্য বলে গড়ে কতটা ছোট করা যায়। তৃতীয়ত, একটি নির্দিষ্ট string-এর “সত্যিকারের” তথ্য-পরিমাণের প্রশ্ন আলাদা: Kolmogorov complexity সেই মাপ, কিন্তু তা গণনাযোগ্য নয়, তাই বাস্তবে আমরা সম্ভাবনার মডেল নিয়েই কাজ করি।

একটি সুন্দর সংযোগ: ভাষা-মডেল যত ভালো ভবিষ্যদ্বাণী করে, তার cross-entropy তত কম, আর arithmetic coding-এর সঙ্গে জুড়লে সেই মডেল দিয়ে সেই পরিমাণ bit-এ লেখা সংকুচিত করা যায়। তাই “ভালো ভবিষ্যদ্বাণী” আর “ভালো সংকোচন” গাণিতিকভাবে একই কাজের দুই রূপ।

মূল উপলব্ধি: Entropy যেকোনো UD code-এর গড় দৈর্ঘ্যের নিম্নসীমা, আর block বা arithmetic coding সেই সীমার কাছে পৌঁছায়; কিন্তু সীমাটি মডেলের, তাই উৎসকে ভালো বোঝাই সংকোচনের আসল চাবি।

প্র ০৪Huffman সর্বোত্তম হলে আধুনিক কিছু compressor কেন arithmetic coding বা ANS ব্যবহার করে?

উপপাদ্য ৫.৩-এর সর্বোত্তমতার একটি শর্ত আছে, যা প্রায়ই চোখ এড়ায়: Huffman সেরা প্রতিটি চিহ্নকে পূর্ণসংখ্যা-সংখ্যক বিটের আলাদা codeword দেওয়া code-এর মধ্যে। এই সীমাবদ্ধতার দাম কত, §৭-এর উদাহরণে দেখেছেন: 90%/10% উৎসে H ≈ 0.469 bit, অথচ অক্ষর-ধরে যেকোনো code অন্তত 1 bit দেয়। খুব সম্ভাব্য একটি চিহ্নের আদর্শ দৈর্ঘ্য −log2 0.9 ≈ 0.152 bit, কিন্তু codeword 0.152 bit লম্বা হতে পারে না।

Block coding এই ফাঁক কমায়, কিন্তু table বিস্ফোরিত হয়। Arithmetic coding অন্য পথে যায়: পুরো বার্তাকে [0, 1) ব্যবধির একটি ক্রমশ সরু হতে থাকা উপ-ব্যবধি দিয়ে চিহ্নিত করে, প্রতিটি চিহ্নে ব্যবধি তার সম্ভাবনার অনুপাতে ছোট হয়। §৩-এর Kraft-নির্মাণের ব্যবধির ছবিটিই এখানে, শুধু পুরো বার্তার স্তরে। ফলে প্রতি চিহ্নে খরচ প্রায় −log2 p, ভগ্নাংশসহ, আর মোট বাড়তি খরচ পুরো বার্তায় মাত্র কয়েক bit.

Arithmetic coding-এর ধারণা ১৯৭০-এর দশকে Rissanen, Pasco প্রমুখের কাজে বাস্তব রূপ পায় বলে উল্লেখ করা হয়; কিন্তু দীর্ঘদিন পেটেন্ট আর গতির কারণে অনেক ফরম্যাট Huffman-এই থেকে যায় বলে বর্ণনা পাওয়া যায়। পরে Jarek Duda-র asymmetric numeral systems (ANS, প্রায় ২০০৯–২০১৪ সালের কাজ) arithmetic coding-এর মতো দক্ষতা আর Huffman-এর কাছাকাছি গতি একসঙ্গে দেয় বলে পরিচিতি পায়, আর কয়েকটি নতুন compressor তা গ্রহণ করে।

তবু Huffman হারিয়ে যায়নি। চিহ্নের সম্ভাবনা খুব বাঁকা না হলে Huffman-এর ফাঁক ছোট, decoding অত্যন্ত সরল আর দ্রুত (একটি table-lookup), আর পুরোনো ফরম্যাটের সঙ্গে মিল রাখতে হয়। প্রকৌশলে “সর্বোত্তম” সবসময় একটি নির্দিষ্ট মডেলের ভেতরে; মডেল বদলালে সেরা উত্তরও বদলায়।

মূল উপলব্ধি: Huffman পূর্ণসংখ্যা-দৈর্ঘ্যের অক্ষর-ধরে code-এর মধ্যে সেরা; এই সীমা ভাঙলে arithmetic coding আর ANS entropy-র আরও কাছে যায়, তবে সরলতা আর গতিতে Huffman এখনো প্রতিযোগী।

প্র ০৫Huffman-এর প্রমাণে “exchange argument” আর “ছোট সমস্যায় নামা” কেন greedy প্রমাণের একটি সাধারণ ছাঁচ?

উপপাদ্য ৫.৩-এর প্রমাণ দুই অংশে: Lemma ৫.১ বলে greedy-র প্রথম পছন্দ (বিরল দুটিকে ভাই করা) কোনো না কোনো সর্বোত্তম সমাধানের সঙ্গে মেলানো যায়, অদলবদলের মাধ্যমে, খরচ না বাড়িয়ে। Lemma ৫.২ বলে সেই পছন্দের পর বাকি সমস্যাটি একই ধরনের একটি ছোট সমস্যা, আর খরচ একটি নির্দিষ্ট পরিমাণে আলাদা। এই দুটি মিলে induction চলে।

এই ছাঁচ অন্যত্রও দেখবেন। Minimum spanning tree-র Kruskal আর Prim algorithm-এর সঠিকতা আসে cut property থেকে, যা একটি exchange argument: সর্বোত্তম tree-তে একটি edge বদলে greedy-র edge বসালে ওজন বাড়ে না (পর্ব ৬৭ “Spanning tree — DFS/BFS, minimum spanning tree আর Kirchhoff-এর গণনা”)। Activity selection-এ প্রথমে শেষ হওয়া কাজটি নেওয়ার যুক্তিও একই। Dijkstra-র প্রমাণ (পর্ব ৬১) একটু আলাদা, “greedy এগিয়ে থাকে” ধাঁচের, কিন্তু মূল প্রশ্ন একই: প্রথম পছন্দটি কি কোনো সর্বোত্তম সমাধানের অংশ হতে পারে?

এর পেছনে একটি গভীর কাঠামোও আছে। কিছু greedy সমস্যা matroid নামের একটি বিমূর্ত গঠনে পড়ে, যেখানে greedy সবসময় সর্বোত্তম; MST তার উদাহরণ। Huffman ঠিক matroid-এর ছাঁচে পড়ে না, তাই তার জন্য নিজস্ব lemma লাগে; এজন্যই তার প্রমাণ পরীক্ষায় আলাদা করে চাওয়া হয়। Greedy প্রমাণের বিস্তারিত কৌশল DSA কোর্সের পর্ব ৫০-এ।

একটি সতর্কবার্তা: exchange-এর হিসাব প্রতিটি বিশেষ ক্ষেত্র ধরে করতে হয়। Lemma ৫.১-এ x যদি নিজেই সবচেয়ে গভীর leaf-গুলোর একটি হয়, তবে “x আর a বদল” অর্থহীন হয়ে যায়; প্রমাণে এই ক্ষেত্রগুলো আলাদা করে বলা হয়েছে। AI-লেখা প্রমাণে প্রায়ই এগুলোই বাদ পড়ে।

মূল উপলব্ধি: Greedy সঠিক প্রমাণের সাধারণ ছাঁচ হলো “প্রথম পছন্দ নিরাপদ” (exchange) আর “বাকিটা একই রকম ছোট সমস্যা” (induction); Huffman, Kruskal আর activity selection একই ছাঁচের তিন রূপ।

অনুশীলন

আগে খাতায় করুন, তারপর উত্তর খুলে মেলান।

  1. প্রমাণ: একটি সর্বোত্তম prefix code-এ pi > pj হলে প্রমাণ করুন li ≤ lj, অর্থাৎ বেশি সম্ভাব্য চিহ্ন কখনো বেশি লম্বা codeword পায় না।
    1. বিরোধের জন্য ধরুন li > lj. — contradiction
    2. i আর j-এর codeword অদলবদল করুন; নতুন code-ও prefix-free, কারণ codeword-এর সেট একই। — exchange
    3. গড় দৈর্ঘ্যের পরিবর্তন = pilj + pjli − pili − pjlj = −(pi − pj)(li − lj) < 0, কারণ দুই বন্ধনীই ধনাত্মক। — হিসাব
    4. গড় দৈর্ঘ্য কঠোরভাবে কমল, অথচ code সর্বোত্তম ছিল। বিরোধ, তাই li ≤ lj. ∎

    pi = pj হলে কিছুই বলা যায় না: tie-এর উদাহরণে (1, 1, 2, 2) সমান ওজনের c আর d দৈর্ঘ্য 2 আর 1 পেতে পারে।

  2. গণনা: গণনা 1, 1, 2, 4, 8, 16-এর জন্য (ক) Huffman-এর codeword-দৈর্ঘ্য, (খ) গড় দৈর্ঘ্য, (গ) entropy বের করুন। (ঘ) আটটি সমান সম্ভাবনার চিহ্নে Huffman কী দেয়?

    (ক) জোড়া: 1 + 1 = 2; 2 + 2 = 4; 4 + 4 = 8; 8 + 8 = 16; 16 + 16 = 32. দৈর্ঘ্য: 16 → 1, 8 → 2, 4 → 3, 2 → 4, দুটি 1 → 5 আর 5. (খ) জোড়ার যোগ 2 + 4 + 8 + 16 + 32 = 62, তাই L = 62/32 = 1.9375. (গ) সম্ভাবনা 1/2, 1/4, 1/8, 1/16, 1/32, 1/32, সবই 2-এর ঘাত, তাই H = L = 1.9375 (উপপাদ্য ৬.২-এর সমতা)। (ঘ) প্রতিটি জোড়ায় সমান ওজন মেলে, ফলে সব দৈর্ঘ্য 3, L = 3 = log2 8 = H: সমান সম্ভাবনায় Huffman fixed-length code-এর চেয়ে ভালো কিছু দিতে পারে না। চারটিই scratch-এ heapq দিয়ে মেলানো।

  3. আউটপুট অনুমান: কোড না চালিয়ে বলুন কী ছাপা হবে, আর code-টি সম্পর্কে Kraft কী বলে। তারপর যেকোনো Python-এ চালিয়ে মেলান।
    from fractions import Fraction
    codes = ["0", "10", "110", "111", "01"]
    print(sum(Fraction(1, 2 ** len(c)) for c in codes))
    print([(u, v) for u in codes for v in codes
           if u != v and v.startswith(u)])
    5/4
    [('0', '01')]

    যোগ 1/2 + 1/4 + 1/8 + 1/8 + 1/4 = 5/4 > 1. Kraft অনুযায়ী এই দৈর্ঘ্যের (1, 2, 2, 3, 3) কোনো prefix code নেই, আর McMillan অনুযায়ী কোনো UD code-ও নেই। দ্বিতীয় লাইন ঠিক একটি সংঘাত দেখায়: 0 হলো 01-এর prefix. সত্যিই code-টি UD নয়: “0110” পড়া যায় 0 · 110 হিসেবে, আবার 01 · 10 হিসেবেও (CPython 3.14-এ চালিয়ে মেলানো)।

  4. প্রমাণের ভুল ধরুন: দাবি: “যেকোনো code, যার codeword-গুলো আলাদা, তার গড় দৈর্ঘ্য L ≥ H.” প্রমাণ: “Codeword আলাদা, তাই Kraft অনুযায়ী ∑ 2−li ≤ 1. qi = 2−li নিয়ে Gibbs-এর অসমতা দেয় H ≤ ∑ pili = L. ∎” ভুল কোথায়? একটি পাল্টা-উদাহরণ দিন।
    • ভুল ধাপ: Kraft-এর অসমতা prefix code-এর জন্য, আর McMillan UD code-এর জন্য; শুধু codeword আলাদা হলে অসমতা মানতে হবে এমন কোনো কারণ নেই। উপপাদ্যের শর্ত বাদ দেওয়াই ভুল।
    • পাল্টা-উদাহরণ: সম্ভাবনা 1/2, 1/4, 1/4 আর code {0, 1, 00}. Codeword আলাদা, Kraft যোগ 1/2 + 1/2 + 1/4 = 5/4 > 1, আর L = 1/2 · 1 + 1/4 · 1 + 1/4 · 2 = 1.25 < 1.5 = H. কিন্তু “00” মানে c না aa, বলা যায় না: code UD নয়।
    • সঠিক কথা: উপপাদ্য ৬.২(ক), যেখানে code uniquely decodable; তখন McMillan অসমতা দেয়, আর Gibbs বাকিটা।

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

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

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

  1. ভিত্তি: দুটি চিহ্নে codeword 0 আর 1, গড় দৈর্ঘ্য 1, যা সর্বনিম্ন
  2. Exchange: এমন সর্বোত্তম tree আছে যাতে বিরল দুটি x, y সবচেয়ে গভীরে ভাই
  3. x, y-কে একটি চিহ্ন z-এ জুড়লে খরচ ঠিক px + py কমে
  4. Hypothesis: ছোট alphabet-এ Huffman-এর tree সেই জোড়া-দেওয়া সর্বোত্তম tree-র চেয়ে খারাপ নয়
  5. z খুলে x, y বসালে Huffman-এর খরচ সর্বোত্তম tree-র খরচের সমান বা কম

নিচের কোন binary code-টি prefix code?

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

পাঁচটি চিহ্নের codeword-দৈর্ঘ্য 1, 2, 3, 3, 3 রাখতে চাইলে কোন কথাটি সত্য?

  • এমন prefix code আছে, যেমন 0, 10, 110, 111, 101
  • এমন কোনো uniquely decodable code নেই
  • Prefix code নেই, তবে একটি UD code আছে
  • Codeword না জেনে বলা যায় না

ছয়টি চিহ্নের গণনা 5, 8, 12, 15, 24, 36 (মোট 100). Huffman code-এ প্রতি চিহ্নে গড় দৈর্ঘ্য কত bit?

  • 2.38
  • 2.31
  • 3
  • 2.94

তিনটি চিহ্নের সম্ভাবনা 1/2, 1/4, 1/4. যেকোনো uniquely decodable binary code-এ গড় দৈর্ঘ্যের সর্বনিম্ন মান কত?

  • 1
  • 1.585
  • 2
  • 1.5

Huffman-এর সর্বোত্তমতার প্রমাণে সবচেয়ে কম সম্ভাবনার দুটি চিহ্ন x, y সম্পর্কে কোন কথাটি প্রমাণ করা হয়?

  • প্রতিটি সর্বোত্তম tree-তে x আর y ভাই
  • অন্তত একটি সর্বোত্তম tree-তে x আর y সবচেয়ে গভীরে ভাই
  • x আর y-এর codeword-ই সবচেয়ে লম্বা, আর কারও সেই দৈর্ঘ্য নেই
  • x সবসময় বাঁ সন্তান, তাই তার codeword 0 দিয়ে শেষ

ওজন 1, 1, 2, 2-এ tie ভাঙার নিয়ম আলাদা হলে দুটি আলাদা Huffman tree আসে। দুটিতে কোনটি নিশ্চিতভাবে একই?

  • প্রতিটি চিহ্নের codeword-দৈর্ঘ্য
  • Tree-র height
  • গড় codeword-দৈর্ঘ্য
  • প্রতিটি চিহ্নের codeword

শব্দকোষ

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

মনে রাখুন

  1. Prefix code = leaf: কোনো codeword অন্যটির শুরু নয় ⇔ codeword-গুলো একটি binary tree-র leaf; প্রতিটি prefix code UD, উল্টোটা নয়।
  2. Kraft–McMillan: দৈর্ঘ্যের prefix code আছে ⇔ ∑ 2−li ≤ 1; UD code-ও এটি মানে, তাই prefix code-ই যথেষ্ট।
  3. Huffman: সবচেয়ে হালকা দুটি বারবার জোড়া; খরচ = জোড়ার ওজনের যোগ; exchange আর induction দিয়ে সর্বোত্তম।
  4. Tie: আলাদা tree, আলাদা দৈর্ঘ্য হতে পারে, কিন্তু গড় দৈর্ঘ্য একই; বাস্তবে canonical code.
  5. Entropy: UD code-এ H ≤ L, Huffman-এ L < H + 1, block-এ H + 1/k; সমতা শুধু 2-এর ঘাতের সম্ভাবনায়।

পরের পর্বে: মডিউল ১২ শুরু, পর্ব ৬৯ “Boolean algebra-র গণিত — function, identity আর minimization”: n চলকের কয়টি Boolean function, কোন operator-এর সেট দিয়ে সব function লেখা যায়, আর একটি function-কে সবচেয়ে ছোট রূপে আনার পদ্ধতি আসবে।

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

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