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

Birthday paradox, hash collision আর balls-into-bins

The Birthday Paradox, Hash Collisions and Balls into Bins — the Exact Formula and Its Exponential Bound, the √n Birthday Bound, Empty Bins and Maximum Load, the Power of Two Choices, Hashing with Chaining, Universal Hashing and Bloom Filters

Taught in Bangla, this lesson derives the exact birthday formula, where 23 people give a match probability of 0.5073, and proves the exponential approximation behind the square-root birthday bound for hash digest length. Indicator variables give expected empty bins and colliding pairs, plus maximum load and two choices. It proves the 1 + α chaining cost and universal hashing, and in-page Python tabulates birthday odds, simulates balls into bins and measures Bloom filter false positives against the formula.

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

  • জন্মদিনের সমস্যার নির্ভুল সূত্র গণনা দিয়ে বের করা, আর e−k(k−1)/2n আসন্নমানের পূর্ণ প্রমাণ
  • Birthday bound থেকে hash digest-এর দৈর্ঘ্য বাছাই, আর “আমার জন্মদিন” বনাম “যেকোনো দুজন” প্রশ্নের পার্থক্য
  • Indicator variable দিয়ে খালি bin আর collision-জোড়ার প্রত্যাশা প্রমাণ, আর maximum load-এর উপরের সীমার প্রমাণ-রূপরেখা
  • Chaining-এর গড় খরচ 1 + α আর universal hashing-এর নিশ্চয়তা প্রমাণসহ
  • Bloom filter-এর false positive সূত্র derive করা, সেরা k বের করা, আর Python-এ মেপে সূত্রের সঙ্গে মেলানো

১ · জন্মদিনের সমস্যা — ২৩ জনেই অর্ধেকের বেশি

বাজারঘরের অফিসে ২৩ জন কর্মী। কেউ জিজ্ঞেস করলেন, “এখানে দুজনের জন্মদিন একই হওয়ার সম্ভাবনা কত?” বেশিরভাগ মানুষ অনুমান করেন খুব কম, হয়তো ৫ বা ১০ শতাংশ, কারণ বছরে দিন ৩৬৫টি আর মানুষ মাত্র ২৩ জন। আসল উত্তর ৫০ শতাংশের বেশি। এই অবাক করা ফলটির নাম birthday paradox. এটি আসলে কোনো যুক্তির বিরোধ নয়, শুধু আমাদের অনুমানের ভুল, তাই অনেকে একে birthday problem বলেন।

অনুমান ভুল হয় কারণ মন অজান্তে প্রশ্নটিকে বদলে ফেলে: “আমার জন্মদিনে আর কারও জন্মদিন পড়বে কি?” সেটি সত্যিই কম সম্ভাব্য। কিন্তু আসল প্রশ্ন যেকোনো দুজন নিয়ে, আর ২৩ জন থেকে জোড়া বানানো যায় C(23, 2) = 253টি। প্রতিটি জোড়ার মিলের সুযোগ ছোট, কিন্তু সুযোগের সংখ্যা মানুষের সংখ্যার বর্গের মতো বাড়ে। পর্ব ৩২-এ দেখেছিলাম, মিল নিশ্চিত করতে ৩৬৭ জন লাগে (২৯ ফেব্রুয়ারিসহ)। এই পর্বের প্রশ্ন অন্য: নিশ্চয়তা নয়, সম্ভাবনা।

মডেল: কী ধরে নিচ্ছি

গণিত করতে হলে পরীক্ষাটিকে স্পষ্ট করতে হয়। পর্ব ৩৮ “বিচ্ছিন্ন সম্ভাবনা — sample space, event আর Laplace-এর সংজ্ঞা”-র ভাষায়, আমরা তিনটি জিনিস ধরে নিচ্ছি: (১) বছর n = 365 দিনের, ২৯ ফেব্রুয়ারি বাদ; (২) প্রত্যেকের জন্মদিন n-টি দিনের যেকোনোটি সমান সম্ভাবনায়; (৩) একজনের জন্মদিন অন্যজনের ওপর নির্ভর করে না (যমজ নেই)। তাহলে k জনের জন্মদিনের তালিকা (d1, …, dk) হলো একটি ফল, আর sample space Ω = {1, …, n}k, যার আকার nk (পর্ব ৩১-এর গুণের নিয়ম)। প্রতিটি ফল সমান সম্ভাব্য, তাই একটি ঘটনার সম্ভাবনা = তার ফলের সংখ্যা ÷ nk.

উপপাদ্য ১.১ (জন্মদিনের সমস্যা, নির্ভুল সূত্র)

উপরের মডেলে k জনের (1 ≤ k ≤ n) মধ্যে অন্তত দুজনের জন্মদিন একই হওয়ার সম্ভাবনা

p(k) = 1 − n(n − 1)(n − 2)⋯(n − k + 1) / nk = 1 − ∏i=0k−1 (1 − i/n).

k > n হলে p(k) = 1.

প্রমাণ:

  1. ঘটনা A = “অন্তত দুজনের জন্মদিন একই”। এর complement Ac = “সবার জন্মদিন আলাদা”। P(A) = 1 − P(Ac). — complement-এর নিয়ম; ঘটনা সেট, তাই পর্ব ১৫-এর complement
  2. Ac-এর ফল হলো এমন তালিকা, যার k-টি মান সব আলাদা। প্রথমজনের n বিকল্প, দ্বিতীয়জনের n − 1, …, k-তম জনের n − k + 1. মোট n(n − 1)⋯(n − k + 1) = P(n, k)টি। — গুণের নিয়ম, পর্ব ৩৩-এর k-permutation
  3. সব ফল সমান সম্ভাব্য, তাই P(Ac) = P(n, k)/nk. প্রতিটি গুণনীয়ককে n দিয়ে ভাগ করলে (n − i)/n = 1 − i/n, i = 0, …, k − 1. — Laplace-এর সংজ্ঞা
  4. k > n হলে P(n, k)-এ শূন্য গুণনীয়ক আসে (n − n = 0), তাই P(Ac) = 0, যা pigeonhole-এর সঙ্গে মেলে। ∎

সূত্রটির একটি “গল্প” আছে, যা stepper-এ দেখবেন: মানুষ একজন একজন করে ঘরে ঢোকেন। i জন ঢোকার পর (সবার জন্মদিন আলাদা থাকলে) i-টি দিন “দখল” হয়েছে, তাই পরের জনের মিল এড়ানোর সম্ভাবনা (n − i)/n. সবগুলো গুণ করলেই সূত্র।

1k=1: P(সব আলাদা) = 12k=2: × 364/365 = 0.997263k=3: × 363/365 = 0.991804k=4: × 362/365 = 0.983645k=5: × 361/365 = 0.972866… k=23: × 343/365 = 0.49270
চলক
নতুন জনের ফাঁকা দিন
P(সব আলাদা)
  1. একজন। একা একজনের সঙ্গে মিলের প্রশ্ন নেই। ৩৬৫টি দিনই ফাঁকা, গুণনীয়ক 365/365 = 1, তাই P(সব আলাদা) = 1.
  2. দুজন। প্রথমজন একটি দিন নিয়েছেন। দ্বিতীয়জনের মিল এড়াতে বাকি ৩৬৪ দিনের একটি লাগবে: 364/365 ≈ 0.99726. মিলের সম্ভাবনা মাত্র 0.00274.
  3. তিনজন। দুটি দিন দখল, ফাঁকা ৩৬৩টি। আগের মানকে 363/365 দিয়ে গুণ: 0.99180. মিল 0.00820, আগের প্রায় তিনগুণ, কারণ জোড়া এখন ৩টি।
  4. চারজন। × 362/365, ফল 0.98364. জোড়া ৬টি, মিল 0.01636.
  5. পাঁচজন। × 361/365, ফল 0.97286. জোড়া ১০টি, মিল 0.02714. প্রতিটি নতুন গুণনীয়ক একটু একটু করে কমায়, কিন্তু কমার হার বাড়ছে।
  6. ২৩ জন। একইভাবে চালিয়ে k = 22-এ P(সব আলাদা) = 0.52430 (মিল 0.47570, অর্ধেকের কম)। ২৩তম জনের গুণনীয়ক 343/365 দিলে 0.49270, তাই মিলের সম্ভাবনা 0.50730 > 1/2. সবচেয়ে ছোট এমন k হলো 23.

মানুষের সংখ্যা বাড়লে মিলের সম্ভাবনা

নীল রেখা নির্ভুল p(k), n = 365. ২৩ জনে রেখাটি 0.5-এর দাগ পেরোয়; ৪০ জনে প্রায় 0.89, ৬০ জনে 0.994.

Birthday problem: probability of a shared birthday against group size 0102030405060 মানুষের সংখ্যা k 00.51 k = 23, p = 0.507 p(k)

ইতিহাস: জন্মদিনের প্রশ্নটি ছাপার অক্ষরে প্রথম তোলার কৃতিত্ব সাধারণত অস্ট্রীয় গণিতবিদ Richard von Mises-কে দেওয়া হয়, ১৯৩৯ সালের একটি প্রবন্ধের জন্য; তবে এর আগে Harold Davenport-সহ কয়েকজন মুখে মুখে প্রশ্নটি নিয়ে আলোচনা করতেন বলেও উল্লেখ পাওয়া যায়। তাই “আবিষ্কারক” হিসেবে একজনের নাম নিশ্চিতভাবে বলা কঠিন।

২ · আসন্নমান e−k(k−1)/2n আর √n-এর নিয়ম

নির্ভুল সূত্রটি গুণফল, তাই বড় n-এ (যেমন n = 264) মাথায় হিসাব করা যায় না, আর “কত k-এ অর্ধেক?” প্রশ্নের উত্তর সরাসরি বেরোয় না। দরকার একটি সহজ সূত্র। মূল হাতিয়ার একটি অসমতা, যা পুরো পর্বে বারবার লাগবে। এখানে e ≈ 2.71828 হলো স্বাভাবিক লগারিদমের ভিত্তি, আর ex = exp(x).

লেমা ২.১

যেকোনো বাস্তব x-এর জন্য 1 − x ≤ e−x, আর সমতা শুধু x = 0-এ। ছোট x-এ দুই পাশ প্রায় সমান।

প্রমাণ:

  1. f(x) = e−x − (1 − x) সংজ্ঞায়িত করুন। f(0) = 1 − 1 = 0. — সরাসরি বসানো
  2. f′(x) = 1 − e−x. x > 0 হলে e−x < 1, তাই f′ > 0; x < 0 হলে e−x > 1, তাই f′ < 0. — derivative-এর চিহ্ন
  3. অর্থাৎ f বাঁ দিক থেকে 0 পর্যন্ত কমে, তারপর বাড়ে; সবচেয়ে ছোট মান f(0) = 0. তাই সব x-এ f(x) ≥ 0, আর x ≠ 0-এ কঠোরভাবে বড়। ∎

ছোট x-এ e−x = 1 − x + x2/2 − ⋯, তাই পার্থক্য প্রায় x2/2, খুবই ছোট। জন্মদিনের গুণনীয়কগুলোতে x = i/n, যা ছোট; তাই অসমতাটি প্রায় সমতা।

উপপাদ্য ২.২ (birthday bound)

n-টি সমান সম্ভাব্য মান থেকে স্বাধীনভাবে k-টি মান নিলে

1 − e−k(k−1)/2n ≤ P(কোনো মিল) ≤ k(k − 1)/2n.

প্রমাণ (নিচের সীমা):

  1. উপপাদ্য ১.১: P(সব আলাদা) = ∏i=1k−1 (1 − i/n). (i = 0-এর গুণনীয়ক 1, বাদ দেওয়া যায়।)
  2. লেমা ২.১-এ প্রতিটি গুণনীয়ক 1 − i/n ≤ e−i/n, আর সব গুণনীয়ক অঋণাত্মক (k ≤ n ধরে; k > n হলে P(মিল) = 1, দাবি এমনিই সত্য)। অঋণাত্মক সংখ্যার অসমতা গুণ করা যায়। — লেমা ২.১
  3. তাই P(সব আলাদা) ≤ e−(1 + 2 + ⋯ + (k−1))/n = e−k(k−1)/2n. — eaeb = ea+b, আর পর্ব ১৭-এর 1 + ⋯ + (k − 1) = k(k − 1)/2
  4. complement নিলে P(মিল) ≥ 1 − e−k(k−1)/2n. ∎

প্রমাণ (উপরের সীমা): জোড়া {a, b}-এর জন্য ঘটনা Eab = “a আর b-এর মান সমান”, P(Eab) = n/n2 = 1/n. “কোনো মিল” = সব Eab-এর union, আর union bound (Boole-এর অসমতা, পর্ব ৩৮-এ প্রমাণিত) বলে union-এর সম্ভাবনা ≤ আলাদা সম্ভাবনার যোগফল = C(k, 2)/n. ∎

k = 23, n = 365-এ k(k − 1)/2n = 253/365 ≈ 0.69315, যা প্রায় হুবহু ln 2 ≈ 0.69315! তাই নিচের সীমা 1 − e−0.69315 ≈ 0.50000, আর নির্ভুল মান 0.50730. উপরের সীমা 0.6932, দুর্বল; k বাড়লে তা 1 ছাড়িয়ে যায় (k = 40-এ 2.137), তখন কোনো তথ্য দেয় না। মনে রাখার মতো ফল হলো নিচের সীমা থেকে আসা “অর্ধেকের নিয়ম”: e−k(k−1)/2n = 1/2 বসালে k(k − 1) = 2n ln 2, অর্থাৎ

k½ ≈ √(2n ln 2) ≈ 1.1774 √n.

n = 365-এ 1.1774 × 19.105 ≈ 22.49, তাই ২৩ জন। আরেকটি পরিচিত মাপ: প্রথম মিল আসা পর্যন্ত গড়ে কতজন লাগে? উত্তর প্রায় √(πn/2) + 2/3; n = 365-এ scratch-এ নির্ভুল হিসাবে 24.62 জন। পর্ব ২৫-এর Pollard rho ঠিক এই √p-যুক্তিতে চলে। সারকথা: n-টি সম্ভাব্য মানে মিল আসে মোটামুটি √n-টি নমুনায়, n/2-তে নয়।

এবার Python-এ নির্ভুল মান (Fraction দিয়ে, কোনো গোলমাল ছাড়া), আসন্নমান, union bound আর একটি seed-দেওয়া simulation পাশাপাশি দেখা যাক। প্রথমবার চালালে ব্রাউজারে Python (Pyodide) নামতে কয়েক সেকেন্ড লাগে।

import math, random
from fractions import Fraction
random.seed(42)

def p_exact(k, n=365):
    """P(at least two of k people share a birthday), exact."""
    no = Fraction(1)
    for i in range(k):
        no *= Fraction(n - i, n)          # person i+1 avoids i used days
    return 1 - no

def p_sim(k, n=365, trials=2000):
    hits = 0
    for _ in range(trials):
        days = [random.randrange(n) for _ in range(k)]
        hits += len(set(days)) < k        # some day used twice
    return hits / trials

print(" k   exact    1-e^(-k(k-1)/2n)  C(k,2)/n   sim")
for k in (5, 10, 20, 23, 30, 40, 60):
    ex = p_exact(k)
    approx = 1 - math.exp(-k * (k - 1) / (2 * 365))
    union = min(1, math.comb(k, 2) / 365)
    print(f"{k:2}  {float(ex):.4f}   {approx:.4f}            "
          f"{union:.4f}     {p_sim(k):.4f}")

k = 1
while p_exact(k) <= Fraction(1, 2):
    k += 1
ex = p_exact(k)
print("smallest k with P > 1/2:", k)
print("exact P(23) =", float(ex), "| denominator has",
      len(str(ex.denominator)), "digits")

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

 k   exact    1-e^(-k(k-1)/2n)  C(k,2)/n   sim
 5  0.0271   0.0270            0.0274     0.0260
10  0.1169   0.1160            0.1233     0.1120
20  0.4114   0.4058            0.5205     0.4240
23  0.5073   0.5000            0.6932     0.5055
30  0.7063   0.6963            1.0000     0.7045
40  0.8912   0.8820            1.0000     0.8935
60  0.9941   0.9922            1.0000     0.9925
smallest k with P > 1/2: 23
exact P(23) = 0.5072972343239854 | denominator has 53 digits

প্রতিটি সারিতে exact মান আসন্নমানের সামান্য ওপরে আর union bound-এর নিচে, ঠিক উপপাদ্য ২.২ যেমন বলে। আসন্নমানের ভুল এক শতাংশের মতো, তাই হাতে হিসাবের জন্য এটি যথেষ্ট। simulation (প্রতি k-এ ২০০০ বার) exact-এর চারপাশে ওঠানামা করে; ২০০০ নমুনায় প্রায় ±0.01 পার্থক্য স্বাভাবিক, তাই 20-এ 0.4240 বনাম 0.4114 কোনো ভুল নয়। শেষ লাইন দেখায়, p(23)-এর লঘিষ্ঠ ভগ্নাংশের হরে ৫৩টি অঙ্ক; Fraction সেটিও নির্ভুল রাখে। union bound-এর কলাম 1-এ আটকে যাওয়া মনে করিয়ে দেয়: এটি সম্ভাবনা নয়, শুধু একটি সীমা।

৩ · Birthday bound আর hash collision

এবার “দিন” বদলে “hash মান” বসান। একটি hash function যেকোনো ইনপুটকে b bit-এর একটি আউটপুটে নেয়, যাকে বলে digest; সম্ভাব্য digest n = 2bটি। দুটি আলাদা ইনপুটের digest সমান হলে তা collision. ইনপুট অসীম, আউটপুট সসীম, তাই collision থাকা নিশ্চিত (পর্ব ৩২)। প্রশ্ন হলো, এলোমেলো ইনপুট নিয়ে চেষ্টা করলে কতগুলোতে একটি collision আশা করা যায়।

ভালো hash-কে মডেলে একটি “এলোমেলো ফাংশন” ধরা হয়: প্রতিটি নতুন ইনপুটের digest যেন n-টি মানের যেকোনোটি সমান সম্ভাবনায়, আগেরগুলোর ওপর নির্ভর না করে (পর্ব ১৬-এর ভাষায় ইনপুটের সেট থেকে n-উপাদানের সেটে একটি এলোমেলো ফাংশন)। এটি জন্মদিনের মডেলই। তাই উপপাদ্য ২.২ সরাসরি খাটে, আর মোটামুটি 1.1774 · 2b/2টি ইনপুটে collision-এর সম্ভাবনা অর্ধেক হয়। এই √n-সীমাকে বলে birthday bound.

digest দৈর্ঘ্য bসম্ভাব্য মান 2b2b/2৫০% collision-এ প্রায় কয়টি ইনপুট
32 bit≈ 4.3 × 10965,536≈ 77,163 (নির্ভুল হিসাবে 77,164)
64 bit≈ 1.8 × 1019≈ 4.3 × 109≈ 5.1 × 109
128 bit≈ 3.4 × 1038≈ 1.8 × 1019≈ 2.2 × 1019
256 bit≈ 1.2 × 1077≈ 3.4 × 1038≈ 4.0 × 1038

টেবিলের শিক্ষা পরিষ্কার। ধরুন বাজারঘর প্রতিটি অর্ডারের জন্য একটি এলোমেলো 32-bit order id বানায়। ৪০০ কোটির বেশি সম্ভাব্য id দেখে মনে হয় collision প্রায় অসম্ভব, অথচ মাত্র ৭৭ হাজার অর্ডারেই দুটি অর্ডারের id মেলার সম্ভাবনা অর্ধেক। ১০,০০০ অর্ডারেও সম্ভাবনা প্রায় 1.16 শতাংশ (নিচের অঙ্ক ১)। তাই এলোমেলো id-তে অন্তত 128 bit রাখা হয়, অথবা database-এ আগে থেকে আছে কি না যাচাই করা হয়।

নিরাপত্তায় একই অঙ্কের নাম birthday attack: কেউ যদি যেকোনো দুটি আলাদা বার্তা চান যাদের digest সমান (কোন বার্তা, তাতে কিছু যায় আসে না), তবে এলোমেলো বার্তার digest হিসাব করে মিল খুঁজলেই হয়, আর সাধারণ পদ্ধতিতে প্রায় 2b/2টি চেষ্টা লাগে। একটি নির্দিষ্ট digest-এর সঙ্গে মেলাতে (preimage) লাগে প্রায় 2bটি, যা অনেক বেশি; এটি “আমার জন্মদিন” প্রশ্নের মতো। তাই b-bit hash-এর collision-এর বিরুদ্ধে নিরাপত্তা মোটামুটি b/2 bit, আর এই কারণেই আধুনিক hash-এর digest 256 bit: তখন 2128টি চেষ্টা লাগে, যা কার্যত অসম্ভব। এই পর্বে আমরা শুধু গণিতটি দেখছি; hash-এর নিরাপদ ব্যবহার Cybersecurity কোর্সের পর্ব ০৯-এ।

সাবধান: birthday bound একটি সাধারণ সীমা, যা ধরে নেয় hash এলোমেলো ফাংশনের মতো আচরণ করে। কোনো hash-এর গঠনে দুর্বলতা থাকলে collision এর চেয়ে অনেক কম চেষ্টায় পাওয়া যেতে পারে; MD5 আর SHA-1 ঠিক এভাবেই ভেঙেছে। তাই “256 bit মানে নিরাপদ” কথাটি শুধু তখনই খাটে, যখন hash-টি বিশেষজ্ঞদের দ্বারা যাচাই করা।

৪ · Balls into bins — খালি bin আর collision-জোড়ার প্রত্যাশা

জন্মদিন, hash আর আরও অনেক সমস্যার একটি সাধারণ মডেল আছে: balls into bins. n-টি bin (ঝুড়ি) আর m-টি বল; প্রতিটি বল স্বাধীনভাবে, সমান সম্ভাবনায় একটি bin-এ পড়ে। বাজারঘরের ভাষায়: m-টি অর্ডার n জন ডেলিভারি রাইডারের মধ্যে এলোমেলো ভাগ হলো। প্রশ্নগুলো স্বাভাবিক: কতজন রাইডার কোনো অর্ডার পাবেন না? কত জোড়া অর্ডার একই রাইডারের কাছে গেল? সবচেয়ে ব্যস্ত রাইডারের হাতে কয়টি? জন্মদিনে bin = দিন, বল = মানুষ; hash table-এ bin = slot, বল = key.

প্রত্যাশা (expected value) আর linearity-র মূল ধারণা পর্ব ৪০ “Random variable আর প্রত্যাশা — linearity-র জাদু”-র বিষয়; এখানে দুটি তথ্যই লাগবে। প্রথমত, একটি ঘটনা A-এর indicator random variable IA হলো A ঘটলে 1, না ঘটলে 0, আর E[IA] = 1 · P(A) + 0 · P(Ac) = P(A). দ্বিতীয়ত, linearity of expectation: E[X + Y] = E[X] + E[Y], চলকগুলো স্বাধীন হোক বা না হোক। এই দুটি মিলে একটি শক্তিশালী কৌশল দেয়: জটিল একটি সংখ্যাকে অনেক indicator-এর যোগফল হিসেবে লিখুন, আর প্রতিটির সম্ভাবনা যোগ করুন।

উপপাদ্য ৪.১ (খালি bin)

m-টি বল n-টি bin-এ (n ≥ 1) স্বাধীন ও সমান সম্ভাবনায় ফেললে খালি bin-এর সংখ্যা X-এর প্রত্যাশা

E[X] = n(1 − 1/n)m ≤ n e−m/n.

বিশেষ করে m = n হলে E[X]/n → 1/e ≈ 0.3679, অর্থাৎ প্রায় ৩৭ শতাংশ bin খালি থাকে।

প্রমাণ:

  1. প্রতিটি bin j-এর জন্য Ij = 1 যদি bin j খালি থাকে, নইলে 0. তাহলে X = I1 + I2 + ⋯ + In. — প্রতিটি খালি bin একবার গোনা হয়
  2. একটি নির্দিষ্ট বল bin j-এ না পড়ার সম্ভাবনা 1 − 1/n. বলগুলো স্বাধীন, তাই m-টির কোনোটিই না পড়ার সম্ভাবনা (1 − 1/n)m. — স্বাধীন ঘটনার গুণ
  3. E[Ij] = P(bin j খালি) = (1 − 1/n)m. — indicator-এর প্রত্যাশা
  4. linearity: E[X] = ∑j E[Ij] = n(1 − 1/n)m. লেমা ২.১-এ 1 − 1/n ≤ e−1/n, তাই E[X] ≤ n e−m/n. ∎

লক্ষ করুন, Ij-গুলো পরস্পর স্বাধীন নয় (একটি bin খালি হলে বাকিগুলোতে বল বেশি পড়ে), তবু linearity খাটে। m = n-এ (1 − 1/n)n → 1/e, এটি e-এর একটি পরিচিত সীমা; n = 10-এ মান 0.3487, n = 1000-এ 0.3677, আর n = 106-এ 0.36788.

1X = I₁ + I₂ + I₃2P(bin 1 খালি) = (2/3)³3E[Iⱼ] = 8/27, j = 1, 2, 34E[X] = 3 · 8/27 = 8/9527 ফল: 6 + 18 + 36মোট খালি = 18 + 2·3 = 247n(1 − 1/n)ⁿ ≈ n/e
চলক
ধাপের হিসাব
E[X]
  1. ভাঙা। ৩টি অর্ডার ৩ জন রাইডারের মধ্যে এলোমেলো ভাগ হলো। X = কতজন কোনো অর্ডার পেলেন না। প্রতিটি রাইডার j-এর জন্য Ij = 1 যদি তিনি খালি হাতে থাকেন; তাহলে X = I1 + I2 + I3.
  2. একটি bin. প্রতিটি অর্ডার রাইডার 1-এর কাছে না যাওয়ার সম্ভাবনা 2/3, আর অর্ডারগুলো স্বাধীন। তাই P(রাইডার 1 খালি) = (2/3)3 = 8/27.
  3. প্রতিসাম্য। একই যুক্তি রাইডার 2 আর 3-এর জন্য। তাই E[I1] = E[I2] = E[I3] = 8/27.
  4. Linearity. E[X] = 8/27 + 8/27 + 8/27 = 24/27 = 8/9 ≈ 0.889. Ij-গুলো স্বাধীন কি না, তা দেখার দরকার পড়েনি।
  5. Brute force. মোট 33 = 27টি সমান সম্ভাব্য ফল। গুনলে: ৬টিতে কেউ খালি নন (তিনজন তিনটি অর্ডার পান, 3! = 6), ১৮টিতে ঠিক একজন খালি, ৩টিতে দুজন খালি (সব অর্ডার একজনের কাছে)।
  6. মিলানো। সব ফলে মোট খালি = 0 · 6 + 1 · 18 + 2 · 3 = 24. গড় 24/27 = 8/9, সূত্রের সঙ্গে হুবহু মিল।
  7. বড় n. n = m হলে E[X] = n(1 − 1/n)n. n = 3-এ 0.889, যা 3/e ≈ 1.104-এর চেয়ে কম; কিন্তু n বাড়লে অনুপাত E[X]/n দ্রুত 1/e ≈ 0.368-এর কাছে যায় (n = 1000-এ অনুপাতটি 0.3677 হয়)।
উপপাদ্য ৪.২ (collision-জোড়া)

m-টি বল n-টি bin-এ ফেললে একই bin-এ পড়া বল-জোড়ার সংখ্যা Y-এর প্রত্যাশা E[Y] = C(m, 2)/n = m(m − 1)/2n.

প্রমাণ: প্রতিটি জোড়া {a, b}-এর জন্য Jab = 1 যদি a আর b একই bin-এ পড়ে। P(Jab = 1) = ∑j (1/n)(1/n) = 1/n. Y = ∑ Jab, জোড়া C(m, 2)টি (পর্ব ৩৪), তাই linearity-তে E[Y] = C(m, 2)/n. ∎ প্রমাণে শুধু “যেকোনো দুটি বল স্বাধীন” লেগেছে; পুরো স্বাধীনতা লাগেনি। এই পর্যবেক্ষণটিই universal hashing-এর মূলে (অংশ ৬)।

এবার জন্মদিনের সঙ্গে সংযোগটি দেখুন: ২৩ জনে প্রত্যাশিত মিল-জোড়া 253/365 ≈ 0.69, এক জোড়ার কম। তবু “অন্তত একটি মিল”-এর সম্ভাবনা অর্ধেকের বেশি। প্রত্যাশা আর সম্ভাবনা আলাদা জিনিস, কিন্তু birthday bound মূলত এই কথাই বলে: প্রত্যাশিত জোড়া প্রায় 1 হলে মিল সম্ভাব্য হয়ে ওঠে, অর্থাৎ m2/2n ≈ 1, বা m ≈ √(2n).

৫ · সবচেয়ে ভরা bin — maximum load আর দুটি বিকল্পের জাদু

m = n হলে গড়ে প্রতিটি bin-এ ঠিক একটি বল। কিন্তু গড় দিয়ে সবচেয়ে ব্যস্ত রাইডারের কষ্ট বোঝা যায় না। hash table-এ সবচেয়ে লম্বা chain খারাপ ক্ষেত্রের খোঁজার সময় ঠিক করে, আর server-এ সবচেয়ে ভরা machine ঠিক করে কে আগে ভেঙে পড়বে। সবচেয়ে ভরা bin-এর বলের সংখ্যাকে বলে maximum load.

উপপাদ্য ৫.১ (maximum load, উপরের সীমা)

n-টি বল n-টি bin-এ ফেললে, যেকোনো পূর্ণসংখ্যা t ≥ 1-এর জন্য P(কোনো bin-এ অন্তত t-টি বল) ≤ n/t!. বিশেষ করে t! ≥ n2 হলে maximum load t-এর কম হওয়ার সম্ভাবনা অন্তত 1 − 1/n.

প্রমাণ:

  1. একটি নির্দিষ্ট bin-এ অন্তত t-টি বল থাকলে কোনো t-উপাদানের বল-সেট পুরোটা সেই bin-এ পড়েছে। একটি নির্দিষ্ট t-সেটের সবগুলো সেখানে পড়ার সম্ভাবনা (1/n)t. — স্বাধীনতা
  2. t-সেট C(n, t)টি, তাই union bound-এ P(bin-এ ≥ t) ≤ C(n, t)(1/n)t. — union bound
  3. C(n, t) = n(n − 1)⋯(n − t + 1)/t! ≤ nt/t!, তাই সীমাটি ≤ 1/t!. — প্রতিটি গুণনীয়ক ≤ n
  4. n-টি bin-এর ওপর আবার union bound: P(কোনো bin-এ ≥ t) ≤ n/t!. t! ≥ n2 হলে এটি ≤ 1/n. ∎

t কত বড় হলে t! ≥ n2? Stirling-এর আসন্নমান t! ≈ (t/e)t√(2πt) দিয়ে হিসাব করলে দেখা যায়, t = ⌈3 ln n / ln ln n⌉ বড় n-এ যথেষ্ট; তাই maximum load উচ্চ সম্ভাবনায় O(log n / log log n). উদাহরণ: n = 1000-এ 10! = 3,628,800 ≥ 106, তাই P(maximum load ≥ 10) ≤ 1000/10! ≈ 0.00028. একটি মিলিয়ে দেখার মতো নিচের সীমাও জানা আছে: উচ্চ সম্ভাবনায় maximum load অন্তত (1 − o(1)) ln n / ln ln n, তাই আসল বৃদ্ধি Θ(log n / log log n). সেই প্রমাণ এখানে দিচ্ছি না; তা উন্নত পাঠ্যবইয়ে (যেমন Mitzenmacher ও Upfal-এর probability and computing বই) পাবেন।

দুটি বিকল্পের জাদু

এবার একটি ছোট বদল। প্রতিটি অর্ডারের জন্য এলোমেলোভাবে দুজন রাইডার বেছে, তাঁদের মধ্যে যাঁর হাতে এই মুহূর্তে কম অর্ডার, তাঁকে দিন। সুপারশপে দুটি ক্যাশ কাউন্টার দেখে ছোট লাইনে দাঁড়ানোর মতো। ফল নাটকীয়: maximum load log n / log log n থেকে নেমে আসে প্রায় ln ln n / ln 2 + O(1)-এ, যা কার্যত ধ্রুবক (n = 106-এ ln ln n / ln 2-এর মান প্রায় 3.8 হয়)। এই ফলটি power of two choices নামে পরিচিত; Azar, Broder, Karlin আর Upfal-এর কাজ, যা ১৯৯৪ সালে সম্মেলনে আর ১৯৯৯ সালে জার্নালে প্রকাশিত বলে উল্লেখ করা হয়। আশ্চর্যের কথা, d-টি বিকল্পে উন্নতি শুধু ln ln n / ln d, অর্থাৎ ১ থেকে ২-এ যাওয়াই বড় লাফ, ২ থেকে ৩-এ সামান্য। এর প্রমাণ এই পর্বের সীমার বাইরে; আমরা Python-এ সত্যিই দেখব।

import math, random
from collections import Counter
random.seed(7)

def throw(m, n, choices=1):
    """Throw m balls into n bins; with 2 choices pick the lighter bin."""
    load = [0] * n
    for _ in range(m):
        cand = [random.randrange(n) for _ in range(choices)]
        load[min(cand, key=lambda b: load[b])] += 1
    return load

n = m = 1000
T = 200
empty = pairs = 0
top1, top2 = Counter(), Counter()
for _ in range(T):
    load = throw(m, n)
    empty += load.count(0)
    pairs += sum(c * (c - 1) // 2 for c in load)
    top1[max(load)] += 1
    top2[max(throw(m, n, 2))] += 1
print(f"empty bins: sim {empty / T:.2f} | n(1-1/n)^m = "
      f"{n * (1 - 1 / n) ** m:.2f} | n/e = {n / math.e:.2f}")
print(f"colliding pairs: sim {pairs / T:.2f} | C(m,2)/n = "
      f"{math.comb(m, 2) / n:.2f}")
print("max load, 1 choice :", sorted(top1.items()))
print("max load, 2 choices:", sorted(top2.items()))
for n in (10**2, 10**4, 10**5):
    a, b = max(throw(n, n)), max(throw(n, n, 2))
    lnn = math.log(n)
    print(f"n={n:>6}: max load 1 choice {a}, 2 choices {b} | "
          f"ln n/ln ln n = {lnn / math.log(lnn):.2f}")

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

empty bins: sim 366.93 | n(1-1/n)^m = 367.70 | n/e = 367.88
colliding pairs: sim 498.50 | C(m,2)/n = 499.50
max load, 1 choice : [(4, 6), (5, 108), (6, 69), (7, 9), (8, 7), (9, 1)]
max load, 2 choices: [(3, 199), (4, 1)]
n=   100: max load 1 choice 4, 2 choices 3 | ln n/ln ln n = 3.02
n= 10000: max load 1 choice 6, 2 choices 4 | ln n/ln ln n = 4.15
n=100000: max load 1 choice 8, 2 choices 4 | ln n/ln ln n = 4.71

প্রথম দুই লাইন উপপাদ্য ৪.১ আর ৪.২-কে ২০০ বারের গড়ে নিশ্চিত করে: খালি bin 366.93 বনাম 367.70, collision-জোড়া 498.50 বনাম 499.50. তৃতীয় লাইন এক বিকল্পে maximum load-এর বণ্টন: ২০০ বারের মধ্যে বেশিরভাগ ৫ বা ৬, কখনো ৮–৯; কখনোই ১০ নয়, যা উপপাদ্য ৫.১-এর 0.00028 সীমার সঙ্গে মেলে। দুই বিকল্পে ২০০ বারের ১৯৯ বারই ঠিক ৩! শেষ তিন লাইনে n বাড়লে এক বিকল্পের maximum load 4 → 6 → 8 বাড়ছে, আর দুই বিকল্পে 3 → 4 → 4, প্রায় স্থির। ln n / ln ln n শুধু বৃদ্ধির হার দেখায়, হুবহু মান নয়; ধ্রুবক গুণনীয়ক আর ছোট পদ বাদ দেওয়া, তাই সরাসরি তুলনা নয়, প্রবণতা দেখুন।

৬ · Chaining-এর খরচ 1 + α আর universal hashing

Hash table-এ n-টি slot থাকে, আর key x রাখা হয় slot h(x)-এ। Separate chaining-এ প্রতিটি slot-এ একটি linked list, আর একই slot-এ পড়া সব key সেই তালিকায় যায়। m-টি key রাখলে load factor α = m/n, গড়ে প্রতিটি তালিকার দৈর্ঘ্য। (DSA বইয়ে প্রায়ই অক্ষর উল্টো, n key আর m slot; এই পর্বে balls-into-bins-এর সঙ্গে মিল রেখে n = slot.) কোডের দিক DSA কোর্সের পর্ব ৩২-এ; এখানে প্রশ্ন একটাই: গড়ে কতগুলো key-এর সঙ্গে তুলনা লাগে, আর কেন।

গণিতের জন্য একটি মডেল লাগে। Simple uniform hashing ধরে নেয়: প্রতিটি key স্বাধীনভাবে, সমান সম্ভাবনায় n-টি slot-এর যেকোনোটিতে যায়। অর্থাৎ key-রা হলো balls, slot-রা bins.

উপপাদ্য ৬.১ (chaining-এর গড় খরচ)

Simple uniform hashing-এ m-টি key আর n-টি slot (α = m/n):

(ক) যে key টেবিলে নেই, তার খোঁজে (unsuccessful search) প্রত্যাশিত তুলনা α, তাই hash হিসাবসহ মোট খরচ Θ(1 + α).

(খ) টেবিলের m-টি key-এর একটি সমান সম্ভাবনায় বেছে খুঁজলে (successful search, নতুন key তালিকার মাথায় বসে) প্রত্যাশিত তুলনা 1 + (m − 1)/2n < 1 + α/2.

প্রমাণ (ক):

  1. খোঁজা key x, h(x) = s. খুঁজতে slot s-এর পুরো তালিকা দেখতে হয়, তাই তুলনা = Ls = সেই তালিকার দৈর্ঘ্য। — x নেই, তাই শেষ পর্যন্ত যেতে হয়
  2. রাখা key k1, …, km-এর জন্য Ii = 1 যদি h(ki) = h(x). Ls = ∑ Ii. — indicator
  3. h(ki) আর h(x) স্বাধীন ও সমবণ্টিত, তাই P(Ii = 1) = 1/n. — মডেল
  4. linearity: E[Ls] = m/n = α. hash হিসাবের একটি ধাপ যোগ করলে মোট 1 + α. ∎

প্রমাণ (খ):

  1. key-গুলো k1, …, km ক্রমে ঢোকানো হয়েছে, প্রতিটি তালিকার মাথায়। তাই ki খুঁজতে তার আগে দেখা হয় শুধু তার পরে ঢোকা, একই slot-এর key-গুলো। — মাথায় বসানো
  2. তুলনা = 1 + ∑j>i Xij, যেখানে Xij = 1 যদি h(kj) = h(ki); E[Xij] = 1/n. তাই E = 1 + (m − i)/n. — indicator আর linearity
  3. i সমান সম্ভাবনায় 1..m, তাই গড় নিলে 1 + (1/m) ∑i=1m (m − i)/n = 1 + (1/m) · m(m − 1)/2 · (1/n) = 1 + (m − 1)/2n. ∎ — ∑(m − i) = 0 + 1 + ⋯ + (m − 1)

অর্থাৎ α ধ্রুবক রাখলে (যেমন টেবিল ভরে গেলে slot দ্বিগুণ করা) খোঁজা গড়ে O(1). উদাহরণ: ৬টি key, ৩টি slot (α = 2): না-থাকা key-এর খোঁজে গড়ে 2টি তুলনা, থাকা key-এর খোঁজে 1 + 5/6 ≈ 1.83টি।

মডেলটি কি বাস্তব? Universal hashing

Simple uniform hashing একটি অনুমান, আর বাস্তবে h একটি নির্দিষ্ট ফাংশন। কেউ যদি জেনে-বুঝে এমন key পাঠান যেগুলো সব একই slot-এ পড়ে, তবে তালিকা m-লম্বা হয়ে খোঁজা O(m) হয়ে যায়। পর্ব ২৬-এ এর উত্তর দেখেছিলাম: universal hashing, যেখানে প্রোগ্রাম শুরুতে একটি ফাংশন-পরিবার থেকে এলোমেলোভাবে h বাছে (যেমন ((ak + b) mod p) mod n; সেখানে বালতির সংখ্যাকে m লেখা হয়েছিল)। পরিবার H-কে universal বলে, যদি যেকোনো দুটি আলাদা key x ≠ y-এর জন্য Ph∈H[h(x) = h(y)] ≤ 1/n.

উপপাদ্য ৬.২ (universal hashing-এর নিশ্চয়তা)

H universal হলে, যেকোনো নির্দিষ্ট m-টি key-এর সেট S আর যেকোনো x ∉ S-এর জন্য, এলোমেলো h ∈ H-এ slot h(x)-এর তালিকার প্রত্যাশিত দৈর্ঘ্য ≤ m/n = α.

প্রমাণ: L = ∑y∈S Iy, যেখানে Iy = 1 যদি h(y) = h(x). universal শর্তে E[Iy] ≤ 1/n, তাই linearity-তে E[L] ≤ m/n. ∎

পার্থক্যটি সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ। উপপাদ্য ৬.১-এ এলোমেলোতা ছিল ডেটায় (key-রা যেন এলোমেলো), যা আমাদের নিয়ন্ত্রণে নেই। উপপাদ্য ৬.২-এ এলোমেলোতা আমাদের বাছাই করা h-এ, আর key-রা যেকোনো, এমনকি প্রতিপক্ষের বাছা। প্রমাণে শুধু জোড়ায় জোড়ায় collision-এর সম্ভাবনা লেগেছে, ঠিক উপপাদ্য ৪.২-এর মতো। এই ধারণাটি Carter আর Wegman-এর, ১৯৭০-এর দশকের শেষভাগে প্রকাশিত।

৭ · Bloom filter — কম জায়গায় “হয়তো আছে” আর “নিশ্চিত নেই”

বাজারঘরে প্রতিদিন লাখো ব্যবহৃত coupon code জমা হয়। নতুন code এলে জানতে হবে আগে ব্যবহার হয়েছে কি না। সব code মেমরিতে রাখা ব্যয়বহুল, আর ডিস্কে খোঁজা ধীর। একটি Bloom filter খুব কম জায়গায় এই প্রশ্নের প্রায়-সঠিক উত্তর দেয়: “নিশ্চিত নেই” বললে সেটি সবসময় সত্য; “হয়তো আছে” বললে মাঝে মাঝে ভুল হতে পারে। এই ভুলের নাম false positive. উত্তর “হয়তো আছে” হলে তবেই ধীর ডিস্কে নিশ্চিত হতে যাওয়া হয়, তাই বেশিরভাগ খোঁজা দ্রুত শেষ হয়।

গঠন সরল। m-টি bit-এর একটি সারি, শুরুতে সব 0, আর k-টি hash function h1, …, hk, প্রতিটি 0..m − 1-এ মান দেয়। (এই অংশে অক্ষর প্রচলিত রীতিতে: m = bit-এর সংখ্যা, n = রাখা item-এর সংখ্যা।) item x ঢোকাতে k-টি অবস্থান h1(x), …, hk(x)-এ bit 1 করা হয়। খুঁজতে একই k-টি অবস্থান দেখা হয়: কোনোটি 0 হলে x কখনো ঢোকেনি (কারণ ঢুকলে সেটি 1 হতো); সবগুলো 1 হলে “হয়তো আছে”, কারণ অন্য item-রা মিলে ঠিক সেই bit-গুলো 1 করে থাকতে পারে। নিচের stepper-এ একটি খেলনা filter: m = 10, k = 3, তিনটি তিন অঙ্কের order id, আর hash হলো h1 = শেষ অঙ্ক, h2 = দশকের অঙ্ক, h3 = অঙ্কগুলোর যোগফল mod 10.

1bits = 00000000002insert 427 → 7, 2, 33insert 815 → 5, 1, 44insert 362 → 2, 6, 15query 815 → 5, 1, 46query 509 → 9, 0, 47query 236 → 6, 3, 18FP হার ≈ (7/10)³
চলক
ঘর
bits
দেখা bit
  1. শুরু। ১০টি bit, ঘর 0 থেকে 9, সব 0. কোনো item নেই, তাই যেকোনো খোঁজে উত্তর “নিশ্চিত নেই”।
  2. 427 ঢোকানো। শেষ অঙ্ক 7, দশকের অঙ্ক 2, অঙ্কের যোগফল 4 + 2 + 7 = 13 → 3. ঘর 2, 3, 7-এ bit 1.
  3. 815 ঢোকানো। শেষ অঙ্ক 5, দশকের অঙ্ক 1, যোগফল 14 → 4. ঘর 1, 4, 5-এ bit 1. এখন ৬টি bit 1.
  4. 362 ঢোকানো। শেষ অঙ্ক 2, দশকের অঙ্ক 6, যোগফল 11 → 1. ঘর 2 আর 1 আগেই 1, শুধু ঘর 6 নতুন। সাতটি bit 1: ঘর 1 থেকে 7.
  5. 815 খোঁজা। ঘর 5, 1, 4: তিনটিই 1. উত্তর “হয়তো আছে”, আর 815 সত্যিই আছে। ঢোকানো item-এ উত্তর কখনো “নেই” হয় না: false negative অসম্ভব।
  6. 509 খোঁজা। শেষ অঙ্ক 9, দশকের অঙ্ক 0, যোগফল 14 → 4. ঘর 9 আর 0-এ bit 0, তাই 509 নিশ্চিতভাবে কখনো ঢোকেনি।
  7. 236 খোঁজা। ঘর 6, 3, 1, কারণ যোগফল 11 → 1. তিনটিই 1, কারণ 362 ঘর 6 আর 1, 427 ঘর 3 ভরেছিল। উত্তর “হয়তো আছে”, অথচ 236 ঢোকানো হয়নি: একটি false positive.
  8. হার। ১০টির ৭টি bit 1, তাই এলোমেলো তিনটি ঘর সব 1 হওয়ার সম্ভাবনা প্রায় (0.7)3 = 0.343. scratch-এ 100..999-এর না-ঢোকানো ৮৯৭টি id গুনে ৩০৯টি false positive পাওয়া গেছে, 309/897 ≈ 0.344. খেলনা filter-টি তিন item-এর জন্যও খুব ছোট।
উপপাদ্য ৭.১ (Bloom filter-এর false positive হার)

m-bit filter-এ k-টি hash দিয়ে n-টি item রাখা হলো; ধরি প্রতিটি hash মান স্বাধীন ও সমবণ্টিত। তাহলে একটি নির্দিষ্ট bit 0 থাকার সম্ভাবনা (1 − 1/m)kn ≈ e−kn/m, আর না-ঢোকানো একটি item-এর false positive হার প্রায়

f ≈ (1 − e−kn/m)k.

m/n স্থির রাখলে f সবচেয়ে কম হয় k = (m/n) ln 2-এ, তখন f ≈ 2−k ≈ (0.6185)m/n.

প্রমাণ (derivation):

  1. n-টি item ঢোকাতে মোট kn বার একটি bit বাছা হয়। একটি নির্দিষ্ট bit প্রতিবার বাছা না হওয়ার সম্ভাবনা 1 − 1/m, তাই kn বারেও না হওয়ার সম্ভাবনা (1 − 1/m)kn. লেমা ২.১-এ এটি ≤ e−kn/m, আর বড় m-এ প্রায় সমান। — উপপাদ্য ৪.১-এর মতোই: bit = bin, বাছাই = বল
  2. তাই প্রত্যাশিতভাবে bit-গুলোর ভগ্নাংশ p = e−kn/m শূন্য। না-ঢোকানো x-এর k-টি অবস্থান এলোমেলো; সবগুলো 1 হওয়ার সম্ভাবনা প্রায় (1 − p)k. — আসন্নমান: এখানে bit-গুলোকে স্বাধীন ধরা হচ্ছে, যা পুরো সত্য নয়
  3. সেরা k: kn/m = −ln p, তাই k = −(m/n) ln p, আর ln f = k ln(1 − p) = −(m/n) · ln p · ln(1 − p). — লগারিদম নেওয়া
  4. ln p · ln(1 − p) রাশিটি p আর 1 − p অদলবদল করলে বদলায় না, আর (0, 1)-এ এর সবচেয়ে বড় মান p = 1/2-এ (প্রতিসাম্য আর একটি সর্বোচ্চ বিন্দু; derivative দিয়ে যাচাই করা যায়)। ln f সবচেয়ে ছোট তখনই। — প্রতিসাম্য
  5. p = 1/2 মানে e−kn/m = 1/2, অর্থাৎ k = (m/n) ln 2, আর f = (1/2)k = (1/2)(m/n) ln 2 ≈ (0.6185)m/n. ∎

সেরা অবস্থায় অর্ধেক bit 1, অর্ধেক 0: filter-টি সবচেয়ে বেশি “তথ্য” বহন করে। ব্যবহারিক সূত্র: false positive হার f চাইলে প্রতি item-এ m/n = −ln f / (ln 2)2 bit লাগে, f = 1% হলে প্রায় 9.59 bit আর k ≈ 7. লক্ষ করুন, এটি item-এর আকারের ওপর নির্ভর করে না; ৩০ অক্ষরের coupon code বা ৩০০ অক্ষরের URL, দুটোতেই প্রায় ১০ bit. খেলনা filter-এ (ধাপ ৮) সূত্রের হিসাব (1 − e−0.9)3 ≈ 0.209, কিন্তু সেখানে সত্যিকারের ভরা bit ৭টি, প্রত্যাশিত প্রায় ৬.১-এর চেয়ে বেশি, তাই আসল হার 0.344. সূত্রটি গড় filter-এর কথা বলে; একটি নির্দিষ্ট filter-এর জন্য (ভরা bit-এর ভগ্নাংশ)k ভালো আন্দাজ।

এবার নিজের ডেটায় মেপে দেখা যাক। ২০০টি order id (“BG-2026-00000” ধাঁচের) একটি 2000-bit filter-এ রাখা হবে (m/n = 10), আর ৫০০০টি কখনো না-রাখা id দিয়ে false positive গোনা হবে। k-টি hash বানাতে একটি SHA-256 digest-কে দুই ভাগ করে hi = (h1 + i·h2) mod m নেওয়া হয়েছে; এই “double hashing” কৌশলে প্রায় একই হার মেলে বলে Kirsch আর Mitzenmacher দেখিয়েছিলেন বলে উল্লেখ করা হয়। এখানে কোনো এলোমেলো সংখ্যা নেই, তাই আউটপুট সবসময় একই।

import hashlib, math

def positions(key, k, m):
    """k bit positions from one SHA-256 digest (double hashing)."""
    d = hashlib.sha256(key.encode()).digest()
    h1 = int.from_bytes(d[:8], "big")
    h2 = int.from_bytes(d[8:16], "big") | 1
    return [(h1 + i * h2) % m for i in range(k)]

m, n = 2000, 200                       # bits, stored order ids
stored = [f"BG-2026-{i:05d}" for i in range(n)]
others = [f"BG-2027-{i:05d}" for i in range(5000)]   # never stored

print(" k  ones   measured  (1-e^(-kn/m))^k")
for k in (1, 2, 4, 7, 10, 14):
    bits = bytearray(m)
    for key in stored:
        for p in positions(key, k, m):
            bits[p] = 1
    assert all(all(bits[p] for p in positions(s, k, m)) for s in stored)
    fp = sum(all(bits[p] for p in positions(q, k, m)) for q in others)
    formula = (1 - math.exp(-k * n / m)) ** k
    print(f"{k:2}  {sum(bits):4}   {fp / len(others):.4f}    {formula:.4f}")

k_opt = m / n * math.log(2)
print(f"best k = (m/n) ln 2 = {k_opt:.2f}, then rate ~ "
      f"{0.5 ** k_opt:.4f}")

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

 k  ones   measured  (1-e^(-kn/m))^k
 1   187   0.0932    0.0952
 2   354   0.0318    0.0329
 4   664   0.0126    0.0118
 7   988   0.0068    0.0082
10  1236   0.0088    0.0102
14  1472   0.0144    0.0190
best k = (m/n) ln 2 = 6.93, then rate ~ 0.0082

মাপা হার সূত্রের কাছাকাছি, আর দুটোই একই আকার দেখায়: k বাড়লে প্রথমে কমে, k = 7-এর কাছে সবচেয়ে কম (মাপা 0.0068, সূত্র 0.0082), তারপর আবার বাড়ে, কারণ বেশি hash মানে বেশি bit 1. “ones” কলাম দেখুন: k = 7-এ 2000-এর মধ্যে 988টি bit 1, প্রায় ঠিক অর্ধেক, যেমন প্রমাণ বলেছিল। assert লাইনটি নিশ্চিত করে যে রাখা প্রতিটি id-তে উত্তর “হয়তো আছে”, অর্থাৎ false negative নেই। ৫০০০টি খোঁজায় মাপা হারে প্রায় ±0.001 ওঠানামা স্বাভাবিক। k = 14-এ মাপা হার সূত্রের চেয়ে বেশ কম; সূত্রটি আসন্নমান, আর একটি নির্দিষ্ট filter ও নির্দিষ্ট ডেটায় এমন পার্থক্য হতে পারে। বাস্তব system-এ Bloom filter আর তার আত্মীয়রা System Design কোর্সের পর্ব ৪৫-এ।

৮ · পরীক্ষার অঙ্ক আর তিনটি পরিচিত ভুল

বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর লিখিত পরীক্ষায় এই ধাঁচের অঙ্ক আসে। প্রতিটি সংখ্যা scratch-এ Python দিয়ে মেলানো।

অঙ্ক ১: বাজারঘর ১০,০০০টি অর্ডারকে এলোমেলো 32-bit id দেয়। অন্তত দুটি id মেলার সম্ভাবনা কত?

  1. n = 232 = 4,294,967,296, k = 10,000. k(k − 1)/2 = 49,995,000.
  2. k(k − 1)/2n = 49,995,000 / 4,294,967,296 ≈ 0.011640.
  3. উপপাদ্য ২.২: 1 − e−0.011640 ≈ 0.011573 ≤ P ≤ 0.011640. নির্ভুল মান (গুণফল দিয়ে) 0.011573, অর্থাৎ প্রায় 1.16 শতাংশ। এত ছোট সম্ভাবনায় দুই সীমা প্রায় সমান।

অঙ্ক ২: ১০০টি অর্ডার ১০০ জন রাইডারের মধ্যে এলোমেলো ভাগ হলে (ক) প্রত্যাশিত কতজন খালি হাতে থাকবেন? (খ) প্রত্যাশিত কত জোড়া অর্ডার একই রাইডারের কাছে যাবে?

  1. (ক) উপপাদ্য ৪.১: 100 · (1 − 1/100)100 = 100 · 0.99100 ≈ 36.60 জন। (100/e ≈ 36.79-এর খুব কাছে।)
  2. (খ) উপপাদ্য ৪.২: C(100, 2)/100 = 4950/100 = 49.5 জোড়া।

অঙ্ক ৩: ১০ লাখ coupon code-এর জন্য 1% false positive হারের Bloom filter-এ কত bit আর কয়টি hash লাগবে?

  1. প্রতি item-এ m/n = −ln(0.01)/(ln 2)2 = 4.6052/0.4805 ≈ 9.585 bit.
  2. m ≈ 9.585 × 106 bit, যা প্রায় 1.2 MB (৮ bit-এ এক byte). k = (m/n) ln 2 ≈ 6.64, তাই k = 7.
  3. যাচাই: m/n = 10, k = 7 নিলে (1 − e−0.7)7 ≈ 0.0082 < 0.01 ✓.

ভুল ধরুন ১: “২৩ জনে আমার জন্মদিনেও কারও জন্মদিন মেলার সম্ভাবনা অর্ধেক”

ভুল কোথায়: ২৩ জনের অর্ধেক সম্ভাবনা যেকোনো জোড়ার জন্য (253টি জোড়া)। আপনার সঙ্গে জোড়া মাত্র ২২টি, আর প্রতিজনের আপনার জন্মদিন এড়ানোর সম্ভাবনা 364/365. তাই P = 1 − (364/365)22 ≈ 0.0586, মাত্র প্রায় ৬ শতাংশ। আপনার সঙ্গে মেলার সম্ভাবনা অর্ধেক ছাড়াতে আপনি ছাড়া আরও ২৫৩ জন লাগে। hash-এর ভাষায় এটিই collision (যেকোনো জোড়া, √n) বনাম preimage (নির্দিষ্ট মান, প্রায় n) পার্থক্য।

ভুল ধরুন ২: “৪০ জনে মিলের সম্ভাবনা C(40, 2)/365 = 2.14”

ভুল কোথায়: সম্ভাবনা কখনো 1-এর বেশি হয় না। C(k, 2)/n হলো union bound, একটি উপরের সীমা, আর প্রত্যাশিত মিল-জোড়ার সংখ্যা (উপপাদ্য ৪.২)। জোড়ার ঘটনাগুলো পরস্পর বর্জনশীল নয় (তিনজনের একই জন্মদিনে তিনটি জোড়া একসঙ্গে মেলে), তাই যোগ করলে বেশি গোনা হয়। নির্ভুল উত্তর 0.8912.

ভুল ধরুন ৩: “প্রতিটি bin-এর প্রত্যাশিত load 1, তাই linearity-তে প্রত্যাশিত maximum load-ও 1”

ভুল কোথায়: linearity খাটে যোগফলে, maximum-এ নয়: E[max] ≠ max E. counterexample: ২টি বল, ২টি bin. চারটি সমান সম্ভাব্য ফলের দুটিতে দুই বল একই bin-এ (max = 2), দুটিতে আলাদা bin-এ (max = 1). E[max] = (2 + 1 + 1 + 2)/4 = 1.5 > 1. ব্লক ২-এর আউটপুটেও n = 1000-এ maximum load ৫–৬, গড় load 1 হলেও।

আরও গভীরে: m = n হলে একটি bin-এর load-এর বণ্টন প্রায় Poisson(1), আর এই “Poisson আসন্নমান” দিয়ে balls-into-bins-এর অনেক প্রশ্ন সহজে উত্তর দেওয়া যায়; একই ভাবনায় তিনজনের একই জন্মদিন (3-way collision) পেতে মোটামুটি n2/3 নমুনা লাগে। দুই বিকল্পের ধারণা থেকে cuckoo hashing এসেছে, যেখানে প্রতিটি key-এর দুটি সম্ভাব্য ঘর থাকে আর খোঁজা খারাপ ক্ষেত্রেও O(1). Bloom filter-এর সূত্রের স্বাধীনতা-অনুমানটি পুরো নির্ভুল নয় বলে পরে সূক্ষ্ম বিশ্লেষণ প্রকাশিত হয়েছে, তবে ব্যবহারিক m-এ পার্থক্য সাধারণত ছোট।

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

  • কমপক্ষে কতজন হলে একই জন্মদিনের সম্ভাবনা অর্ধেকের বেশি? ২৩ জন; p(23) ≈ 0.507.
  • 64-bit hash-এ মোটামুটি কয়টি এলোমেলো ইনপুটে collision আশা করবেন? প্রায় 232, অর্থাৎ কয়েকশো কোটি।
  • n-টি key n-টি slot-এ এলোমেলো রাখলে কত slot খালি? প্রায় n/e, অর্থাৎ ৩৭ শতাংশ।
  • Bloom filter কি false negative দেয়? না; ঢোকানো item-এর সব bit 1 থাকে, আর bit কখনো 0-তে ফেরে না।
  • Chaining-এ load factor α হলে না-থাকা key খুঁজতে গড়ে কত খরচ? Θ(1 + α): α-টি তুলনা আর একটি hash হিসাব।

৯ · AI-এর যুগে — সম্ভাবনার হিসাব যাচাই

AI সহকারী birthday problem-এর সূত্র মুখস্থের মতো বলে দেয়, কিন্তু সামান্য বদলানো প্রশ্নে প্রায়ই ভুল করে। এই বিষয়ে যে ভুলগুলো নিয়মিত দেখা যায়:

AI-এর ভুলকীভাবে ধরবেন
“যেকোনো দুজন” আর “আমার সঙ্গে কেউ” গুলিয়ে ফেলা, বা collision আর preimage-এর খরচ একই বলাপ্রশ্নে জোড়া কয়টি, নিজে গুনুন: C(k, 2) না k − 1
union bound বা প্রত্যাশিত জোড়াকে সম্ভাবনা বলা (মান 1-এর বেশি হলেও)উত্তর 0 থেকে 1-এর মধ্যে কি না দেখুন; ছোট n-এ Fraction দিয়ে নির্ভুল হিসাব চালান
“b-bit hash-এ collision খুঁজতে 2b চেষ্টা” বলাbirthday bound মনে করুন: প্রায় 2b/2; ছোট b-তে (যেমন 20 bit) simulation চালান
অনুমান ছাড়া উপপাদ্য: সমবণ্টন, স্বাধীনতা বা simple uniform hashing না বলে সূত্র দেওয়াপ্রতিটি সূত্রের মডেল-অনুমান লিখতে বলুন, আর কোনটি বাস্তবে ভাঙতে পারে ভাবুন
E[max] = max E-এর মতো linearity-র অপব্যবহার, বা Bloom filter-এর সূত্রে k-এর ঘাত বাদ২টি বল ২টি bin-এর মতো ক্ষুদ্র ক্ষেত্র হাতে গুনুন; সূত্র নিজের ডেটায় মেপে দেখুন
ভুল সাল বা বানানো উৎস (“Bloom ১৯৮০”, অস্তিত্বহীন প্রবন্ধ)বিশ্বস্ত পাঠ্যবই বা মূল প্রবন্ধে মিলিয়ে নিন

AI-এর কাছে সম্ভাবনার হিসাব চাইলে এমন কাঠামো দিন, যাতে প্রতিটি অংশ আলাদা করে যাচাই করা যায়:

নিচের প্রশ্নের সম্ভাবনা হিসাব করো: প্রশ্ন: [যেমন 50,000টি এলোমেলো 40-bit coupon code-এ অন্তত দুটি মেলার সম্ভাবনা] ১. প্রথমে মডেলটি লেখো: sample space কী, কোন অনুমান (সমবণ্টন, স্বাধীনতা) নিচ্ছ, আর বাস্তবে কোনটি ভাঙতে পারে। ২. নির্ভুল সূত্র দাও (গুণফল আকারে), তারপর আসন্নমান আর তার দিক (উপরের না নিচের সীমা) কোন অসমতা থেকে এল, বলো। ৩. সংখ্যাগুলো ধাপে ধাপে বসাও; উত্তর 0 থেকে 1-এর মধ্যে কি না দেখাও। ৪. প্রশ্নটি "যেকোনো জোড়া" না "নির্দিষ্ট একটি মান" নিয়ে, স্পষ্ট করো। ৫. নির্ভুল মান (Fraction বা log দিয়ে) আর seed দেওয়া simulation মেলানোর একটি Python কোড দাও (শুধু standard library).

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

এই পর্বের প্রায় প্রতিটি দাবি দুইভাবে যাচাই করা যায়: ছোট ক্ষেত্রে সব ফল গুনে নির্ভুল ভগ্নাংশ, আর বড় ক্ষেত্রে seed দেওয়া simulation. AI-এর উত্তর পেলে এই দুটি পরীক্ষা আগে চালান, তারপর প্রমাণের ধাপ পড়ুন।

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

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

প্র ০১বাস্তবে জন্মদিন সমানভাবে ছড়ানো নয়: কিছু মাসে জন্ম বেশি হয়। তাহলে কি ২৩ জনের হিসাব ভুল? অসম বণ্টনে মিলের সম্ভাবনা বাড়ে না কমে?

মডেলটি সরল করা, এটি ঠিক: অনেক দেশের জন্মের হিসাবে মাস বা ঋতু অনুযায়ী ওঠানামা দেখা যায়, ২৯ ফেব্রুয়ারি আছে, যমজ আছে। কিন্তু এই প্রতিটি বিচ্যুতি মিলের সম্ভাবনা বাড়ায়, কমায় না। কারণটি গণিতে প্রমাণ করা যায়: n-টি মানের ওপর যেকোনো বণ্টনের মধ্যে সমবণ্টনেই সব k-টি মান আলাদা হওয়ার সম্ভাবনা সবচেয়ে বেশি, অর্থাৎ মিলের সম্ভাবনা সবচেয়ে কম। সহজ রূপটি দুজনের জন্য দেখা যায়: দুজনের মিলের সম্ভাবনা ∑ pi2, যেখানে pi হলো দিন i-এর সম্ভাবনা। Cauchy–Schwarz অসমতা থেকে ∑ pi2 ≥ (∑ pi)2/n = 1/n, আর সমতা শুধু সব pi = 1/n হলে। কিছু দিন বেশি “ভিড়” হলে সেখানে মিলও বেশি হয়।

তাই ২৩ জনের সংখ্যাটি একটি নিরাপদ, রক্ষণশীল উত্তর: বাস্তবের অসম বণ্টনে ২৩ বা তার কম জনেই অর্ধেক ছাড়ায়। একই যুক্তি hash-এর জন্য আরও গুরুত্বপূর্ণ। একটি দুর্বল hash যদি কিছু digest বেশি দেয়, তবে collision birthday bound-এর চেয়ে আগে আসে। পর্ব ২৬-এ দেখেছিলাম, ধাপ-1024 key-গুলো k mod 1024-এ সব একটি বালতিতে পড়েছিল: চরম অসম বণ্টন, আর সঙ্গে সঙ্গে collision. তাই hash-এর গুণমানের একটি মাপ হলো আউটপুট কতটা সমবণ্টনের কাছাকাছি।

আরেকটি শিক্ষা মডেলিং নিয়ে। ভালো মডেল বাস্তবের প্রতিটি খুঁটিনাটি ধরে না, কিন্তু জানা থাকে কোন দিকে ভুল করছে। এখানে সমবণ্টন ধরে আমরা মিলের সম্ভাবনার একটি নিচের সীমা পাই, যা সিদ্ধান্ত নিতে যথেষ্ট: “অন্তত এতটা সম্ভাবনা আছে”। কোনো দাবিকে মডেল দিয়ে পরীক্ষা করলে সবসময় জিজ্ঞেস করুন, বাস্তবের বিচ্যুতি দাবিটিকে দুর্বল করে না শক্তিশালী।

মূল উপলব্ধি: সমবণ্টন মিলের সম্ভাবনাকে সবচেয়ে কম রাখে; তাই আদর্শ মডেলের birthday হিসাব বাস্তবের জন্য একটি রক্ষণশীল নিচের সীমা, আর অসম hash collision-কে আরও কাছে নিয়ে আসে।

প্র ০২MD5-এর digest 128 bit, অথচ আজ একে collision-এর বিরুদ্ধে নিরাপদ ধরা হয় না। birthday bound আর গঠনগত দুর্বলতা, এই দুই কারণ কীভাবে আলাদা? random UUID-তে collision নিয়ে চিন্তা করা লাগে কি?

128-bit digest-এ birthday bound প্রায় 264টি hash, অর্থাৎ প্রায় 1.8 × 1019. এটি বিশাল, কিন্তু বড় গণনা-সম্পদের নাগালের একেবারে বাইরে নয়। তাই কেবল দৈর্ঘ্যের কারণেই 128-bit hash-কে দীর্ঘমেয়াদি collision-নিরাপত্তার জন্য ছোট ধরা হয়। MD5-এর সমস্যা তার চেয়েও গুরুতর: ২০০৪ সালে Xiaoyun Wang ও সহকর্মীরা MD5-এর collision প্রকাশ করেন বলে জানা যায়, যা birthday bound-এর চেয়ে বহুগুণ কম কাজে পাওয়া গিয়েছিল। এটি গঠনগত দুর্বলতা: hash-এর ভেতরের গঠন কাজে লাগিয়ে এলোমেলো চেষ্টার চেয়ে অনেক দ্রুত মিল বানানো। পরে কয়েক সেকেন্ডে সাধারণ কম্পিউটারে MD5 collision বানানো সম্ভব হয় বলে প্রকাশিত হয়েছে।

তাই দুটি আলাদা প্রশ্ন আছে। (১) এলোমেলো ফাংশনের মতো আদর্শ hash-এও সাধারণ আক্রমণ কতটা সময় নেবে? উত্তর birthday bound, 2b/2, যা এই পর্বের গণিত। (২) এই নির্দিষ্ট hash কি আদর্শের মতো আচরণ করে? এর উত্তর গণিতে প্রমাণ করা যায় না, বছরের পর বছর বিশেষজ্ঞদের বিশ্লেষণে আস্থা জমে। প্রথম প্রশ্ন ঠিক করে digest কত লম্বা হওয়া উচিত; দ্বিতীয়টি ঠিক করে কোন hash ব্যবহার করা যায়। SHA-256-এর 256 bit প্রথম প্রশ্নে 2128-এর নিরাপত্তা দেয়।

এবার random UUID-এর চতুর্থ সংস্করণ (version 4) দেখা যাক। এতে 122টি এলোমেলো bit থাকে। ৫০% collision-এর জন্য দরকার প্রায় 1.1774 · 261 ≈ 2.7 × 1018টি UUID, আর কোটি কোটি UUID-তেও সম্ভাবনা নগণ্য। তবে শর্ত হলো bit-গুলো সত্যিই এলোমেলো, অর্থাৎ ভালো CSPRNG থেকে আসা। দুর্বল বা ভুলভাবে seed করা generator হলে (পর্ব ২৬-এর LCG-এর মতো) মান অনেক কম ভিন্ন হয়, আর collision গণিতের হিসাবের চেয়ে অনেক আগে আসে। প্রায়োগিক নির্দেশনা Cybersecurity কোর্সের পর্ব ০৯-এ।

মূল উপলব্ধি: birthday bound বলে আদর্শ অবস্থায় কতটা নিরাপত্তা সম্ভব; বাস্তব নিরাপত্তা তার চেয়ে কম হতে পারে, যদি hash-এর গঠন দুর্বল বা এলোমেলোতার উৎস খারাপ হয়।

প্র ০৩দুটি bin থেকে হালকাটি বাছলে maximum load এত নাটকীয়ভাবে কমে কেন? এই ধারণা বাস্তবের load balancing-এ কীভাবে কাজে লাগে?

একটি স্বজ্ঞাত ব্যাখ্যা “উচ্চতা অনুযায়ী ভিড়ের” হিসাব থেকে আসে। এক বিকল্পে i-টি বলের bin-এর ভগ্নাংশ মোটামুটি 1/i!-এর মতো কমে (Poisson বণ্টন), তাই 1/t! ≈ 1/n হলে t-এ পৌঁছানো যায়, যা log n / log log n দেয়। দুই বিকল্পে নতুন বল i + 1 উচ্চতায় উঠতে পারে শুধু যদি তার দুটি বিকল্পই ইতিমধ্যে i উচ্চতায় থাকে। ধরুন উচ্চতা ≥ i-এর bin-এর ভগ্নাংশ βi. তাহলে βi+1 মোটামুটি βi2-এর সমানুপাতিক। বর্গ করতে থাকলে ভগ্নাংশ দ্বিঘাতে কমে: 1/2, 1/4, 1/16, 1/256, 1/65536 … তাই প্রায় log2 log n ধাপেই তা 1/n-এর নিচে নামে। এক বিকল্পে কমার হার শুধু factorial-এর মতো; দুই বিকল্পে “বর্গের বর্গ”, যা অনেক দ্রুত।

এই ফলটি Azar, Broder, Karlin আর Upfal-এর প্রবন্ধে প্রমাণিত (১৯৯৪ সালে সম্মেলনে, ১৯৯৯ সালে জার্নালে প্রকাশিত বলে উল্লেখ করা হয়), আর Michael Mitzenmacher ১৯৯০-এর দশকের মাঝামাঝি তাঁর গবেষণায় এর queue আর বাস্তব system-এর রূপ বিশ্লেষণ করেন বলে জানা যায়। মূল আকর্ষণ হলো খরচ প্রায় নেই: প্রতিটি কাজের জন্য সব server-এর অবস্থা জানতে হয় না, শুধু এলোমেলো দুটির খবর নিলেই চলে। হাজার server-এর load balancer-এ সব server জিজ্ঞেস করা ধীর ও ব্যয়বহুল; দুটি জিজ্ঞেস করা সস্তা, অথচ ফল প্রায় “সবচেয়ে কম ভরা বেছে নেওয়ার” কাছাকাছি।

একই ভাবনা hash-এর নকশায়ও এসেছে। cuckoo hashing-এ প্রতিটি key-এর দুটি সম্ভাব্য ঘর, আর খুঁজতে শুধু সেই দুটি দেখতে হয়; ২০০১ সালে Pagh আর Rodler এটি প্রকাশ করেন বলে উল্লেখ করা হয়। আবার distributed system-এ key-কে server-এ ভাগ করার consistent hashing-এ (System Design কোর্সের পর্ব ১৬) অসম ভারের সমস্যা দেখা দেয়, আর virtual node বা একাধিক বিকল্পের মতো কৌশল সেখানে ভার সমান করতে সাহায্য করে।

মূল উপলব্ধি: সামান্য তথ্য (দুটি বিকল্পের তুলনা) এলোমেলো বণ্টনের সবচেয়ে খারাপ অংশকে দ্বিঘাত হারে কমিয়ে দেয়; একটি থেকে দুটিতে যাওয়াই বড় লাফ।

প্র ০৪Bloom filter কোনো item মুছতে দেয় না, আর মাঝে মাঝে ভুল “হ্যাঁ” বলে। তবু এটি এত জনপ্রিয় কেন? Burton Bloom-এর মূল ভাবনা কী ছিল?

Burton H. Bloom ১৯৭০ সালে Communications of the ACM-এ “space/time trade-off” নিয়ে একটি প্রবন্ধে এই কাঠামো প্রকাশ করেন বলে সাধারণত উল্লেখ করা হয়। তাঁর মূল কথা ছিল: অনেক প্রয়োগে নির্ভুল উত্তরের দরকার নেই, যদি ভুল হয় শুধু এক দিকে আর তা নিয়ন্ত্রিত হারে। উদাহরণ হিসেবে তিনি শব্দ ভাঙার (hyphenation) একটি অভিধানের কথা বলেছিলেন বলে জানা যায়: বেশিরভাগ শব্দ সাধারণ নিয়মে ভাঙে, অল্প কিছু ব্যতিক্রম ডিস্কে রাখা। filter “নিশ্চিত ব্যতিক্রম নয়” বললে ডিস্কে যেতে হয় না, আর মাঝে মাঝে অপ্রয়োজনে ডিস্কে যাওয়া খুব সামান্য খরচ।

এই ধাঁচটিই আজও এর জনপ্রিয়তার কারণ। অনেক database-এ ডিস্কের প্রতিটি ফাইলের সঙ্গে একটি Bloom filter রাখা হয়, যাতে খোঁজা key যে ফাইলে নিশ্চিত নেই, সেটি পড়তেই না হয়। web cache, নেটওয়ার্ক যন্ত্র আর spell checker-এ একই কৌশল দেখা যায়। প্রতি item-এ প্রায় ১০ bit-এ 1% ভুল, আর item-এর আকার যত বড়ই হোক, এই অনুপাত বদলায় না, যা উপপাদ্য ৭.১ থেকে এসেছে। সম্পূর্ণ সেট রাখলে প্রতি item-এ অনেক বেশি জায়গা লাগত।

মুছতে না পারার কারণ গাণিতিক: একটি bit একাধিক item-এর হতে পারে, তাই একটি item মুছতে bit 0 করলে অন্য item-এর জন্য false negative তৈরি হয়, যা Bloom filter-এর একমাত্র নিশ্চয়তা ভেঙে দেয়। সমাধান হিসেবে counting Bloom filter (প্রতিটি ঘরে bit-এর বদলে ছোট counter) ২০০০ সালের দিকে প্রস্তাবিত হয়েছে বলে জানা যায়, আর পরে cuckoo filter-এর মতো বিকল্প এসেছে, যা মোছা সমর্থন করে। প্রতিটির নিজস্ব জায়গা-ভুল বিনিময় আছে। বাস্তব ব্যবহার System Design কোর্সের পর্ব ৪৫-এ।

মূল উপলব্ধি: এক দিকের, নিয়ন্ত্রিত ভুল মেনে নিলে জায়গা নাটকীয়ভাবে কমে; আর সেই ভুলের হার balls-into-bins-এর একটি সরল হিসাব থেকে আগেই জানা যায়।

প্র ০৫“Hash table গড়ে O(1)” কথাটি একটি সম্ভাবনার অনুমানের ওপর দাঁড়িয়ে। কেউ যদি ইচ্ছা করে খারাপ key পাঠান, তাহলে কী হয়? programming language-গুলো এর জবাবে কী করেছে?

উপপাদ্য ৬.১-এর 1 + α খরচ ধরে নেয় key-রা এলোমেলো slot-এ যায়। কিন্তু hash function যদি নির্দিষ্ট আর সবার জানা হয়, তবে একজন প্রতিপক্ষ আগে থেকে হাজারো key বানাতে পারেন যাদের hash একই slot দেয়। তখন একটি তালিকায় m-টি key, প্রতিটি নতুন key ঢোকাতে পুরো তালিকা দেখতে হয়, আর m-টি key ঢোকাতে মোট সময় m2-এর মতো। একটি web server যদি অনুরোধের parameter একটি hash table-এ রাখে, তবে ছোট একটি অনুরোধেই CPU দীর্ঘক্ষণ ব্যস্ত থাকতে পারে। এই ধরনের আক্রমণ hash flooding নামে পরিচিত।

২০১১ সালের শেষে একটি নিরাপত্তা সম্মেলনে গবেষকেরা দেখান যে তখনকার অনেক জনপ্রিয় web প্রযুক্তি এভাবে আক্রান্ত হতে পারে, বলে উল্লেখ করা হয়। জবাবে বেশ কয়েকটি ভাষা তাদের string hash-এ একটি গোপন, এলোমেলো seed যোগ করে, যা প্রতিবার প্রোগ্রাম শুরুতে নতুন বাছা হয়। যেমন Python 3.3 থেকে string hash randomization স্বাভাবিকভাবে চালু, আর পরে Python SipHash নামের একটি keyed hash গ্রহণ করে (Aumasson আর Bernstein-এর নকশা) বলে জানা যায়। এটি ঠিক universal hashing-এর ভাবনা: h বাছাই এলোমেলো, প্রতিপক্ষ জানেন না কোন key কোথায় যাবে।

গণিতের দিক থেকে মূল বদলটি হলো এলোমেলোতার উৎস। উপপাদ্য ৬.১-এ এলোমেলোতা ছিল ডেটায়, তাই প্রতিপক্ষ ডেটা বেছে নিয়ে নিশ্চয়তা ভাঙতে পারতেন। উপপাদ্য ৬.২-এ এলোমেলোতা আমাদের হাতে, আর নিশ্চয়তা প্রতিটি key-সেটের জন্য। Carter আর Wegman ১৯৭০-এর দশকের শেষভাগে যে তাত্ত্বিক ধারণা দিয়েছিলেন, কয়েক দশক পরে তা বাস্তব নিরাপত্তার সমাধান হয়ে উঠল। একই ভাবনা randomized algorithm-এর মূলে, যা পর্ব ৪৩ “Probabilistic method আর randomized algorithm”-এর বিষয়, আর কোডের দিক DSA কোর্সের পর্ব ৬১-এ।

মূল উপলব্ধি: “গড়ে দ্রুত” নিশ্চয়তা কার ওপর নির্ভর করে, তা জানতে হয়; এলোমেলোতা ডেটা থেকে সরিয়ে algorithm-এর ভেতরে আনলে প্রতিপক্ষের সামনেও নিশ্চয়তা টিকে থাকে।

অনুশীলন

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

  1. প্রমাণ: m-টি key একটি hash function দিয়ে n-টি slot-এ যায়। ধরুন শুধু এটুকু জানা: যেকোনো দুটি আলাদা key-এর একই slot-এ যাওয়ার সম্ভাবনা ঠিক 1/n (পুরো স্বাধীনতা জানা নেই)। প্রমাণ করুন, একই slot-এ পড়া key-জোড়ার প্রত্যাশিত সংখ্যা C(m, 2)/n. তারপর দেখান যে m ≤ √n হলে কোনো collision না হওয়ার সম্ভাবনা অন্তত 1/2 (সংকেত: অঋণাত্মক পূর্ণসংখ্যা চলক Y-এর জন্য P(Y ≥ 1) ≤ E[Y]).
    1. প্রতিটি জোড়া {a, b}-এর জন্য Jab = 1 যদি h(a) = h(b). তাহলে collision-জোড়া Y = ∑ Jab, জোড়া C(m, 2)টি। — indicator
    2. E[Jab] = P(h(a) = h(b)) = 1/n, শর্ত অনুযায়ী। — indicator-এর প্রত্যাশা
    3. linearity-তে E[Y] = C(m, 2)/n. linearity-র জন্য কোনো স্বাধীনতা লাগে না, শুধু প্রতিটি জোড়ার সম্ভাবনা লেগেছে। ∎
    4. সংকেতের কারণ: Y পূর্ণসংখ্যা আর অঋণাত্মক, তাই সবসময় Y ≥ I{Y≥1}; দুই পাশের প্রত্যাশা নিলে E[Y] ≥ P(Y ≥ 1). — indicator-এর প্রত্যাশা
    5. m ≤ √n হলে P(Y ≥ 1) ≤ m(m − 1)/2n < m2/2n ≤ n/2n = 1/2. তাই P(কোনো collision নেই) = 1 − P(Y ≥ 1) > 1/2. ∎

    লক্ষ করুন, এই যুক্তিতে শুধু জোড়ায় জোড়ায় সম্ভাবনা লেগেছে, তাই এটি universal hashing-এও খাটে। পুরো স্বাধীনতা থাকলে উপপাদ্য ২.২-এর মতো আরও সূক্ষ্ম হিসাব সম্ভব, যা প্রায় 1.18√n পর্যন্ত যায়।

  2. হিসাব: (ক) একটি ক্লাসের ৩০ জনের মধ্যে অন্তত দুজনের একই জন্মদিনের সম্ভাবনা কত (৩৬৫ দিন, সমবণ্টন)? (খ) ৫০টি key ১০০০টি slot-এ এলোমেলো রাখলে প্রত্যাশিত collision-জোড়া কত? (গ) 2,000,000-bit Bloom filter-এ ২,০০,০০০টি item, k = 7: আসন্ন false positive হার কত?

    (ক) নির্ভুল সূত্রে 1 − ∏i=029(1 − i/365) ≈ 0.7063, অর্থাৎ প্রায় ৭১ শতাংশ। আসন্নমান 1 − e−435/365 ≈ 0.6963.

    (খ) C(50, 2)/1000 = 1225/1000 = 1.225 জোড়া।

    (গ) m/n = 10, kn/m = 0.7, তাই (1 − e−0.7)7 ≈ 0.0082, অর্থাৎ প্রায় 0.82 শতাংশ। সব সংখ্যা scratch-এ যাচাই করা।

  3. আউটপুট অনুমান: কোড না চালিয়ে বলুন কী ছাপা হবে। তারপর যেকোনো Python-এ চালিয়ে মেলান।
    from fractions import Fraction
    p = Fraction(1)
    for i in range(4):
        p *= Fraction(6 - i, 6)
    print(p, 1 - p)
    5/18 13/18

    এটি চারটি ছক্কার জন্মদিন-সমস্যা: n = 6, k = 4. সবার মান আলাদা হওয়ার সম্ভাবনা (6/6)(5/6)(4/6)(3/6) = 360/1296, যা লঘিষ্ঠ আকারে 5/18. তাই অন্তত দুটি ছক্কায় একই সংখ্যা ওঠার সম্ভাবনা 13/18 ≈ 0.722. মাত্র চারটি ছক্কাতেই মিল বেশি সম্ভাব্য, কারণ জোড়া C(4, 2) = 6টি আর প্রতিটির মিলের সম্ভাবনা 1/6. Fraction স্বয়ংক্রিয়ভাবে লঘিষ্ঠ আকার দেখায়।

  4. প্রমাণের ভুল ধরুন: একজন লিখলেন: “Bloom filter-এ n-টি item, k-টি hash, m-টি bit. একটি না-ঢোকানো item-এর প্রতিটি অবস্থান 1 হওয়ার সম্ভাবনা kn/m, কারণ মোট kn বার bit বসানো হয়েছে m-টি ঘরে। তাই false positive হার (kn/m)k. k বাড়ালে kn/m বাড়ে, তাই k = 1 সবসময় সেরা।” ভুলগুলো কোথায়? m/n = 10-এ সঠিক সূত্রে k = 1 আর k = 7-এর হার তুলনা করুন।
    • ভুল ১: kn/m হলো একটি bit প্রত্যাশিতভাবে কতবার বাছা হয়েছে, bit 1 হওয়ার সম্ভাবনা নয়। একই bit একাধিকবার বাছা হতে পারে, তাই এটি বেশি গোনে, আর kn > m হলে মানটি 1 ছাড়ায়। সঠিক: P(bit 1) = 1 − (1 − 1/m)kn ≈ 1 − e−kn/m (উপপাদ্য ৪.১-এর complement থেকে)।
    • ভুল ২: “k বাড়লে bit 1-এর সম্ভাবনা বাড়ে” সত্য, কিন্তু একই সঙ্গে ঘাত k বাড়ে, আর বেশি bit মেলানো কঠিন হয়। দুই প্রভাবের ভারসাম্যে সেরা k = (m/n) ln 2.
    • তুলনা: m/n = 10-এ k = 1: 1 − e−0.1 ≈ 0.0952; k = 7: (1 − e−0.7)7 ≈ 0.0082. k = 7 প্রায় ১২ গুণ ভালো, যা ব্লক ৩-এর মাপা মানের (0.0932 আর 0.0068) সঙ্গে মেলে।

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

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

ক্রম“k-টি মানে মিলের সম্ভাবনা ≥ 1 − e−k(k−1)/2n”, এর প্রমাণের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. complement নেওয়া: P(মিল) = 1 − P(সব আলাদা)
  2. গণনা করে P(সব আলাদা) = ∏i=1k−1 (1 − i/n) লেখা
  3. প্রতিটি গুণনীয়কে 1 − x ≤ e−x বসানো
  4. ঘাতগুলো যোগ করে 1 + 2 + ⋯ + (k − 1) = k(k − 1)/2 পাওয়া
  5. সিদ্ধান্ত: P(মিল) ≥ 1 − e−k(k−1)/2n

বছরে ৩৬৫ দিন, প্রত্যেকের জন্মদিন সমান সম্ভাবনায় আর স্বাধীনভাবে। কমপক্ষে কতজন হলে অন্তত দুজনের একই জন্মদিনের সম্ভাবনা অর্ধেকের বেশি হয়?

  • 22
  • 23
  • 183
  • 366

একটি hash function-এর digest 64 bit, আর এটি এলোমেলো ফাংশনের মতো আচরণ করে। মোটামুটি কয়টি এলোমেলো ইনপুটে একটি collision পাওয়ার ভালো সম্ভাবনা থাকে?

  • প্রায় 64টি
  • প্রায় 264টি
  • প্রায় 232টি
  • প্রায় 263টি

১০০০টি বল ১০০০টি bin-এ স্বাধীন ও সমান সম্ভাবনায় ফেলা হলো। প্রত্যাশিত খালি bin প্রায় কত?

  • প্রায় 368
  • 0
  • 500
  • প্রায় 135

একটি Bloom filter খোঁজা item x-এর জন্য উত্তর দিল “নেই” (x-এর একটি অবস্থানের bit শূন্য)। এ থেকে কী বলা যায়?

  • x হয়তো আছে, নিশ্চিত হতে ডিস্ক দেখতে হবে
  • x নেই, তবে false positive হারের সমান সম্ভাবনায় ভুল
  • x আগে ছিল, পরে মুছে ফেলা হয়েছে
  • x নিশ্চিতভাবে কখনো ঢোকানো হয়নি

একটি Bloom filter-এ প্রতি item-এ ১০টি করে bit, অর্থাৎ m/n = 10. false positive হার সবচেয়ে কম রাখতে hash function-এর সংখ্যা k প্রায় কত হওয়া উচিত?

  • 1
  • 7
  • 10
  • 14

Separate chaining-এর hash table-এ ৩০০টি key আর ১০০টি slot, simple uniform hashing ধরে নেওয়া। টেবিলে নেই এমন একটি key খুঁজতে প্রত্যাশিতভাবে কয়টি key-এর সঙ্গে তুলনা করতে হবে?

  • 1
  • 150
  • 3
  • 2.5

শব্দকোষ

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

মনে রাখুন

  1. জন্মদিন: P(সব আলাদা) = ∏i=0k−1(1 − i/n); n = 365-এ ২৩ জনে মিলের সম্ভাবনা 0.5073. “আমার সঙ্গে মেলা” আলাদা প্রশ্ন: ২২ জনে মাত্র 0.0586.
  2. Birthday bound: 1 − x ≤ e−x থেকে 1 − e−k(k−1)/2n ≤ P(মিল) ≤ C(k, 2)/n; অর্ধেক সম্ভাবনা প্রায় 1.1774√n-এ। b-bit hash-এ collision প্রায় 2b/2-এ।
  3. Balls-into-bins: indicator আর linearity দিয়ে প্রত্যাশিত খালি bin n(1 − 1/n)m ≈ n e−m/n, collision-জোড়া C(m, 2)/n. maximum load Θ(log n / log log n); দুই বিকল্পে প্রায় ln ln n / ln 2.
  4. Hash table: simple uniform hashing-এ chaining-এর খরচ 1 + α (না-থাকা) আর 1 + (m − 1)/2n (থাকা); universal hashing একই নিশ্চয়তা যেকোনো key-সেটে দেয়।
  5. Bloom filter: false negative নেই; false positive প্রায় (1 − e−kn/m)k, সেরা k = (m/n) ln 2, 1% হারে প্রতি item-এ প্রায় 9.6 bit.

পরের পর্বে: পর্ব ৪৩ “Probabilistic method আর randomized algorithm”: Las Vegas আর Monte Carlo algorithm, Freivalds-এর matrix যাচাই, Karger-এর min-cut, আর এলোমেলো বস্তুর অস্তিত্ব দেখিয়ে Ramsey সংখ্যার নিচের সীমা প্রমাণের মতো সুন্দর কৌশল আসবে।

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

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