হাফম্যান কোডিং
এই পাঠে যা শিখবেন
- হাফম্যান কোডিংয়ের গ্রিডি নিয়ম এবং কেন এটি একটি ট্রি-নির্মাণ সমস্যা
- মিনিমাম-দুই-নোড-মার্জ নিয়মের পেছনের এক্সচেঞ্জ আর্গুমেন্ট সংক্ষেপে
- একটি প্রকৃত
heapq-ভিত্তিক ইমপ্লিমেন্টেশন যা ট্রি তৈরি করে, কোড বের করে - প্রিফিক্স-ফ্রি প্রপার্টি ও এনকোড/ডিকোড রাউন্ড-ট্রিপ — উভয়ই প্রোগ্রামগতভাবে, সত্যিকারভাবে যাচাই করা
১ · সমস্যাটি এবং কেন প্রিফিক্স-ফ্রি কোড দরকার
একটি টেক্সটের প্রতিটি ক্যারেক্টারকে যদি একই দৈর্ঘ্যের কোড দেওয়া হয় (যেমন ASCII-তে ৮ বিট), তাহলে বেশি
ব্যবহৃত ও কম ব্যবহৃত ক্যারেক্টার সমান জায়গা নেয় — অপচয়। যদি ভ্যারিয়েবল-লেংথ কোড ব্যবহার করি (বেশি ব্যবহৃত
ক্যারেক্টারকে ছোট কোড), তাহলে সমস্যা হলো: ডিকোড করার সময় বুঝতে হবে একটি কোড কোথায় শেষ হচ্ছে। এর সমাধান হলো
প্রিফিক্স-ফ্রি কোডPrefix-free Codeএমন একটি কোড সেট যেখানে কোনো ক্যারেক্টারের কোড অন্য কোনো ক্যারেক্টারের কোডের শুরুর অংশ (প্রিফিক্স) নয় — ফলে বিট স্ট্রিম বাম থেকে ডানে পড়ে দ্ব্যর্থহীনভাবে ডিকোড করা যায়, কোনো সেপারেটর ছাড়াই।
— যেমন যদি 'a' = 0 হয়, তাহলে অন্য কোনো ক্যারেক্টারের কোড 0 দিয়ে শুরু হতে পারবে না।
হাফম্যান কোডিং এই দুটো লক্ষ্যই একসাথে অর্জন করে: প্রিফিক্স-ফ্রি (ট্রি-ভিত্তিক গঠনের কারণে স্বয়ংক্রিয়ভাবে) এবং এক্সপেক্টেড কোড লেংথ ন্যূনতম (গ্রিডি মার্জ নিয়মের কারণে) — যেকোনো প্রিফিক্স-ফ্রি কোডিং স্কিমের মধ্যে সেরা। এই মেকানিক্স DSA কোর্সে বিস্তারিত কভার করা হয়েছে; এখানে আমরা গ্রিডি নিয়মের সঠিকতার যুক্তি এবং প্রকৃত ভেরিফিকেশনে মনোযোগ দেব।
২ · গ্রিডি নিয়ম ও এর সঠিকতার যুক্তি সংক্ষেপে
অ্যালগরিদম: প্রতিটি ক্যারেক্টারের জন্য একটি পাতা (leaf) নোড তৈরি করো (ফ্রিকোয়েন্সি সহ), তারপর বারবার — সবচেয়ে কম ফ্রিকোয়েন্সির দুটি নোড বেছে নিয়ে একটি নতুন প্যারেন্ট নোডে মার্জ করো (নতুন নোডের ফ্রিকোয়েন্সি = দুটোর যোগফল), যতক্ষণ না একটিমাত্র নোড (রুট) অবশিষ্ট থাকে।
এক্সচেঞ্জ আর্গুমেন্ট এখানে (L19-এর টেমপ্লেট অনুসরণ করে) দেখায়: যেকোনো অপটিমাল প্রিফিক্স-ফ্রি ট্রিতে, সবচেয়ে কম দুটি ফ্রিকোয়েন্সির ক্যারেক্টারকে সবসময় সবচেয়ে গভীর (deepest) দুই সিবলিং পাতা হিসেবে রাখা যায় — কারণ যদি তারা গভীরতম না হতো, তাহলে তাদের সাথে একটি গভীরতর, বেশি-ফ্রিকোয়েন্সির পাতার অবস্থান অদলবদল (exchange) করলে মোট এনকোডেড দৈর্ঘ্য কমে যেত বা একই থাকত (কম ফ্রিকোয়েন্সিকে গভীরে ও বেশি ফ্রিকোয়েন্সিকে কম গভীরে রাখাই সবসময় সমান-বা-ভালো) — এটাই ধাপ ৩। ফলে দুটি সবচেয়ে কম ফ্রিকোয়েন্সির নোডকে সিবলিং হিসেবে মার্জ করা নিরাপদ, এবং অবশিষ্ট সমস্যাটি (মার্জ করা নোডকে একটি একক নোড ধরে) একই আকারের একটি ছোট সাবপ্রবলেম — অপটিমাল সাবস্ট্রাকচার। এই সম্পূর্ণ আনুষ্ঠানিক প্রমাণ (যা ইনডাকশনসহ কিছুটা দীর্ঘ) স্ট্যান্ডার্ড অ্যালগরিদম টেক্সটবইগুলোতে (যেমন CLRS) বিস্তারিত পাওয়া যায় — এখানে মূল যুক্তির কাঠামোটি বোঝাই যথেষ্ট।
৩ · ইমপ্লিমেন্টেশন ও যাচাই — min-heap দিয়ে ট্রি নির্মাণ
নিচের কোড সেলে heapq (একটি সত্যিকারের min-heap) দিয়ে হাফম্যান ট্রি তৈরি করা হয়েছে, কোড বের
করা হয়েছে, এবং দুটো জিনিস প্রোগ্রামগতভাবে যাচাই করা হয়েছে: (ক) কোনো কোড অন্য কোনো কোডের প্রিফিক্স নয়
(সব কোড-জোড়ার উপর সরাসরি চেক), এবং (খ) এনকোড করে আবার ডিকোড করলে মূল স্ট্রিং হুবহু ফিরে আসে।
import heapq
import itertools
from collections import Counter
class Node:
__slots__ = ("freq", "char", "left", "right", "order")
def __init__(self, freq, char=None, left=None, right=None, order=0):
self.freq = freq
self.char = char # leaf হলে ক্যারেক্টার, অভ্যন্তরীণ নোড হলে None
self.left = left
self.right = right
self.order = order # heapq-তে টাই ভাঙার জন্য (Node তুলনাযোগ্য করতে)
def __lt__(self, other):
if self.freq != other.freq:
return self.freq < other.freq
return self.order < other.order
def build_huffman_tree(freqs):
counter = itertools.count()
heap = [Node(f, ch, order=next(counter)) for ch, f in freqs.items()]
heapq.heapify(heap) # সত্যিকারের min-heap তৈরি
if len(heap) == 1:
only = heap[0]
return Node(only.freq, left=only, right=None, order=next(counter))
while len(heap) > 1:
a = heapq.heappop(heap) # সবচেয়ে কম ফ্রিকোয়েন্সির নোড
b = heapq.heappop(heap) # দ্বিতীয় সবচেয়ে কম
merged = Node(a.freq + b.freq, left=a, right=b, order=next(counter))
heapq.heappush(heap, merged)
return heap[0]
def build_codes(node, prefix="", codes=None):
if codes is None:
codes = {}
if node is None:
return codes
if node.char is not None:
codes[node.char] = prefix if prefix else "0"
return codes
build_codes(node.left, prefix + "0", codes)
build_codes(node.right, prefix + "1", codes)
return codes
def is_prefix_free(codes):
# প্রতিটি জোড়া কোডের উপর সরাসরি চেক -- কোনোটি অন্যটির প্রিফিক্স কি না
items = list(codes.values())
for i in range(len(items)):
for j in range(len(items)):
if i != j and items[j].startswith(items[i]):
return False
return True
def encode(text, codes):
return "".join(codes[ch] for ch in text)
def decode(bits, root):
result = []
node = root
for b in bits:
node = node.left if b == "0" else node.right
if node.char is not None:
result.append(node.char)
node = root
return "".join(result)
text = "abracadabra_huffman_coding_example"
freqs = Counter(text)
tree = build_huffman_tree(freqs)
codes = build_codes(tree)
print("ফ্রিকোয়েন্সি:", dict(freqs))
print("হাফম্যান কোড:", codes)
assert is_prefix_free(codes), "কোডগুলো প্রিফিক্স-ফ্রি নয়!"
print("\nপ্রিফিক্স-ফ্রি চেক পাস করেছে (সব কোড-জোড়া পরীক্ষা করে)")
encoded = encode(text, codes)
decoded = decode(encoded, tree)
assert decoded == text, "রাউন্ড-ট্রিপ ব্যর্থ!"
print("এনকোড -> ডিকোড রাউন্ড-ট্রিপ সফল, মূল স্ট্রিং হুবহু ফিরে এসেছে:", decoded == text)
original_bits = len(text) * 8
compressed_bits = len(encoded)
print(f"\nমূল আকার (৮ বিট/ক্যারেক্টার ধরে): {original_bits} বিট")
print(f"হাফম্যান-এনকোডেড আকার: {compressed_bits} বিট")
print(f"সংকোচনের অনুপাত: {compressed_bits / original_bits:.2f}x আসল আকারের")
is_prefix_free ফাংশনটি হাফম্যান ট্রির গঠন সম্পর্কে কোনো অনুমান করছে না — এটি শুধু
সরাসরি প্রতিটি কোড-জোড়ার str.startswith() চেক করছে, যা প্রিফিক্স-ফ্রি হওয়ার আনুষ্ঠানিক
সংজ্ঞারই প্রত্যক্ষ প্রয়োগ। একইভাবে রাউন্ড-ট্রিপ টেস্টও (এনকোড করে ডিকোড করে মূল স্ট্রিংয়ের সাথে
assert মেলানো) কোনো তাত্ত্বিক দাবি নয়, একটি প্রকৃত কম্পিউট করা ফলাফল।
হাফম্যান কোডিং দেখায় গ্রিডি এক্সচেঞ্জ আর্গুমেন্ট শুধু "একটি জিনিস বেছে নেওয়া" সমস্যায় নয় (যেমন L20), বরং একটি ট্রি নির্মাণের মতো জটিল আউটপুট-কাঠামোতেও প্রযোজ্য — যতক্ষণ গ্রিডি-চয়েস প্রপার্টি ও অপটিমাল সাবস্ট্রাকচার আলাদাভাবে প্রমাণ করা যায়। L22-এ আমরা দেখব ফ্র্যাকশনাল ন্যাপস্যাকে গ্রিডি কাজ করে, কিন্তু এর নিকটাত্মীয় 0/1 ন্যাপস্যাকে (M6/L25) করে না।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
Node ক্লাসে order ফিল্ড এবং __lt__-এ টাই-ব্রেকিং লজিক কেন দরকার?
heapq-এর সরাসরি tuple-বিহীন কাস্টম অবজেক্ট ব্যবহারের জন্য অবজেক্টগুলো তুলনাযোগ্য
(< সংজ্ঞায়িত) হতে হয়। যদি দুটি নোডের freq সমান হয়, Python-এর
Node অবজেক্টের মধ্যে অন্য কোনো ফিল্ড (যেমন char, যা None হতে
পারে) দিয়ে তুলনা করতে গেলে TypeError হতে পারে। একটি ইউনিক, ক্রমবর্ধমান order
মান টাই ভাঙার জন্য একটি নির্ভরযোগ্য দ্বিতীয় কী দেয়, যা নিশ্চিত করে হিপ অপারেশন কখনো ব্যর্থ হবে না।
প্র ০২
যদি ইনপুট টেক্সটে মাত্র একটি ইউনিক ক্যারেক্টার থাকে (যেমন "aaaa"), তাহলে কী সমস্যা হতে পারত, এবং কোডে তা কীভাবে সামলানো হয়েছে?
একটিমাত্র ক্যারেক্টার থাকলে হিপে শুরুতেই একটিমাত্র নোড থাকবে — while len(heap) > 1
লুপ একবারও চলবে না, এবং সেই নোডটিই রুট হয়ে যাবে। কিন্তু build_codes রুট নোডকে সরাসরি
পাতা (leaf) হিসেবে দেখলে খালি স্ট্রিং কোড ("") দিত, যা এনকোডিং-এর জন্য অকার্যকর (শূন্য বিট
দিয়ে কিছু আলাদা করা যায় না)। এজন্যই build_huffman_tree-এ বিশেষভাবে len(heap) == 1
চেক করে রুটের নিচে একটি কৃত্রিম চাইল্ড নোড তৈরি করা হয়েছে, যাতে সেই একমাত্র ক্যারেক্টার অন্তত ১ বিটের
("0") কোড পায়।
প্র ০৩ প্রিফিক্স-ফ্রি চেক এবং রাউন্ড-ট্রিপ চেক — এই দুটো কি একই জিনিস প্রমাণ করে, নাকি ভিন্ন?
ভিন্ন। প্রিফিক্স-ফ্রি চেক প্রমাণ করে যে কোড সেটটি তাত্ত্বিকভাবে দ্ব্যর্থহীনভাবে ডিকোডযোগ্য (কোনো কোড অন্যটির শুরু নয়)। রাউন্ড-ট্রিপ চেক প্রমাণ করে যে এই নির্দিষ্ট ইমপ্লিমেন্টেশন (এনকোড ও ডিকোড ফাংশন দুটো) বাস্তবে সঠিকভাবে কাজ করছে — বাগমুক্ত। তাত্ত্বিকভাবে প্রিফিক্স-ফ্রি কোড থাকা সত্ত্বেও ডিকোড ফাংশনে একটি বাগ থাকলে রাউন্ড-ট্রিপ ব্যর্থ হতে পারত — তাই দুটো চেকই আলাদাভাবে গুরুত্বপূর্ণ।
অনুশীলন
-
চিন্তা করুন: উপরের আউটপুটে সবচেয়ে বেশি ফ্রিকোয়েন্সির ক্যারেক্টার 'a' (৭ বার) সবচেয়ে
ছোট কোড পেয়েছে না বড়? কেন এটাই প্রত্যাশিত?
'a' সবচেয়ে ছোট কোড (মাত্র ৩ বিট,
"111") পেয়েছে, কারণ এটি সবচেয়ে বেশি ফ্রিকোয়েন্সির ক্যারেক্টার — গ্রিডি নিয়মে বেশি ফ্রিকোয়েন্সির নোডগুলো শেষের দিকে মার্জ হয়, ফলে ট্রিতে কম গভীরে (root-এর কাছে) থাকে, তাই তাদের কোড ছোট হয়। এটাই হাফম্যান কোডিংয়ের মূল লক্ষ্য — এক্সপেক্টেড কোড লেংথ কমানো। -
পরীক্ষা করুন: কোড সেলে
textভ্যারিয়েবলটি বদলে একটি সম্পূর্ণ ভিন্ন স্ট্রিং (যেমন"mississippi") বসিয়ে রান করুন — প্রিফিক্স-ফ্রি চেক ও রাউন্ড-ট্রিপ চেক উভয়ই এখনও পাস করে কি না দেখুন।"mississippi"-তে ফ্রিকোয়েন্সি{'i': 4, 's': 4, 'p': 2, 'm': 1}-এর মতো হবে (সঠিক সংখ্যা রান করলেই নিশ্চিত হবে)। দুটো চেকই পাস করা উচিত — কারণ প্রমাণটি ইনপুট-নির্দিষ্ট নয়, যেকোনো ফ্রিকোয়েন্সি ডিস্ট্রিবিউশনের জন্য সাধারণভাবে প্রযোজ্য। যদি কোনো একটি চেক ব্যর্থ হতো, সেটি প্রমাণে নয় বরং ইমপ্লিমেন্টেশনে বাগ নির্দেশ করত।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ — ফ্র্যাকশনাল ন্যাপস্যাক L22 রেশিও-ভিত্তিক গ্রিডি নিয়ম, এবং কেন এটি 0/1 ন্যাপস্যাকে কাজ করে না তার একটি প্রিভিউ।
- আগের পাঠ — অ্যাক্টিভিটি সিলেকশন প্রবলেম L20 এক্সচেঞ্জ আর্গুমেন্ট টেমপ্লেটের প্রথম সম্পূর্ণ প্রয়োগ, একটি সরল সিলেকশন সমস্যায়।
- Data Structures & Algorithms কোর্স সহোদর কোর্স হাফম্যান কোডিং ও min-heap ইমপ্লিমেন্টেশনের বিস্তারিত ধাপে ধাপে ব্যাখ্যা সেই কোর্সে আছে।