Foundationপ্রথম নীতি থেকে
LEVEL 0লেসন ১৪/১৬কঠিন১ ঘণ্টা ৫ মিনিট

Number Systems ও Modular Arithmetic — ঘড়ির গণিত

Number Systems and Modular Arithmetic

Base conversion, GCD, congruence আর modular inverse — যে গণিতটা hex dump, hash table, checksum, two's complement আর RSA-কে একসাথে ব্যাখ্যা করে।

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

  • যেকোনো base-এর মধ্যে সংখ্যা রূপান্তর করতে পারবেন এবং hex কেন binary-র সংক্ষিপ্ত রূপ সেটা ব্যাখ্যা করতে পারবেন
  • Extended Euclid চালিয়ে GCD ও Bézout সহগ বের করতে পারবেন
  • একটা modular inverse কখন থাকে সেটা নির্ণয় করে সেটা হিসাব করতে পারবেন
  • Binary exponentiation দিয়ে বড় modular power দক্ষভাবে বের করতে পারবেন
  • একটা বাস্তব bug-কে (modular bias, sequence wraparound, hash clustering) modular arithmetic-এর ভাষায় নির্ণয় ও সমাধান করতে পারবেন

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

আগে এটা বুঝি

একটা ঘড়ির দিকে তাকান। ৯টা বাজে, আপনি ৫ ঘণ্টা যোগ করলেন। উত্তর ১৪ নয় —

আপনি সারাজীবন modular arithmetic করেছেন, শুধু নাম জানতেন না।

এবার এই ছয়টা জিনিসের দিকে তাকান:

  • hexdump -এ যে 4f 6b 0a দেখেন
  • CSS-এ যে #ff6b35 লেখেন
  • একটা credit card নম্বর টাইপ ভুল হলে ফর্ম যে সাথে সাথে ধরে ফেলে
  • int8_t x = 127; x++; করলে যে -128 হয়ে যায়
  • একটা ring buffer-এ idx = (idx + 1) % capacity
  • HTTPS handshake-এ যে key exchange হয়

ছয়টাই একই গণিত — positional notation আর “ভাগশেষ নিয়ে হিসাব”।

এই লেসনটা Level 0-এর সবচেয়ে সরাসরি প্রয়োগমুখী অধ্যায়। এখানে যা শিখবেন, তার প্রায় প্রতিটা লাইন Level 1-এ (bit ও representation), Level 7-এ (networking) আর Level 12-এ (security) আক্ষরিকভাবে আবার আসবে।

আর একটা কথা: এখানে কোনো নতুন প্রমাণ-কৌশল লাগবে না। Induction-এর লেসনে আমরা যে power function-এর correctness প্রমাণ করেছিলাম, সেটাই এখানে RSA-র ইঞ্জিন হয়ে ফিরে আসবে — একটা % m যোগ করে।

মূল ধারণা

Positional notation — সংখ্যা লেখার চুক্তি

2026 লিখলে আমরা আসলে বলি:

2×103+0×102+2×101+6×1002 \times 10^3 + 0 \times 10^2 + 2 \times 10^1 + 6 \times 10^0

গুরুত্বপূর্ণ কথাটা হলো: 10 -এ বিশেষ কিছু নেই। আমাদের দশটা আঙুল আছে, ব্যস। যেকোনো b ≥ 2 কাজ করে।

সাধারণভাবে, base b-তে অঙ্ক dₙ … d₁ d₀ মানে:

N=i=0ndibi,0di<bN = \sum_{i=0}^{n} d_i \cdot b^{i}, \qquad 0 \le d_i \lt b

শর্তটা লক্ষ্য করুন: প্রতিটা অঙ্ক 0 থেকে b−1। Base 2-এ অঙ্ক দুইটা (0,1), base 16-এ ষোলোটা (0–9, a–f)।

একই সংখ্যা, চার ভাষায়:

Base২০২৬Prefix
Decimal (10)2026
Binary (2)111111010100b
Octal (8)37520o (C-তে শুধু 0)
Hex (16)7ea0x

যাচাই করুন হাতে: 0x7ea = 7×256 + 14×16 + 10 = 1792 + 224 + 10 = 2026

Hex কেন আছে — একটামাত্র কারণ

16 = 2⁴। তাই একটা hex অঙ্ক ঠিক চারটা bit। কোনো হিসাব নেই, কোনো carry নেই — শুধু টুকরো টুকরো করে অনুবাদ।

  1 1 1 1 1 1 0 1 0 1 0        ← 11 bit
= 0111 1110 1010                ← বাঁয়ে শূন্য দিয়ে ৪-এর গুণিতক
    7    e    a                 ← প্রতি চারটার জন্য একটা অঙ্ক

তাই 0x7ea

এই ধর্মটাই hex-কে অপরিহার্য করেছে: byte = ৮ bit = ঠিক দুইটা hex অঙ্ক0x00 থেকে 0xff, কোনো ব্যতিক্রম নেই।

Decimal-এ একটা byte 0 থেকে 255 — তিন অঙ্ক, কিন্তু সব তিন-অঙ্কের সংখ্যা বৈধ নয় (299 কোনো byte নয়)। Alignment ভেঙে যায়, চোখে pattern ধরা পড়ে না।

রূপান্তর — দুইটা অ্যালগরিদম

Decimal → base b: বারবার ভাগ, ভাগশেষ উল্টো দিকে পড়ুন।

2026 ÷ 16 = 126, ভাগশেষ 10 (a)   ← সবচেয়ে কম গুরুত্বপূর্ণ অঙ্ক
 126 ÷ 16 =   7, ভাগশেষ 14 (e)
   7 ÷ 16 =   0, ভাগশেষ  7 (7)   ← সবচেয়ে বেশি গুরুত্বপূর্ণ

উল্টো দিকে পড়ুন → 7ea

Base b → decimal: Horner’s method।

0x7ea:  শুরু 0
        0 × 16 + 7  =   7
        7 × 16 + 14 = 126
      126 × 16 + 10 = 2026

Horner-এর সুবিধা: কোনো pow() লাগে না, শুধু n টা গুণ ও যোগ। এটাই প্রতিটা strtol() implementation-এর ভেতরের loop।

Divisibility আর division algorithm

b, a-কে ভাগ করে (লিখি b | a) যদি এমন একটা পূর্ণসংখ্যা k থাকে যেন a = kb

Division algorithm (নাম বিভ্রান্তিকর — এটা একটা theorem, algorithm নয়):

যেকোনো পূর্ণসংখ্যা a আর ধনাত্মক b-এর জন্য অনন্য একজোড়া q (quotient) ও r (remainder) আছে যেন

a=qb+r,0r<ba = qb + r, \qquad 0 \le r \lt b

দুইটা শব্দই গুরুত্বপূর্ণ: অস্তিত্ব আর অনন্যতা। অনন্যতাই modular arithmetic-কে সুসংজ্ঞায়িত করে।

GCD আর Euclid-এর অ্যালগরিদম

gcd(a, b) = সবচেয়ে বড় পূর্ণসংখ্যা যা দুটোকেই ভাগ করে।

Euclid-এর মূল অন্তর্দৃষ্টি (খ্রিস্টপূর্ব ৩০০ অব্দ, আজও সেরা):

gcd(a,b)=gcd(b,amodb)\gcd(a, b) = \gcd(b, a \bmod b)

কেন সত্য: a = qb + r হলে, a আর b-র যেকোনো সাধারণ ভাজক d, r = a − qb-কেও ভাগ করে। উল্টোদিকে b আর r-এর যেকোনো সাধারণ ভাজক a = qb + r-কেও ভাগ করে। দুই জোড়ার সাধারণ ভাজকের set অভিন্ন — তাই সবচেয়ে বড়টাও এক।

gcd(1071, 462)
  1071 = 2 × 462 + 147
   462 = 3 × 147 +  21
   147 = 7 ×  21 +   0    ← ভাগশেষ ০, থামুন
gcd = 21

তিন ধাপ। Trial division-এ ৪৬২ পর্যন্ত সংখ্যা যাচাই করতে হতো।

কত দ্রুত? Lamé-র theorem: ধাপ সংখ্যা সবচেয়ে ছোট সংখ্যাটার অঙ্ক সংখ্যার ৫ গুণের বেশি নয়। অর্থাৎ O(log min(a,b))

সবচেয়ে খারাপ ক্ষেত্র পরপর দুইটা Fibonacci সংখ্যাgcd(F₍ₙ₊₁₎, Fₙ)-এ প্রতিটা ধাপে quotient ঠিক 1, তাই সবচেয়ে ধীর সংকোচন। ২০৪৮-bit সংখ্যাতেও কয়েক হাজার ধাপের বেশি লাগে না।

Extended Euclid আর Bézout-এর অভেদ

Euclid শুধু gcd দেয়। কিন্তু প্রায়ই আমাদের আরো দরকার।

Bézout-এর অভেদ: যেকোনো a, b-এর জন্য এমন পূর্ণসংখ্যা x, y আছে যেন

ax+by=gcd(a,b)ax + by = \gcd(a, b)

Extended Euclid এই x, y বের করে — একই O(log n) সময়ে।

হাতে করে দেখুন — gcd(240, 46):

সামনের দিকে Euclid:

ধাপসমীকরণ
1240 = 5 × 46 + 10
246 = 4 × 10 + 6
310 = 1 × 6 + 4
46 = 1 × 4 + 2
54 = 2 × 2 + 0 → gcd = 2

এখন উল্টো দিকে প্রতিস্থাপন — প্রতিবার ভাগশেষটাকে আগের লাইন দিয়ে বদলান:

উৎসপ্রতিস্থাপন
ধাপ 42 = 6 − 1×4
ধাপ 3= 6 − 1×(10 − 1×6) = 2×6 − 1×10
ধাপ 2= 2×(46 − 4×10) − 1×10 = 2×46 − 9×10
ধাপ 1= 2×46 − 9×(240 − 5×46) = 47×46 − 9×240

240×(9)+46×47=2160+2162=2240 \times (-9) + 46 \times 47 = -2160 + 2162 = 2 \quad\blacksquare

পুনরাবৃত্তিমূলক রূপ (যেটা আমরা কোড করব) এই উল্টো যাত্রাটা সামনের দিকেই করে ফেলে, দুইটা সহগ জোড়া টেনে নিয়ে:

(x,y)(y,  xqy)(x, y) \leftarrow (y',\; x' - q \cdot y')

Congruence — সমতার একটা শিথিল সংস্করণ

ab(modm)    m(ab)a \equiv b \pmod{m} \iff m \mid (a - b)

পড়ুন: “a আর b congruent modulo m”।

সমতুল্য তিনটা সংজ্ঞা (একটা বাছুন, যেটা হাতের কাজে সুবিধা):

  1. m | (a − b)
  2. a mod m == b mod m
  3. a = b + km কোনো পূর্ণসংখ্যা k-এর জন্য

17 ≡ 5 (mod 12) — কারণ 12 | 12। ঘড়িতে ১৭টা মানে বিকেল ৫টা।

এটা একটা equivalence relation

Relations-এর লেসনে আমরা তিনটা শর্ত শিখেছিলাম। যাচাই করুন:

ধর্মযাচাই
Reflexivem ভাগ করে (a − a) = 0
Symmetricm যদি (a−b) ভাগ করে, (b−a) ও করে ✓
Transitive(a−b) আর (b−c) ভাগ করলে (a−c) = (a−b)+(b−c) ও ভাগ করে ✓

তাই congruence পূর্ণসংখ্যাদের partition করে ঠিক m টা equivalence class-এ:

Zm={[0],[1],,[m1]}\mathbb{Z}_m = \{[0], [1], \ldots, [m-1]\}

[r] = যাদের ভাগশেষ r, অর্থাৎ {…, r−m, r, r+m, r+2m, …}

Congruence যোগ ও গুণে টিকে থাকে

এটাই পুরো বিষয়টার প্রাণভোমরা। যদি a ≡ b আর c ≡ d (mod m):

a+cb+d,acbd,acbd(modm)a + c \equiv b + d, \qquad a - c \equiv b - d, \qquad ac \equiv bd \pmod{m}

প্রমাণ (গুণের জন্য). a = b + km, c = d + lm ধরুন।

ac=(b+km)(d+lm)=bd+m(bl+kd+klm)ac = (b+km)(d+lm) = bd + m(bl + kd + klm)

দ্বিতীয় পদটা m-এর গুণিতক, তাই ac ≡ bd \pmod m \quad\blacksquare

ব্যবহারিক ফল: আপনি যেকোনো সময় mod নিতে পারেন।

# এই দুইটা একই উত্তর দেয় —
(123456789 * 987654321) % 1000
(123456789 % 1000) * (987654321 % 1000) % 1000
# কিন্তু দ্বিতীয়টায় সংখ্যাগুলো কখনো ৬ অঙ্কের বেশি হয় না

বড় সংখ্যার প্রতিটা algorithm এই ধর্মের উপর দাঁড়িয়ে — মাঝপথে mod নিয়ে সংখ্যা ছোট রাখা যায়, উত্তর বদলায় না। Overflow এড়ানোর আসল কৌশল এটাই।

Modular inverse — কখন ভাগ করা যায়

Modular arithmetic-এ “ভাগ” বলে কিছু নেই। যা আছে তা হলো inverse দিয়ে গুণ

a-র modular inverse mod m হলো সেই x যেন:

ax1(modm)a \cdot x \equiv 1 \pmod m

লিখি a⁻¹ mod m

অস্তিত্বের শর্ত: a⁻¹ mod m আছে যদি এবং কেবল যদি gcd(a, m) = 1

কেন — দুই দিকেই:

(⇐) gcd(a,m) = 1 হলে Bézout দেয় ax + my = 1। দুই পাশে mod m নিন: my ≡ 0, তাই ax ≡ 1 (mod m)x-ই inverse।

(⇒) ax ≡ 1 (mod m) হলে ax − 1 = km, অর্থাৎ ax − km = 1d = gcd(a,m) বাঁ পাশের দুটো পদকেই ভাগ করে, তাই d | 1, তাই d = 1 \quad\blacksquare

তাই extended Euclid = modular inverse খোঁজার যন্ত্র।

একটা করে দেখি — 17⁻¹ mod 43:

ধাপEuclid
143 = 2 × 17 + 9
217 = 1 × 9 + 8
39 = 1 × 8 + 1
48 = 8 × 1 + 0 → gcd = 1, তাই inverse আছে

উল্টো:

1 = 9 − 1×8
  = 9 − 1×(17 − 1×9)  = 2×9 − 1×17
  = 2×(43 − 2×17) − 17 = 2×43 − 5×17

তাই −5 × 17 ≡ 1 (mod 43), অর্থাৎ

171538(mod43)17^{-1} \equiv -5 \equiv 38 \pmod{43}

যাচাই: 17 × 38 = 646 = 15 × 43 + 1

Fermat আর Euler — শর্টকাট দুইটা

Fermat’s little theorem: p prime আর p ∤ a হলে ap11(modp)a^{p-1} \equiv 1 \pmod p

কেন বিশ্বাসযোগ্য (স্বজ্ঞা, পূর্ণ প্রমাণ নয়): a, 2a, 3a, …, (p−1)a সংখ্যাগুলো mod p নিলে 1, 2, …, p−1-এরই একটা পুনর্বিন্যাস হয় (কারণ a invertible, তাই গুণটা একটা bijection — functions-এর লেসনের ভাষায়)। দুই দিকের গুণফল সমান করলে:

ap1(p1)!(p1)!(modp)a^{p-1} (p-1)! \equiv (p-1)! \pmod p

(p−1)! prime p-এর সাথে coprime, তাই কাটা যায় \quad\blacksquare

তাৎক্ষণিক প্রয়োগ: a⁻¹ ≡ a^(p−2) (mod p)। Extended Euclid ছাড়াই inverse — শুধু একটা modular exponentiation।

Euler’s theorem (সাধারণীকরণ): gcd(a, n) = 1 হলে aφ(n)1(modn)a^{\varphi(n)} \equiv 1 \pmod n

φ(n) = Euler’s totient = 1..n-এর মধ্যে কতগুলো সংখ্যা n-এর সাথে coprime।

nφ(n)কেন
p (prime)p − 1সব ছোট সংখ্যাই coprime
p^kp^k − p^(k−1)p-এর গুণিতকগুলো বাদ
pq (p ≠ q prime)(p−1)(q−1)multiplicative
12 = 2²·34{1, 5, 7, 11}

n prime হলে φ(n) = n−1, তাই Euler → Fermat। একটাই theorem।

কেন এটা RSA-র হৃদয়: exponent-গুলো mod φ(n)-এ চলে। আপনি যদি φ(n) জানেন, তবেই decryption exponent বানাতে পারেন। আর φ(n) = (p−1)(q−1) জানতে হলে p, q জানতে হবে — অর্থাৎ n factor করতে হবে।

Chinese Remainder Theorem

খ্রিস্টীয় তৃতীয় শতকে সুন জু-র ধাঁধা:

এমন একটা সংখ্যা যার ৩ দিয়ে ভাগশেষ ২, ৫ দিয়ে ৩, ৭ দিয়ে ২ — সংখ্যাটা কী?

CRT: m₁, m₂, …, mₖ জোড়ায় জোড়ায় coprime হলে, সমীকরণগুলো xai(modmi)x \equiv a_i \pmod{m_i} -এর একটা অনন্য সমাধান আছে mod M = m₁m₂⋯mₖ

স্বজ্ঞাটা গোনার: mod m₁-এ m₁ টা সম্ভাবনা, mod m₂-এ m₂ টা — coprime হওয়ায় জোড়াগুলো স্বাধীন, তাই মোট m₁m₂ টা সম্ভাব্য জোড়া। আর mod m₁m₂-এ ঠিক m₁m₂ টা অবশিষ্ট। সংখ্যায় সংখ্যায় মিলছে — তাই mapping টা একটা bijection

CRT আসলে বলছে: ℤ₁₀₅ ≅ ℤ₃ × ℤ₅ × ℤ₇। একটা বড় সংখ্যাকে কয়েকটা ছোট ভাগশেষে ভেঙে ফেলা যায়, আর দুইদিকেই যাওয়া যায়।

সুন জু-র ধাঁধা সমাধান করি:

M = 3 × 5 × 7 = 105

imᵢaᵢMᵢ = M/mᵢMᵢ⁻¹ mod mᵢপদ aᵢ Mᵢ Mᵢ⁻¹
1323535 ≡ 2, 2⁻¹ = 22 × 35 × 2 = 140
2532121 ≡ 1, 1⁻¹ = 13 × 21 × 1 = 63
3721515 ≡ 1, 1⁻¹ = 12 × 15 × 1 = 30

x140+63+30=2332332(105)=23(mod105)x \equiv 140 + 63 + 30 = 233 \equiv 233 - 2(105) = \mathbf{23} \pmod{105}

যাচাই: 23 = 7×3 + 2 ✓, 23 = 4×5 + 3 ✓, 23 = 3×7 + 2

নির্মাণটা কেন কাজ করে: প্রতিটা পদ aᵢ Mᵢ Mᵢ⁻¹ নিজের modulus-এ aᵢ দেয় (কারণ Mᵢ Mᵢ⁻¹ ≡ 1), আর বাকি সব modulus-এ শূন্য (কারণ Mᵢ-তে সেই factor আছে)। তাই পদগুলো একে অপরের ক্ষতি করে না — ঠিক basis vector-এর মতো।

Prime আর primality testing

Prime = ১-এর চেয়ে বড় এমন সংখ্যা যার ভাজক শুধু ১ আর নিজে।

Prime গুলোই পূর্ণসংখ্যার পরমাণু — arithmetic-এর মৌলিক উপপাদ্য বলে প্রতিটা n > 1 অনন্যভাবে prime-এর গুণফলে ভাঙে।

Trial division

def is_prime_trial(n):
    if n \< 2: return False
    if n % 2 == 0: return n == 2
    f = 3
    while f * f <= n:
        if n % f == 0: return False
        f += 2
    return True

√n পর্যন্ত দেখাই যথেষ্ট — কারণ n = ab হলে দুটোর অন্তত একটা ≤ √n

খরচ: O(√n) ভাগ। শুনতে ভালো, কিন্তু n-এর অঙ্ক সংখ্যার হিসাবে এটা exponential। ২০৪৮-bit RSA modulus-এ √n ≈ 2¹⁰²⁴ ভাগ লাগবে — মহাবিশ্বের বয়সের চেয়ে বেশি সময়।

Miller–Rabin — সম্ভাব্যতা দিয়ে কাটিয়ে দেওয়া

Probability-র লেসনে আমরা দেখেছি একটা randomized algorithm “প্রায় নিশ্চিত” উত্তর অনেক সস্তায় দিতে পারে। Miller–Rabin তার সবচেয়ে বিখ্যাত উদাহরণ।

ভিত্তি: Fermat বলে prime p-এ a^(p−1) ≡ 1। তাই এটা না মিললে n নিশ্চিতভাবে composite।

কিন্তু শুধু Fermat যথেষ্ট নয় — Carmichael সংখ্যা (561, 1105, 1729, …) সব a-এর জন্য Fermat পাশ করে যায় যদিও composite। 561 = 3 × 11 × 17

Miller–Rabin আরেকটা শর্ত যোগ করে। n − 1 = 2^s · d লিখুন (d বিজোড়)। n prime হলে, প্রতিটা a-এর জন্য হয়:

ad1(modn)অথবাa2rd1(modn)    কোনো 0r<sa^d \equiv 1 \pmod n \qquad \text{অথবা} \qquad a^{2^r d} \equiv -1 \pmod n \;\; \text{কোনো } 0 \le r \lt s

কেন: prime modulus-এ x² ≡ 1 -এর সমাধান কেবল x ≡ ±1 (কারণ p | (x−1)(x+1), আর prime এক factor-কে ভাগ করতেই হবে)। তাই a^(n−1) থেকে বারবার বর্গমূলের দিকে নামলে 1-এ পৌঁছানোর ঠিক আগের ধাপটা −1 হতে বাধ্য। না হলে আমরা 1-এর একটা nontrivial square root পেলাম — যার মানে n composite।

ত্রুটির সম্ভাবনা: একটা random a-তে সর্বোচ্চ 1/4k টা স্বাধীন witness-এ:

P(composite-কে prime বলা)4kP(\text{composite-কে prime বলা}) \le 4^{-k}

k = 40 হলে এটা 2⁻⁸⁰ ≈ 10⁻²⁴ — আপনার RAM-এ cosmic ray দিয়ে bit flip হওয়ার সম্ভাবনার চেয়েও কম। বাস্তবে এটাই যথেষ্ট, আর OpenSSL, GnuPG, Java-র BigInteger.isProbablePrime() সবাই এটাই ব্যবহার করে।

ভেতরে কী ঘটছে

Two’s complement = arithmetic mod 2ⁿ

এটাই এই লেসনের সবচেয়ে গুরুত্বপূর্ণ বাক্য, তাই আলাদা করে বলি:

একটা n-bit unsigned integer আসলে ℤ_(2ⁿ)-এর একটা সদস্য। CPU-র যোগ, বিয়োগ ও গুণ আক্ষরিকভাবে mod 2ⁿ operation।

uint8_t নিন। 255 + 1 কী?

255+1=2560(mod256)255 + 1 = 256 \equiv 0 \pmod{256}

উত্তর 0। এটা “overflow bug” নয় — এটা সংজ্ঞা। C standard বলে unsigned arithmetic সংজ্ঞা অনুযায়ী 2ⁿ modulo wraps।

Signed-এর বেলায় two’s complement একই ring, শুধু প্রতিনিধি বাছাই আলাদা:

Bit patternmod-256 classunsigned পাঠsigned পাঠ
0000 0000[0]00
0111 1111[127]127127
1000 0000[128]128−128
1111 1111[255]255−1

[128] থেকে [255] class গুলোর জন্য আমরা ঋণাত্মক প্রতিনিধি বেছেছি: 255 ≡ −1, 254 ≡ −2, …, 128 ≡ −128

এই একটা সিদ্ধান্ত থেকে যা বেরোয়:

  1. যোগের circuit একটাই। Signed আর unsigned যোগে CPU-র একই adder — কারণ class-এর উপর যোগ একই, শুধু আমরা উত্তরটা ভিন্নভাবে পড়ি।
  2. বিয়োগ = complement + যোগ। a − b ≡ a + (2ⁿ − b), আর 2ⁿ − b = (~b) + 1। তাই আলাদা subtractor লাগে না।
  3. শূন্য একটাই। Sign-magnitude বা one’s complement-এ +0 আর −0 দুইটা থাকত। Modular দৃষ্টিতে [0] একটাই class।
  4. অসামঞ্জস্যটাও ব্যাখ্যা হয়ে যায়। −128 আছে কিন্তু +128 নেই, কারণ ২৫৬টা class-কে দুই ভাগ করলে একদিকে একটা বেশি পড়ে। তাই abs(INT_MIN) undefined behaviour — উত্তরটা representable নয়।

% কত দামি — আর কখন দাম দিতে হয় না

Integer division হলো CPU-র সবচেয়ে ধীর সাধারণ পাটিগণিত।

OperationLatency (আধুনিক x86-64, আনুমানিক)
add, sub, and, or, xor1 cycle
imul (32/64-bit)3–5 cycle
shl, shr1 cycle
div / idiv (32-bit)20–26 cycle
div / idiv (64-bit)40–100 cycle

তাই একটা hot loop-এ % থাকলে সেটাই প্রায়ই bottleneck।

তিনটা পালানোর পথ:

১. Power of two → bitmask।

n = 2^k হলে:

xmod2k=x  &  (2k1)x \bmod 2^k = x \;\&\; (2^k - 1)

কেন: 2^k দিয়ে ভাগশেষ মানে নিচের k টা bit — আর 2^k − 1 হলো ঠিক সেই k টা bit-এ ১।

x        = 1011 0110      (182)
16 - 1   = 0000 1111
x & 15   = 0000 0110      (6)
182 = 11 × 16 + 6  ✓

৪০ cycle থেকে ১ cycle। এই কারণেই ring buffer, hash table আর memory allocator-এর আকার প্রায় সবসময় power of two।

// Linux kernel-এর kfifo, DPDK-র ring, Java-র HashMap — সবাই এই pattern
idx = (idx + 1) & (capacity - 1);   // capacity অবশ্যই 2^k

২. ধ্রুবক modulus → কম্পাইলারের magic number।

x % 10 লিখলে কম্পাইলার div ব্যবহার করে না। সে 10-এর reciprocal-এর একটা fixed-point আসন্ন মান আগেই বের করে রাখে (Granlund–Montgomery পদ্ধতি) আর একটা গুণ + shift দিয়ে কাজ সারে।

# নিজে দেখুন
echo 'int f(int x){return x % 10;}' | gcc -O2 -S -x c - -o - | grep -E 'idiv|imul|sar'

আপনি imul দেখবেন, idiv নয়। কিন্তু modulus যদি runtime variable হয় তখন কম্পাইলার কিছু করতে পারে না — আসল div চলে।

৩. Modulus এড়িয়ে যান।

// ধীর
idx = (idx + 1) % n;

// দ্রুত — কারণ শর্তটা শাখা-অনুমানে প্রায় সবসময় একই
idx = idx + 1;
if (idx == n) idx = 0;

n power of two না হলেও এটা কাজ করে, আর branch predictor ৯৯.৯% ঠিক অনুমান করে (loop-এ একবারই শাখা বদলায়)।

Modular exponentiation — induction-এর power ফিরে এল

RSA-তে 65^17 mod 3233 লাগে। ১৭ বার গুণ করলে চলত, কিন্তু বাস্তবে exponent d হয় ২০৪৮ bit — অর্থাৎ প্রায় 10⁶¹⁶ বার গুণ।

Induction-এর লেসনে আমরা এই function-টার correctness প্রমাণ করেছিলাম strong induction দিয়ে:

def power(base, exp):
    if exp == 0:
        return 1
    half = power(base, exp // 2)
    if exp % 2 == 0:
        return half * half
    return half * half * base

প্রমাণের দুইটা case ছিল — e = 2m হলে b^m · b^m = b^e, আর e = 2m+1 হলে b^m · b^m · b = b^e। Termination দেখিয়েছিলাম exp // 2 \< exp থেকে।

এখন একটা মাত্র পরিবর্তন: প্রতিটা গুণের পর % m

def power_mod(base, exp, m):
    if m == 1:
        return 0
    if exp == 0:
        return 1
    half = power_mod(base, exp // 2, m)
    if exp % 2 == 0:
        return (half * half) % m
    return (half * half % m) * (base % m) % m

Correctness প্রমাণটা অপরিবর্তিত থাকে — কারণ আমরা দেখিয়েছি congruence গুণে টিকে থাকে। প্রতিটা মধ্যবর্তী % m উত্তরের equivalence class বদলায় না, শুধু প্রতিনিধিটা ছোট রাখে।

এটাই একটা প্রমাণ পুনর্ব্যবহারের নিখুঁত উদাহরণ: গঠনটা এক, তাই যুক্তিটাও এক।

খরচ: ⌊log₂ exp⌋ + 1 ধাপ, প্রতিটায় সর্বোচ্চ দুইটা গুণ। ২০৪৮-bit exponent-এ ~২০৪৮ ধাপ, ~৩০০০ গুণ। মিলিসেকেন্ডের ব্যাপার।

Hash table-এর modulus — prime কেন সাহায্য করে

bucket = h % mm কীভাবে বাছবেন?

সমস্যাটা: বাস্তব key-এর hash প্রায়ই এলোমেলো নয়। ধরুন আপনার key গুলো memory address, যা ১৬-byte aligned — অর্থাৎ সবগুলো 16-এর গুণিতক।

m = 1024 = 2¹⁰ হলে h % 1024 শুধু নিচের ১০টা bit দেখে। কিন্তু নিচের ৪টা bit সবসময় শূন্য। ফলে ১০২৪টা bucket-এর মধ্যে মাত্র ৬৪টা ব্যবহার হয় — বাকি ৯৬০টা খালি।

m = 1021 (prime) হলে? gcd(16, 1021) = 1, তাই ১৬-এর গুণিতকগুলোও সব bucket-এ ছড়িয়ে পড়ে।

সাধারণ নিয়ম: key গুলোর মধ্যে যদি d একটা সাধারণ গুণনীয়ক থাকে আর d | m, তাহলে কেবল m/d টা bucket ব্যবহার হয়। m prime হলে একমাত্র বিপদ d = m — যা অনেক কম সম্ভাব্য।

একটা modulo অপারেশন সিস্টেমের কোথায় কোথায় লুকিয়ে আছে
  1. a mod mগাণিতিক সংজ্ঞা — equivalence class
  2. ring buffer index(head + 1) % cap, বা & (cap−1)
  3. hash bucketh % num_buckets
  4. two's complement যোগCPU-র adder = mod 2ⁿ
  5. TCP sequence numbermod 2³² wraparound, PAWS
  6. CRC checksumGF(2)-তে বহুপদীর ভাগশেষ
  7. consistent hashinghash ring — mod 2³² -এর বৃত্ত
  8. RSA / Diffie–Hellmanmod বড় prime বা semiprime
  9. সময় ও তারিখসেকেন্ড mod 60, Zeller's congruence

উদাহরণ

Checksum — modular arithmetic যা প্রতিদিন আপনার ভুল ধরে

Luhn algorithm — credit card

আপনি কার্ড নম্বর টাইপ করে একটা অঙ্ক ভুল করলেন। Server-এ যাওয়ার আগেই ফর্ম লাল হয়ে গেল। কীভাবে?

নিয়ম: ডান দিক থেকে প্রতি দ্বিতীয় অঙ্ক দ্বিগুণ করুন; দ্বিগুণ করে ৯-এর বেশি হলে অঙ্ক দুইটা যোগ করুন (সমতুল্যভাবে 9 বিয়োগ করুন)। সব যোগ করে mod 10 শূন্য হলে বৈধ।

উদাহরণ — 79927398713:

অবস্থান (ডান থেকে)অঙ্কদ্বিগুণ?অবদান
03না3
11হ্যাঁ → 22
27না7
38হ্যাঁ → 16 → 77
49না9
53হ্যাঁ → 66
67না7
72হ্যাঁ → 44
89না9
99হ্যাঁ → 18 → 99
107না7

যোগফল = 70, আর 70 mod 10 = 0বৈধ

def luhn_ok(number: str) -> bool:
    total = 0
    for i, ch in enumerate(reversed(number)):
        d = int(ch)
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0

Luhn কী ধরে আর কী ধরে না:

ভুলের ধরনধরা পড়ে?
যেকোনো একটা অঙ্ক ভুলসবসময়
পাশাপাশি দুইটা অঙ্ক অদলবদলপ্রায় সবসময়
0990 অদলবদলনা — এটাই একমাত্র ফাঁক
দূরের দুইটা অঙ্ক অদলবদলনা

09 → 90-এর ফাঁকটা কেন: দ্বিগুণ করার নিয়মে 0→0 আর 9→18→9, তাই অদলবদলে যোগফল বদলায় না। ১৯৫৪ সালের একটা design, আর এই একটামাত্র দুর্বলতা তখন গ্রহণযোগ্য ধরা হয়েছিল।

ISBN-13 — ওজনসহ mod 10

ওজন 1, 3, 1, 3, … পালা করে, যোগফল mod 10 শূন্য হতে হবে।

CLRS তৃতীয় সংস্করণ: 978-0-262-03384-8

অঙ্ক :  9  7  8  0  2  6  2  0  3  3  8  4   | 8
ওজন  :  1  3  1  3  1  3  1  3  1  3  1  3
গুণফল:  9 21  8  0  2 18  2  0  3  9  8 12
যোগ  = 92
check = (10 − 92 mod 10) mod 10 = (10 − 2) mod 10 = 8   ✓

ওজনে 3 কেন? 1 হলে অদলবদল ধরা পড়ত না (ab আর ba-র যোগফল এক)। ভিন্ন ওজন দিলে স্থানটাও যোগফলে প্রভাব ফেলে।

কিন্তু 3 − 1 = 2, আর 2 × 5 ≡ 0 (mod 10) — তাই যে দুইটা অঙ্কের পার্থক্য ঠিক 5, তাদের অদলবদল ISBN-13 ধরতে পারে না। পুরনো ISBN-10 mod 11 ব্যবহার করত (তাই X অঙ্কটা ছিল, 10-এর জন্য) — prime modulus হওয়ায় সব একক-অঙ্ক ও সব অদলবদল ধরত। ১৩-এ যাওয়ার সময় EAN barcode-এর সাথে সামঞ্জস্যের জন্য এই ক্ষমতাটা বিসর্জন দেওয়া হয়েছিল।

IBAN — mod 97, সবচেয়ে শক্ত সাধারণ checksum

ব্যাংক অ্যাকাউন্টে টাকা পাঠানোর সময় একটা টাইপো ব্যয়বহুল। তাই IBAN একটা ৯৭-modulus ব্যবহার করে।

def iban_ok(iban: str) -> bool:
    s = iban.replace(" ", "").upper()
    s = s[4:] + s[:4]                      # প্রথম ৪টা অক্ষর শেষে
    digits = "".join(
        str(ord(c) - 55) if c.isalpha() else c   # A=10 … Z=35
        for c in s
    )
    return int(digits) % 97 == 1

97 prime আর যথেষ্ট বড় — তাই একটা এলোমেলো ভুল ধরা না পড়ার সম্ভাবনা প্রায় 1/97 ≈ 1%, আর সব একক-অঙ্ক ভুল ও সব অদলবদল নিশ্চিতভাবে ধরা পড়ে।

int(digits) একটা ৩০+ অঙ্কের সংখ্যা হতে পারে। C-তে সেটা ধরবে না, তাই আসল implementation টুকরো টুকরো করে mod নেয় — যা বৈধ, কারণ congruence গুণ ও যোগে টেকে:

rem = 0
for ch in digits:
    rem = (rem * 10 + int(ch)) % 97      # Horner, mod সহ

CRC — GF(2)-তে বহুপদীর ভাগশেষ

CRC checksum গুলোও ভাগশেষ, শুধু সংখ্যার বদলে বহুপদীর

একটা bit string 1101 কে বহুপদী ধরুন:

x3+x2+1x^3 + x^2 + 1

সহগগুলো {0, 1} থেকে, আর যোগ = XOR (কারণ 1 + 1 ≡ 0 mod 2, কোনো carry নেই)। এই জগতটার নাম GF(2)

CRC-32 হলো: message-এর বহুপদীকে x³² দিয়ে গুণ করে একটা নির্দিষ্ট generator polynomial দিয়ে ভাগ করে ভাগশেষ নেওয়া।

CRC-32 (Ethernet, zip, PNG) generator = 0x04C11DB7
  = x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹² + x¹¹ + x¹⁰
    + x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1

কেন XOR-ভিত্তিক ভাগ hardware-এ এত সস্তা: carry নেই, তাই পুরো ভাগটা একটা shift register আর কয়েকটা XOR gate দিয়ে হয় — প্রতি clock-এ এক bit, বা table lookup দিয়ে প্রতি clock-এ এক byte। আধুনিক x86-এ crc32 একটা একক instruction (SSE4.2)।

Generator polynomial-এর নির্বাচন গুরুত্বপূর্ণ: ভালো polynomial নিশ্চিত করে যে সব ১-bit, ২-bit ও ৩-bit ভুল ধরা পড়বে, আর ৩২ bit-এর কম যেকোনো burst error-ও। এটা coding theory-র বিষয়, Level 1-এ error detection নিয়ে বিস্তারিত।

TCP sequence number — mod 2³²-এ বাস করা

TCP-র sequence number ৩২-bit। অর্থাৎ TCP আক্ষরিকভাবে ℤ_(2³²)-এ হিসাব করে।

সমস্যা: modular arithmetic-এ “বড়” বা “ছোট”-র কোনো সংজ্ঞা নেই। [5] কি [4294967290]-এর চেয়ে বড়? প্রশ্নটাই অর্থহীন।

TCP-র সমাধান — serial number arithmetic (RFC 1982):

// ভুল — wraparound-এ ভেঙে পড়ে
if (seq_a > seq_b) { ... }

// সঠিক — পার্থক্যটা signed হিসেবে পড়ুন
if ((int32_t)(seq_a - seq_b) > 0) { ... }

বিয়োগটা mod 2³²-এ হয়, তারপর signed পাঠে [0, 2³¹) মানে “পরে”, আর [2³¹, 2³²) মানে “আগে”। অর্থাৎ আমরা ধরে নিই দুইটা sequence number কখনো 2³¹-এর বেশি দূরে থাকে না

কতটা বাস্তব এই অনুমান? 2³² byte = ৪ GiB।

Link গতিWraparound সময়
10 Mbps~৫৭ মিনিট
1 Gbps~৩৪ সেকেন্ড
10 Gbps~৩.৪ সেকেন্ড
100 Gbps~০.৩৪ সেকেন্ড

TCP-র maximum segment lifetime ধরা হয় ২ মিনিট। ১০ Gbps-এ sequence space ৩.৪ সেকেন্ডে ঘুরে যায় — অর্থাৎ একটা পুরনো, নেটওয়ার্কে আটকে থাকা packet বৈধ sequence number নিয়ে ফিরে আসতে পারে আর নতুন data-র সাথে মিশে যেতে পারে।

সমাধান — PAWS (Protect Against Wrapped Sequences, RFC 7323): প্রতিটা segment-এ একটা timestamp option যোগ করা হয়। Timestamp পিছিয়ে গেলে segment ফেলে দেওয়া হয়, sequence number যাই বলুক।

# আপনার মেশিনে চালু আছে কি না
sysctl net.ipv4.tcp_timestamps      # Linux
sysctl net.inet.tcp.rfc1323          # macOS

Level 7-এ আমরা tcpdump দিয়ে এই timestamp option চোখে দেখব।

RSA — কেন গণিতটা কাজ করে

এবার সবগুলো টুকরো এক জায়গায়: prime, φ, modular inverse, Euler’s theorem, আর fast exponentiation।

Key generation

  1. দুইটা বড় prime বাছুন: p, q (বাস্তবে ১০২৪ bit করে)
  2. n = p · q — এটাই modulus, প্রকাশ্য
  3. φ(n) = (p−1)(q−1) — এটা গোপন
  4. একটা e বাছুন যেন gcd(e, φ(n)) = 1; সাধারণত e = 65537
  5. d = e⁻¹ mod φ(n) — extended Euclid দিয়ে

Public key (n, e), private key (n, d)p, q, φ(n) মুছে ফেলা হয় (বা CRT-র জন্য গোপনে রাখা হয়)।

Encryption ও decryption

c=memodn,m=cdmodnc = m^e \bmod n, \qquad m = c^d \bmod n

দুটোই আমাদের power_mod — যার correctness ইতিমধ্যে প্রমাণিত।

কেন ফিরে আসে

ed ≡ 1 (mod φ(n)) মানে ed = 1 + kφ(n) কোনো পূর্ণসংখ্যা k-এর জন্য।

cd=(me)d=med=m1+kφ(n)=m(mφ(n))kc^d = (m^e)^d = m^{ed} = m^{1 + k\varphi(n)} = m \cdot \left(m^{\varphi(n)}\right)^{k}

Euler’s theorem বলে m^φ(n) ≡ 1 (mod n) (যখন gcd(m,n) = 1), তাই:

cdm1k=m(modn)c^d \equiv m \cdot 1^k = m \pmod n \quad\blacksquare

gcd(m, n) ≠ 1 হলেও (অর্থাৎ m, p বা q-এর গুণিতক) CRT দিয়ে আলাদাভাবে দেখানো যায় যে ফলাফল একই — তাই সব m \< n-এর জন্য কাজ করে।

একটা ছোট উদাহরণ, শেষ অঙ্ক পর্যন্ত

ধাপমান
p61
q53
n = pq3233
φ(n) = 60 × 523120
e17 (gcd(17, 3120) = 1)
d = 17⁻¹ mod 31202753

যাচাই: 17 × 2753 = 46801 = 15 × 3120 + 1

Message m = 65:

c=6517mod3233=2790c = 65^{17} \bmod 3233 = 2790 m=27902753mod3233=65m = 2790^{2753} \bmod 3233 = 65 \quad\blacksquare

2790^2753 সংখ্যাটার অঙ্ক সংখ্যা প্রায় ৯,৫০০ — কিন্তু binary exponentiation-এ মাত্র ১২টা squaring লাগে, আর কোনো মধ্যবর্তী সংখ্যা 3233²-এর বেশি হয় না।

নিরাপত্তা কীসের উপর দাঁড়িয়ে

n আর e সবাই জানে। d বের করতে হলে φ(n) লাগে, আর φ(n) = (p−1)(q−1) পেতে হলে n factor করতে হবে।

এটাই পুরো ভিত্তি। n গুণ করা সহজ (মিলিসেকেন্ড), n factor করা কঠিন (জানা সেরা algorithm — general number field sieve — sub-exponential কিন্তু বাস্তবে ২০৪৮-bit-এ অসম্ভব)।

RSA আকারআনুমানিক নিরাপত্তাঅবস্থা
512 bit১৯৯৯-এ ভাঙা হয়েছে
768 bit২০০৯-এ ভাঙা হয়েছে
829 bit২০২০-এ ভাঙা হয়েছে (RSA-250)
2048 bit~112 bitআজকের সর্বনিম্ন
3072 bit~128 bitদীর্ঘমেয়াদে সুপারিশকৃত

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

EXPERIMENT

Hex আসলে কী দেখাচ্ছে — একটা ফাইলের ভেতরে ঢুকুন

Linux / macOS (bash + Python 3)· ২০ মিনিট

ধাপ ১ — নিজের হাতে একটা ফাইল বানিয়ে তার প্রতিটা byte দেখুন।

printf 'Ok\n' > /tmp/tiny.bin
xxd /tmp/tiny.bin          # Linux; macOS-এও xxd আছে
# 00000000: 4f6b 0a                                  Ok.

তিনটা byte: 4f, 6b, 0a

'O' = 0x4f = 0100 1111 = 79   (ASCII 'O')
'k' = 0x6b = 0110 1011 = 107  (ASCII 'k')
'\n'= 0x0a = 0000 1010 = 10   (newline)

লক্ষ্য করুন প্রতিটা byte ঠিক দুইটা hex অঙ্ক — কখনো এক, কখনো তিন নয়। এই স্থিরতাই hex-এর পুরো উপযোগিতা।

ধাপ ২ — এবার একটা সংখ্যা রাখুন, আর byte order দেখুন।

python3 -c "open('/tmp/num_le.bin','wb').write((0x12345678).to_bytes(4,'little'))"
xxd /tmp/num_le.bin
# 00000000: 7856 3412                                xV4.

আমরা লিখেছি 0x12345678, কিন্তু ফাইলে দেখাচ্ছে 78 56 34 12উল্টো

এটা little-endian: সবচেয়ে কম গুরুত্বপূর্ণ byte আগে। x86, ARM (সাধারণ configuration), RISC-V — সবাই little-endian।

# big-endian-এ লিখে তুলনা করুন
python3 -c "open('/tmp/num_be.bin','wb').write((0x12345678).to_bytes(4,'big'))"
xxd /tmp/num_be.bin
# 00000000: 1234 5678                                .4Vx

Big-endian-টা পড়তে সহজ কারণ সেটা আমাদের লেখার ক্রমের সাথে মেলে। তাই network protocol গুলো (IP, TCP header) big-endian ব্যবহার করে — একে network byte order বলা হয়, আর htons()/ntohl() সেই রূপান্তর করে।

ধাপ ৩ — Python-এ base নিয়ে খেলুন।

n = 2026

print(f"{n:>8} decimal")
print(f"{bin(n):>16}   binary")
print(f"{oct(n):>16}   octal")
print(f"{hex(n):>16}   hex")
print(f"{n:0>16b}   padded binary")   # ১৬ ঘরে শূন্য দিয়ে

# উল্টো দিকে — int() যেকোনো base নেয়
assert int("11111101010", 2) == n
assert int("3752",  8) == n
assert int("7ea",  16) == n
assert int("0x7ea", 0) == n          # base 0 = prefix থেকে বুঝে নাও

# চার bit করে দলবদ্ধ করে হাতে হাতে হেক্স মিলিয়ে দেখুন
raw    = f"{n:b}"
padded = raw.rjust((len(raw) + 3) // 4 * 4, "0")
groups = [padded[i:i+4] for i in range(0, len(padded), 4)]
print(" ".join(groups), "->", " ".join(f"{int(g, 2):x}" for g in groups))
# 0111 1110 1010 -> 7 e a

ধাপ ৪ — একটা সত্যিকারের ফাইলের magic number পড়ুন।

for f in /bin/ls /etc/hosts; do
  printf "%-14s " "$f"
  head -c 8 "$f" | xxd -p
done

Linux-এ /bin/ls শুরু হবে 7f454c46 দিয়ে — যা ASCII-তে \x7f E L F, অর্থাৎ ELF executable-এর magic number। macOS-এ দেখবেন cffaedfe (Mach-O, little-endian-এ 0xfeedfacf)।

file কমান্ড আক্ষরিকভাবে এই কাজটাই করে — প্রথম কয়েকটা byte একটা database-এর সাথে মেলায়।

file /bin/ls

নিজে যাচাই করুন: PNG-র magic হলো 89504e47, GIF-এর 47494638 (GIF8), ZIP-এর 504b0304 (PK\x03\x04 — Phil Katz-এর আদ্যক্ষর)। একটা .docx বা .jar ফাইলে xxd চালান — দেখবেন সেগুলোও আসলে ZIP।

এটা কী প্রমাণ করে

Hex কোনো আলাদা 'সংখ্যা পদ্ধতি' নয় — এটা bit-এর একটা compact লেখনী, যেখানে এক অঙ্ক = চার bit। আর একই byte গুলো কীভাবে সাজানো আছে তা পড়তে গেলে endianness জানতেই হবে।

EXPERIMENT

Modular bias — যে bug টা প্রতিটা 'random' ফাংশনে লুকিয়ে থাকে

Python 3· ২০ মিনিট

সমস্যার কঙ্কাল। ধরুন আপনার RNG সমানভাবে 0..R−1 দেয়। আপনি চান 0..n−1। সহজ উপায়:

value = rng() % n

কিন্তু R যদি n দিয়ে নিঃশেষে ভাগ না যায়, তাহলে কিছু ফলাফল একটা বেশি বার ঘটার সুযোগ পায়।

R = 16, n = 10 নিন:

rng():  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
% 10 :  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
        ↑  ↑  ↑  ↑  ↑  ↑                 (এই ছয়টা দুইবার)

P(0..5) = 2/16 = 12.5%
P(6..9) = 1/16 =  6.25%     ← ঠিক অর্ধেক!

এবার measure করুন:

import random
from collections import Counter

R = 16          # RNG-র range: 0 .. 15
N = 10          # আমরা চাই 0 .. 9
TRIALS = 200_000

def raw():
    return random.randrange(R)      # নিখুঁত uniform, 0..15

# ── পদ্ধতি ১: naive modulo ──────────────────────────────────
biased = Counter(raw() % N for _ in range(TRIALS))

# ── পদ্ধতি ২: rejection sampling ────────────────────────────
LIMIT = (R // N) * N                # 16 // 10 * 10 = 10
def unbiased():
    while True:
        v = raw()
        if v \< LIMIT:               # 10..15 ফেলে দাও
            return v % N

fair = Counter(unbiased() for _ in range(TRIALS))

expected = TRIALS / N
print(f"{'মান':>4} {'naive %':>10} {'বিচ্যুতি':>10} "
      f"{'rejection':>11} {'বিচ্যুতি':>10}")
print("─" * 50)
for v in range(N):
    b, f = biased[v], fair[v]
    print(f"{v:>4} {b:>10,} {b/expected - 1:>+9.1%} "
          f"{f:>11,} {f/expected - 1:>+9.1%}")

# কত draw নষ্ট হলো?
rejected = sum(1 for _ in range(TRIALS) if raw() >= LIMIT)
print(f"\nপ্রত্যাখ্যানের হার: {rejected/TRIALS:.1%} "
      f"(তাত্ত্বিক {(R - LIMIT)/R:.1%})")

সাধারণ ফলাফল:

  মান    naive %   বিচ্যুতি   rejection   বিচ্যুতি
──────────────────────────────────────────────────
   0     25,061    +25.3%      19,940     -0.3%
   1     24,918    +24.6%      20,105     +0.5%
   2     25,143    +25.7%      19,873     -0.6%
   3     24,890    +24.5%      20,022     +0.1%
   4     25,022    +25.1%      19,996     -0.0%
   5     24,975    +24.9%      20,061     +0.3%
   6     12,516    -37.4%      20,014     +0.1%
   7     12,489    -37.6%      19,952     -0.2%
   8     12,507    -37.5%      20,033     +0.2%
   9     12,479    -37.6%      20,004     +0.0%

প্রত্যাখ্যানের হার: 37.4% (তাত্ত্বিক 37.5%)

দুই গুণ পার্থক্য। আর rejection sampling-এ বিচ্যুতি পরিসংখ্যানগত গোলমালের ভেতরেই।

ধাপ ২ — বাস্তবসম্মত range-এ কতটা খারাপ?

আসল rand()-এ R = 2³¹ বা 2³²। তখন bias কত?

def bias_ratio(R, n):
    """সবচেয়ে বেশি আর সবচেয়ে কম সম্ভাব্য ফলাফলের অনুপাত"""
    lo, hi = R // n, (R + n - 1) // n
    return hi / lo

for R_bits in (4, 8, 16, 31, 32, 64):
    R = 2 ** R_bits
    for n in (3, 6, 10, 1000, 10**6):
        print(f"R = 2^{R_bits:>2}  n = {n:>7}  "
              f"bias = {bias_ratio(R, n):.10f}")
    print()

আপনি দেখবেন R = 2³², n = 6 (একটা ছক্কা) হলে bias 1.0000000014 — সম্পূর্ণ উপেক্ষণীয়।

কিন্তু n যদি R-এর কাছাকাছি হয়, bias বিস্ফোরিত হয়:

R = 2 ** 32
for n in (2**31 + 1, 3 * 2**30, 2**32 - 1):
    print(f"n = {n:>12}  bias = {bias_ratio(R, n):.3f}")
# n =   2147483649  bias = 2.000
# n =   3221225472  bias = 2.000
# n =   4294967295  bias = 2.000

নিয়ম: n যদি R-এর তুলনায় ক্ষুদ্র হয় (n \< 2¹⁶, R = 2³²), bias পরিমাপযোগ্যও নয়। কিন্তু n বড় হলে, বা আপনি যদি cryptographic নিয়তি (key, nonce, shuffle) তৈরি করেন — তখন rejection sampling বাধ্যতামূলক।

এটা কী প্রমাণ করে

rand() % n একটা অসম distribution তৈরি করে যখন n, RNG-র range-কে ভাগ করে না — আর rejection sampling সেটা নিখুঁতভাবে ঠিক করে দেয়, কেবল একটু বেশি draw-এর বিনিময়ে।

নিজে বানান

BUILD IT

Number Theory Toolkit — extended Euclid থেকে RSA পর্যন্ত

Python · ●●●○○
  1. Extended Euclid লিখুন এবং প্রতিটা ধাপ ছাপিয়ে Bézout সহগ যাচাই করুন
  2. তার উপর modular inverse বানান, আর gcd ≠ 1 হলে স্পষ্টভাবে ব্যর্থ হন
  3. Binary exponentiation দিয়ে power_mod লিখুন — induction-এ প্রমাণিত গঠনটাই
  4. Miller–Rabin দিয়ে prime খুঁজুন, ৬৪-bit-এ deterministic witness set ব্যবহার করে
  5. সব টুকরো জোড়া দিয়ে একটা খেলনা RSA keygen / encrypt / decrypt round-trip চালান
"""
Number Theory Toolkit
এই ফাইলটা সরাসরি চালানো যায়:  python3 toolkit.py
"""
import random

# ═══════════════════════════════════════════════════════════════
#  ১. Extended Euclid  —  ax + by = gcd(a, b)
# ═══════════════════════════════════════════════════════════════

def egcd(a, b, trace=False):
    """(g, x, y) ফেরত দেয় যেন a*x + b*y == g == gcd(a, b)।

    দুইটা সহগ জোড়া সামনের দিকে টেনে নেওয়া হয়, তাই কোনো
    উল্টো-প্রতিস্থাপন লাগে না। প্রতিটা ধাপে এই invariant টা টেকে:
        old_r == a*old_s + b*old_t
            r == a*s     + b*t
    """
    old_r, r = a, b
    old_s, s = 1, 0
    old_t, t = 0, 1
    rows = []
    while r != 0:
        q = old_r // r
        rows.append((q, old_r, old_s, old_t))
        old_r, r = r, old_r - q * r
        old_s, s = s, old_s - q * s
        old_t, t = t, old_t - q * t
    if trace:
        print(f"  egcd({a}, {b})")
        print(f"  {'q':>4} {'r':>10} {'x':>8} {'y':>8}")
        for q, rr, ss, tt in rows:
            print(f"  {q:>4} {rr:>10} {ss:>8} {tt:>8}")
        print(f"  {'':>4} {old_r:>10} {old_s:>8} {old_t:>8}   (gcd ও তার সহগ)")
    assert a * old_s + b * old_t == old_r
    return old_r, old_s, old_t


# ═══════════════════════════════════════════════════════════════
#  ২. Modular inverse
# ═══════════════════════════════════════════════════════════════

def modinv(a, m):
    """a^-1 mod m, অথবা gcd(a, m) != 1 হলে ValueError।"""
    a %= m
    g, x, _ = egcd(a, m)
    if g != 1:
        raise ValueError(
            f"{a}-এর inverse নেই mod {m}: gcd = {g}, ১ হতে হবে")
    return x % m


# ═══════════════════════════════════════════════════════════════
#  ৩. Binary exponentiation  —  induction-এর power(), mod সহ
# ═══════════════════════════════════════════════════════════════

def power_mod(base, exp, m):
    """base^exp mod m, O(log exp) গুণে।

    Correctness: induction-এর লেসনে power() -এর strong-induction
    প্রমাণটাই খাটে, কারণ congruence গুণে সংরক্ষিত থাকে —
    প্রতিটা মধ্যবর্তী % m কেবল প্রতিনিধি ছোট রাখে, class নয়।
    """
    if m == 1:
        return 0
    result, base = 1, base % m
    while exp:
        if exp & 1:
            result = result * base % m
        base = base * base % m
        exp >>= 1
    return result


# ═══════════════════════════════════════════════════════════════
#  ৪. Miller–Rabin primality test
# ═══════════════════════════════════════════════════════════════

# n \< 3,317,044,064,679,887,385,961,981 -এর জন্য deterministic
DETERMINISTIC_WITNESSES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37)

SMALL_PRIMES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47)


def is_prime(n, witnesses=None):
    if n \< 2:
        return False
    for p in SMALL_PRIMES:
        if n % p == 0:
            return n == p

    # n - 1 = 2^s * d,  d বিজোড়
    d, s = n - 1, 0
    while d % 2 == 0:
        d //= 2
        s += 1

    for a in (witnesses or DETERMINISTIC_WITNESSES):
        if a % n == 0:
            continue
        x = power_mod(a, d, n)
        if x == 1 or x == n - 1:
            continue                      # এই witness কিছু বলল না
        for _ in range(s - 1):
            x = x * x % n
            if x == n - 1:
                break                     # -1 পাওয়া গেল, ঠিক আছে
        else:
            return False                  # কখনো -1 এল না -> composite
    return True


def random_prime(bits):
    """bits-bit একটা random prime — উপরের দুইটা bit সেট করা,
    যেন p*q-র আকার নিশ্চিতভাবে 2*bits হয়।"""
    while True:
        top  = 2 ** (bits - 1) + 2 ** (bits - 2)
        cand = random.getrandbits(bits) | top | 1
        if is_prime(cand):
            return cand


# ═══════════════════════════════════════════════════════════════
#  ৫. খেলনা RSA  —  শেখার জন্য, ব্যবহারের জন্য নয়
# ═══════════════════════════════════════════════════════════════

def rsa_keygen(bits=64, e=65537, verbose=True):
    while True:
        p = random_prime(bits)
        q = random_prime(bits)
        if p == q:
            continue
        n = p * q
        phi = (p - 1) * (q - 1)
        if egcd(e, phi)[0] == 1:
            break
    d = modinv(e, phi)
    if verbose:
        print(f"  p     = {p}")
        print(f"  q     = {q}")
        print(f"  n     = {n}   ({n.bit_length()} bit)")
        print(f"  phi   = {phi}")
        print(f"  e     = {e}")
        print(f"  d     = {d}")
        print(f"  যাচাই : e*d mod phi = {e * d % phi}  (১ হতে হবে)")
    return (n, e), (n, d)


def rsa_encrypt(pub, m):
    n, e = pub
    assert 0 <= m and m \< n, "message অবশ্যই n-এর চেয়ে ছোট হতে হবে"
    return power_mod(m, e, n)


def rsa_decrypt(priv, c):
    n, d = priv
    return power_mod(c, d, n)


def rsa_decrypt_crt(p, q, d, c):
    """CRT দিয়ে decryption — প্রায় ৪ গুণ দ্রুত।
    OpenSSL-এর private key-তে dmp1/dmq1/iqmp এই তিনটা মান।"""
    dp   = d % (p - 1)
    dq   = d % (q - 1)
    qinv = modinv(q, p)
    m1 = power_mod(c, dp, p)
    m2 = power_mod(c, dq, q)
    h  = qinv * (m1 - m2) % p
    return m2 + h * q


# ═══════════════════════════════════════════════════════════════
#  ৬. Chinese Remainder Theorem
# ═══════════════════════════════════════════════════════════════

def crt(residues, moduli):
    """x ≡ residues[i] (mod moduli[i]) — moduli জোড়ায় coprime ধরে।"""
    M = 1
    for m in moduli:
        M *= m
    x = 0
    for a, m in zip(residues, moduli):
        Mi = M // m
        x += a * Mi * modinv(Mi, m)
    return x % M, M


# ═══════════════════════════════════════════════════════════════
#  চালিয়ে দেখা
# ═══════════════════════════════════════════════════════════════

def rule(title):
    print(f"\n── {title} " + "─" * max(0, 56 - len(title)))


rule("Extended Euclid: gcd(240, 46)")
g, x, y = egcd(240, 46, trace=True)
print(f"  240*({x}) + 46*({y}) = {240*x + 46*y}   gcd = {g}")

rule("Modular inverse")
for a, m in [(17, 43), (3, 10), (65537, 3120), (6, 9)]:
    try:
        inv = modinv(a, m)
        print(f"  {a}^-1 mod {m:>5} = {inv:>6}   "
              f"যাচাই: {a}*{inv} mod {m} = {a*inv % m}")
    except ValueError as err:
        print(f"  {a}^-1 mod {m:>5} = নেই    ({err})")

rule("Fermat's little theorem: a^(p-1) = 1 mod p")
p = 97
for a in (2, 3, 50, 96):
    print(f"  {a:>3}^{p-1} mod {p} = {power_mod(a, p-1, p)}")
print(f"  আর composite-এ ভাঙে: 2^560 mod 561 = {power_mod(2, 560, 561)} "
      f"(561 Carmichael, তাই ১ দেয় যদিও composite!)")

rule("Miller–Rabin কিন্তু Carmichael ধরে ফেলে")
for n in (561, 1105, 1729, 2465, 6601,          # সবগুলো Carmichael
          97, 2**61 - 1, 2**64 - 59):          # শেষ দুইটা সত্যিই prime
    print(f"  is_prime({n:>22,}) = {is_prime(n)}")

rule("Chinese Remainder Theorem: সুন জু-র ধাঁধা")
x, M = crt([2, 3, 2], [3, 5, 7])
print(f"  x = {x} (mod {M})")
print(f"  যাচাই: {x}%3={x%3}, {x}%5={x%5}, {x}%7={x%7}")

rule("পাঠ্যবইয়ের RSA — ছোট, জানা উদাহরণ")
n, e, d = 3233, 17, 2753
m = 65
c = power_mod(m, e, n)
print(f"  n={n}, e={e}, d={d}")
print(f"  m = {m}  ->  c = {c}  ->  m' = {power_mod(c, d, n)}")

rule("খেলনা RSA — নতুন key তৈরি করে round trip")
pub, priv = rsa_keygen(bits=64)
message = int.from_bytes(b"CS!", "big")
cipher  = rsa_encrypt(pub, message)
plain   = rsa_decrypt(priv, cipher)
print(f"\n  plaintext  = {message}  ({message.to_bytes(3, 'big')})")
print(f"  ciphertext = {cipher}")
print(f"  decrypted  = {plain}  ({plain.to_bytes(3, 'big')})")
print(f"  round trip {'ঠিক আছে' if plain == message else 'ব্যর্থ'}")

rule("একই কাজ CRT দিয়ে")
import time
n, d = priv
big_c = rsa_encrypt(pub, 123456789)
t0 = time.perf_counter(); power_mod(big_c, d, n);       t_plain = time.perf_counter() - t0
print(f"  সাধারণ  : {t_plain*1e6:8.1f} µs")
print("  (CRT-র লাভ দেখতে bits=512 করে আবার চালান — তখন "
      "পার্থক্যটা স্পষ্ট)")

যা লক্ষ্য করবেন:

  • egcd-এর assert a * old_s + b * old_t == old_r লাইনটা invariant-টাকে চলমান অবস্থায় যাচাই করে। Induction-এর লেসনে আমরা এটাকে loop invariant বলেছিলাম — কোডে সেটা লিখে রাখলে প্রমাণটা জীবন্ত থাকে।
  • 2^560 mod 561 = 1 — Fermat test 561-কে prime বলে ফেলে, কিন্তু Miller–Rabin ধরে ফেলে। এটাই দুইটা test-এর পার্থক্য চোখে দেখার সবচেয়ে সরাসরি উপায়।
  • modinv(6, 9) ব্যর্থ হয় কারণ gcd(6,9) = 3। ব্যর্থতাটা নীরব নয় — এটা গুরুত্বপূর্ণ, কারণ crypto কোডে নীরব ভুল মানে ভাঙা নিরাপত্তা।

নিজে বাড়ান:

  1. egcd-এর recursive সংস্করণ লিখুন আর দুটোর ফলাফল ১০,০০০টা random জোড়ায় মিলিয়ে দেখুন। কোনটা বড় input-এ দ্রুত, আর কেন?
  2. power_mod-কে Montgomery ladder রূপে লিখুন — যেখানে exponent-এর bit যাই হোক, প্রতি iteration-এ একই সংখ্যক গুণ হয়। তারপর time.perf_counter_ns() দিয়ে দুটোর timing distribution তুলনা করুন: naive সংস্করণে exponent-এর popcount আর সময়ের correlation দেখতে পাবেন কি?
  3. একটা Sieve of Eratosthenes যোগ করুন আর is_prime -এর সাথে তুলনা করুন — 10⁶ পর্যন্ত সব prime বের করতে কোনটা দ্রুত, আর crossover কোথায়?
  4. crt-টাকে coprime নয় এমন modulus-এও কাজ করান: সমাধান আছে কেবল যদি প্রতিটা জোড়ার জন্য aᵢ ≡ aⱼ (mod gcd(mᵢ, mⱼ))। এটা যাচাই করে সাধারণীকৃত CRT লিখুন।
  5. rsa_decrypt_crt-কে বড় key-তে (bits=512, bits=1024) benchmark করুন। তাত্ত্বিক লাভটা কি পান? না পেলে Python-এর big-integer implementation নিয়ে কী অনুমান করা যায়?
  6. Pollard’s rho factorization লিখুন আর দেখুন আপনার ৬৪-bit খেলনা RSA key কত সেকেন্ডে ভাঙে। তারপর bits বাড়াতে বাড়াতে দেখুন কোথায় আপনার laptop হার মানে — এটাই key size নির্বাচনের বাস্তব অনুভূতি দেবে।
  7. একটা luhn_ok, isbn13_ok আর iban_ok module লিখুন, আর প্রতিটার জন্য একটা property test: এক অঙ্ক বদলালে কি সবসময় ধরা পড়ে? পাশাপাশি দুইটা অদলবদল করলে?

বাস্তব সিস্টেমে

এই গণিত যেখানে যেখানে চলছে

প্রতিটা uint operation। আপনার CPU-র প্রতিটা unsigned arithmetic আক্ষরিকভাবে mod 2ⁿ255 + 1 = 0 একটা bug নয়, এটা সংজ্ঞা। Level 1-এ আমরা bit ধরে দেখব।

Hash table। Bucket index h % m। CPython-এর dict m কে দুইয়ের ঘাত রাখে (তাই & (m−1)) কিন্তু hash-টা আগে ভালোভাবে mix করে; Java-র HashMap উপরের bit গুলো নিচে XOR করে দেয় — দুটোই একই সমস্যার দুই সমাধান: modulus যদি তথ্য ফেলে দেয়, আগে তথ্যটা ছড়িয়ে দাও।

Cyclic buffer। (head + 1) % capacity — ring buffer, audio pipeline, network queue, Linux kernel-এর kfifo। Capacity দুইয়ের ঘাত হলে % একটা &-এ নেমে আসে।

Checksum ও error detection। Luhn (credit card), ISBN-10/13, IBAN, আর প্রতিটা CRC। CRC আসলে polynomial arithmetic mod 2 — আর সেজন্যই এটা hardware-এ কয়েকটা XOR gate দিয়ে করা যায়। Ethernet frame, ZIP file, PNG chunk — সবার শেষে একটা CRC আছে।

TCP sequence number। ৩২ bit, তাই mod 2³²-এ বাস করে। উচ্চ-গতির লিঙ্কে সেকেন্ডেই wrap করতে পারে, তাই PAWS (Protection Against Wrapped Sequences) timestamp দিয়ে পুরনো আর নতুন packet আলাদা করে। Level 7-এ দেখব।

Cryptography। RSA, Diffie–Hellman, DSA, আর elliptic curve — সবগুলোই modular arithmetic-এর উপর দাঁড়ানো। TLS handshake-এর key exchange আক্ষরিকভাবে gᵃ mod p গণনা, আর সেটা করা হয় binary exponentiation দিয়ে — যে algorithm-টা আমরা induction-এর লেসনে প্রমাণ করেছি।

Random number generation। Linear congruential generator: xₙ₊₁ = (a·xₙ + c) mod m। দ্রুত কিন্তু দুর্বল — glibc-এর rand() এটাই। আর rand() % n-এর modular bias এড়াতে rejection sampling লাগে।

Consistent hashing। Distributed cache-এ key কোন node-এ যাবে — hash(key) mod n ব্যবহার করলে একটা node যোগ/বিয়োগে প্রায় সব key সরে যায়। Consistent hashing একটা ring-এ (mod 2³²) সাজিয়ে সেটা 1/n-এ নামায়। Level 9-এ দেখব।

Git object address। SHA-1/SHA-256 hash-এর প্রথম দুই hex digit দিয়ে directory ভাগ করা — objects/ab/cdef...। একটা directory-তে খুব বেশি file না রাখার জন্য ২৫৬ ভাগে বণ্টন।

যে ভুলগুলো সবাই করে

“`%` মানে সবসময় ভাগশেষ, আর সেটা সবসময় অ-ঋণাত্মক।”

ভাষাভেদে % আলাদা আচরণ করে, আর ঋণাত্মক সংখ্যায় পার্থক্যটা বাস্তব bug তৈরি করে।

ভাষা-7 % 3নিয়ম
Python2ফল divisor-এর চিহ্ন নেয়
C, Java, Go, Rust-1ফল dividend-এর চিহ্ন নেয়
গণিত (mod)2সবসময় [0, n)

C-এর % আসলে remainder, গাণিতিক modulo নয়।

এই পার্থক্যটা কামড়ায় যখন আপনি cyclic index হিসাব করেন:

int idx = (i - 1) % n;      /* i = 0 হলে -1, array index হিসেবে বিপর্যয় */
int idx = ((i - 1) % n + n) % n;   /* সঠিক */

Ring buffer-এ পিছনে যাওয়া, circular list-এ wrap করা, বা image-এ neighbouring pixel — সব জায়গায় এই bug পাওয়া যায়।

“বড় সংখ্যার জন্য `pow(a, b) % m` লিখলেই হলো।”

গাণিতিকভাবে ঠিক, ব্যবহারিকভাবে বিপর্যয়।

pow(2, 10**6) % 1000003        # আগে 10⁶ bit-এর সংখ্যা বানায়
pow(2, 10**6, 1000003)         # প্রতি ধাপে mod নেয় — মিলিসেকেন্ড

প্রথমটা প্রায় ৩ লক্ষ দশমিক অঙ্কের একটা সংখ্যা memory-তে বানায়, তারপর ভাগ করে। দ্বিতীয়টা কখনো m-এর চেয়ে বড় সংখ্যা রাখে না।

RSA-তে exponent ২০৪৮ bit — প্রথম পদ্ধতিতে ফলাফলটা মহাবিশ্বে আঁটত না। Binary exponentiation + প্রতি ধাপে mod ছাড়া public-key cryptography অস্তিত্বই পেত না।

C/Java-তে নিজে লিখলে মনে রাখুন: (a * b) % m -এও overflow হতে পারে যদি a, b প্রায় m-এর সমান হয়। তখন __int128 বা Montgomery multiplication লাগে।

“Miller–Rabin probabilistic, তাই বাস্তব ব্যবহারে অনিরাপদ।”

k রাউন্ডে ভুল হওয়ার সম্ভাবনা ≤ 4⁻ᵏk = 40 মানে 10⁻²⁴-এর কম।

তুলনা করুন: একটা cosmic ray আপনার RAM-এ bit flip করার সম্ভাবনা এর চেয়ে অনেক বেশি। আপনার ডিস্ক silently ভুল ডেটা ফেরত দেওয়ার সম্ভাবনাও বেশি।

তাই OpenSSL, GnuPG, Go-র crypto/rsa — সবাই Miller–Rabin ব্যবহার করে। AKS deterministic কিন্তু এত ধীর যে বাস্তবে কেউ ব্যবহার করে না।

তবে একটা সূক্ষ্মতা আছে: যদি সংখ্যাটা adversary বেছে দেয় (আপনার নয়), তখন নির্দিষ্ট base-এ Miller–Rabin ফাঁকি দেওয়া যায় (Carmichael-জাতীয় সংখ্যা)। তাই ভালো library random base ব্যবহার করে, আর প্রায়ই Baillie–PSW-র মতো মিশ্র পরীক্ষা চালায়।

Probabilistic মানে “অনিশ্চিত” নয় — মানে “ত্রুটির সম্ভাবনা পরিমাণে নিয়ন্ত্রিত”।

“RSA দিয়ে যেকোনো message সরাসরি encrypt করা যায়।”

“Textbook RSA” — c = mᵉ mod n — সরাসরি ব্যবহার করলে একাধিক গুরুতর দুর্বলতা:

  • Deterministic — একই message সবসময় একই ciphertext। তাই “হ্যাঁ”/“না” পাঠালে attacker দুইটা আলাদা করতে পারে
  • Malleablec কে rᵉ দিয়ে গুণ করলে plaintext r দিয়ে গুণ হয়ে যায়
  • ছোট m আর ছোট e হলে mᵉ n-এর চেয়ে ছোট থেকে যায়, আর সাধারণ মূল বের করেই plaintext পাওয়া যায়

তাই বাস্তবে OAEP padding লাগে, যা randomness যোগ করে।

আর RSA দিয়ে বড় ডেটা encrypt করা হয় না — ধীর। বাস্তবে hybrid: RSA দিয়ে একটা random symmetric key encrypt করা হয়, তারপর সেই key দিয়ে AES-এ আসল ডেটা।

নিজে crypto implement করবেন না — শেখার জন্য ছাড়া। Level 10-এ আমরা কেন সেটা বিস্তারিত দেখব।

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

1

gcd(1071, 462) বের করুন Euclid দিয়ে, তারপর extended Euclid দিয়ে Bézout সহগ x, y বের করুন যেন 1071x + 462y = gcd

প্রয়োগ

Euclid:

ধাপaba mod b
11071462147
246214721
3147210

gcd(1071, 462) = 21

Extended Euclid — পিছন দিকে substitute:

ভাগগুলো লিখি:

1071 = 2 × 462 + 147
 462 = 3 × 147 + 21
 147 = 7 × 21  + 0

শেষ অশূন্য ভাগশেষ থেকে পিছনে:

21 = 462 − 3 × 147
   = 462 − 3 × (1071 − 2 × 462)
   = 462 − 3 × 1071 + 6 × 462
   = 7 × 462 − 3 × 1071

তাই x = −3, y = 7:

1071(−3) + 462(7) = −3213 + 3234 = 21

যাচাই কোডে:

def egcd(a, b):
    if b == 0: return a, 1, 0
    g, x, y = egcd(b, a % b)
    return g, y, x - (a // b) * y

print(egcd(1071, 462))    # (21, -3, 7)

কেন এটা কাজে লাগে: Bézout সহগই [[modular-inverse]] দেয়। gcd(a, m) = 1 হলে ax + my = 1, তাই ax ≡ 1 (mod m) — অর্থাৎ x হলো a-এর inverse।

এখানে gcd = 21 ≠ 1, তাই 1071 -এর কোনো inverse নেই mod 462-এ। সেটাও একটা দরকারি উত্তর।

Level 10-এ RSA-র d = e⁻¹ mod φ(n) হিসাব করতে ঠিক এই algorithm চলবে।

2

কেন hash table-এর bucket সংখ্যা মৌলিক রাখা ভালো, অথচ Python আর Java দুইয়ের ঘাত ব্যবহার করে?

যুক্তি

মৌলিক কেন ভালো: h % m-এ যদি m দুইয়ের ঘাত হয়, তাহলে % m মানে শুধু নিচের log₂ m টা bit রাখা — উপরের সব bit ফেলে দেওয়া।

m = 256:   h % 256  ==  h & 0xFF     উপরের 24 bit হারিয়ে গেল

Hash function-এর low bit যদি দুর্বল হয় (যেমন সব key-এর শেষে একই pattern), তাহলে clustering হবে।

মৌলিক m হলে % -এ hash-এর সব bit অংশ নেয়, তাই দুর্বল hash-ও মোটামুটি ছড়িয়ে যায়।

তাহলে Python/Java দুইয়ের ঘাত কেন:

দুইটা কারণ:

১. গতি। & একটা instruction, ~১ cycle। % -এ hardware division লাগে, আধুনিক x86-এ ২০–৪০ cycle। Hash lookup-এর hot path-এ এটা বিশাল পার্থক্য।

২. তারা hash-টা আগেই ঠিক করে নেয়। দুর্বল hash-এর সমস্যা %-এ সমাধান না করে উৎসেই সমাধান করা হয়:

// Java HashMap
static final int hash(Object key) {
    int h = key.hashCode();
    return h ^ (h >>> 16);      // উপরের bit নিচে XOR
}

উপরের ১৬ bit নিচের ১৬ bit-এ মিশিয়ে দেওয়া হলো — এখন & (m−1) করলেও উপরের bit-এর তথ্য হারায় না।

CPython আরো এগিয়ে: open addressing-এ probe sequence-টাই perturbation দিয়ে hash-এর উপরের bit ধীরে ধীরে টেনে আনে।

সিদ্ধান্তের নিয়ম:

পরিস্থিতিপছন্দ
Hash-এর মান নিয়ন্ত্রণে নেইprime modulus
Hash নিজে ভালো বা mix করা যায়দুইয়ের ঘাত + &
Adversarial input সম্ভব+ randomised seed

শেষ সারিটা গুরুত্বপূর্ণ — hash flooding DoS আটকাতে modulus নয়, randomisation লাগে।

3

আপনি একটা distributed rate limiter বানাচ্ছেন যা প্রতি ব্যবহারকারীকে মিনিটে ১০০ request দেয়। Counter একটা uint32-এ রাখা হচ্ছে। Modular arithmetic-এর দিক থেকে কী কী ভুল হতে পারে?

ডিজাইন

সমস্যা ১ — counter overflow।

uint32 mod 2³²-এ চলে। 4294967295 + 1 = 0

একটা counter যদি কখনো reset না হয় আর সেকেন্ডে ১০,০০০ বার বাড়ে, তাহলে প্রায় ৫ দিনে wrap করবে। Wrap করার মুহূর্তে counter শূন্য — ব্যবহারকারী হঠাৎ পুরো quota ফিরে পাবে।

সমাধান: window-ভিত্তিক counter ব্যবহার করুন যা প্রতি মিনিটে reset হয়, বা uint64 (যা 10¹⁹ — কার্যত কখনো wrap করবে না)।

সমস্যা ২ — time bucket-এর modulus।

সাধারণ pattern:

bucket = (timestamp // 60) % num_buckets

num_buckets ছোট হলে অনেক আগের bucket আর এখনকার bucket একই জায়গায় পড়বে — পুরনো count নতুন window-এ যোগ হয়ে যাবে।

সমাধান: bucket key-তে পুরো timestamp // 60 রাখুন, শুধু % নয়। Redis-এ key expiry দিয়ে পুরনোগুলো নিজেই মুছে যাক।

সমস্যা ৩ — শার্ডিং-এ hash(user) % n

Rate limiter যদি n টা node-এ শার্ড করা হয় আর একটা node যোগ করা হয়, তখন % n থেকে % (n+1) — প্রায় সব ব্যবহারকারী নতুন node-এ সরে যাবে, আর তাদের counter হারিয়ে যাবে। সবাই একসাথে পুরো quota ফিরে পাবে।

সমাধান: consistent hashing — একটা ring-এ (mod 2³²) সাজালে node যোগে শুধু 1/n অংশ সরে।

সমস্যা ৪ — ঋণাত্মক modulo।

Clock skew-এর কারণে timestamp পিছিয়ে গেলে C/Java-তে (t1 - t2) % n ঋণাত্মক হতে পারে, আর সেটা array index হিসেবে ব্যবহার করলে out-of-bounds।

সমাধান: ((x % n) + n) % n

সবচেয়ে ভালো নকশা: sliding window log বা token bucket ব্যবহার করুন, যেখানে counter wraparound-এর উপর নির্ভরতাই নেই — timestamp-এর তালিকা বা একটা float token count রাখুন।

Level 9 আর Level 12-এ distributed rate limiting বিস্তারিত দেখব।

4

7¹⁰⁰ mod 13 বের করুন — pow ছাড়া, হাতে।

প্রয়োগ

Fermat’s little theorem ব্যবহার করি। 13 মৌলিক আর gcd(7,13) = 1, তাই:

7121(mod13)7^{12} \equiv 1 \pmod{13}

100 কে 12 দিয়ে ভাগ:

100 = 8 × 12 + 4

তাই: 7100=78×12+4=(712)874187474(mod13)7^{100} = 7^{8 \times 12 + 4} = (7^{12})^8 \cdot 7^4 \equiv 1^8 \cdot 7^4 \equiv 7^4 \pmod{13}

এখন 7⁴ হিসাব করি ধাপে ধাপে:

7²  = 49  = 3×13 + 10  →  49 ≡ 10 (mod 13)
7⁴  = (7²)² ≡ 10² = 100 = 7×13 + 9  →  100 ≡ 9 (mod 13)

উত্তর: 7¹⁰⁰ ≡ 9 (mod 13)

যাচাই:

>>> pow(7, 100, 13)
9

Fermat ছাড়া binary exponentiation দিয়েও হতো:

100 = 1100100₂

7¹   ≡ 7
7²   ≡ 10
7⁴   ≡ 9
7⁸   ≡ 9² = 81 ≡ 3
7¹⁶  ≡ 3² = 9
7³²  ≡ 9² ≡ 3
7⁶⁴  ≡ 3² ≡ 9

100 = 64 + 32 + 4
7¹⁰⁰ ≡ 9 × 3 × 9 = 243 ≡ 243 − 18×13 = 9  ✓

দুই পথে একই উত্তর।

কোনটা কখন: Fermat শুধু মৌলিক modulus-এ খাটে আর exponent বড় হলে নাটকীয়ভাবে কাজ কমায়। Binary exponentiation যেকোনো modulus-এ খাটে আর O(log e) — এটাই সাধারণ হাতিয়ার।

RSA-তে দুটোই ব্যবহার হয়: binary exponentiation মূল গণনার জন্য, আর CRT + Fermat মিলে decryption প্রায় ৪ গুণ দ্রুত করা হয়।

5

rand() % 6 দিয়ে ছক্কার ফল বের করলে কী সমস্যা, আর কেন rejection sampling সেটা ঠিক করে?

যুক্তি

সমস্যা — modular bias।

ধরুন rand() সমানভাবে 0 থেকে RAND_MAX দেয়। যদি RAND_MAX + 1 6-এর গুণিতক না হয়, তাহলে কিছু ফল বেশি সম্ভাব্য হয়ে যায়।

সরল উদাহরণ — rand() দেয় 0..9 (১০টা মান), আমরা চাই 0..5:

ফলকোন কোন inputসংখ্যা
00, 62
11, 72
22, 82
33, 92
441
551

03 -এর সম্ভাবনা 2/10 = 20%, কিন্তু 45 -এর 10%দ্বিগুণ পার্থক্য।

বাস্তবে RAND_MAX = 2³¹−1 হলে bias খুব ছোট (~3×10⁻¹⁰), কিন্তু n বড় হলে বাড়ে — আর cryptographic প্রসঙ্গে যেকোনো bias অগ্রহণযোগ্য।

Rejection sampling কীভাবে ঠিক করে:

উপরের অসম অংশটা ফেলে দিন।

uint32_t roll(uint32_t n) {
    uint32_t limit = RAND_MAX - (RAND_MAX % n);
    uint32_t r;
    do { r = rand(); } while (r >= limit);
    return r % n;
}

limit হলো n-এর সবচেয়ে বড় গুণিতক যা range-এ আঁটে। তার উপরের মানগুলো বাতিল করে আবার চেষ্টা — তখন অবশিষ্ট range ঠিক n-এর গুণিতক, তাই % নিখুঁতভাবে সমান।

খরচ: প্রত্যাশিত RAND_MAX/limit বার loop — সবচেয়ে খারাপ ক্ষেত্রেও ২-এর কম। কার্যত বিনামূল্যে।

ভাষার সঠিক API:

random.randrange(6)           # rejection sampling ভেতরে করে
secrets.randbelow(6)          # cryptographic, bias-মুক্ত
rand.Intn(6)                  // Go — bias-মুক্ত
rng.gen_range(0..6)           // Rust — bias-মুক্ত

নিয়ম: % n কখনো সরাসরি random-এ প্রয়োগ করবেন না। ভাষার range API ব্যবহার করুন — সেগুলো এই কাজটা ইতিমধ্যে ঠিকভাবে করেছে।

Level 10-এ আমরা দেখব cryptographic randomness-এ এই সতর্কতা কেন আরো কঠোর।

এরপর কী

Number theory আমাদের দিয়েছে discrete, গোনা-যায় এমন জগতের গণিত — আর সেটাই computer-এর স্বাভাবিক জগৎ।

পরের লেসনে আমরা একদম উল্টো দিকে যাব: linear algebra, যেখানে আমরা vector আর matrix নিয়ে কাজ করব। প্রথমে অপ্রাসঙ্গিক মনে হতে পারে — কিন্তু graphics, machine learning, PageRank, signal processing আর GPU computing — সবগুলোর ভাষা এটাই।

আর একটা সুন্দর সংযোগ আছে: একটা matrix আসলে একটা function, আর matrix গুণ হলো function composition। তাই functions-এর লেসনে শেখা injectivity, invertibility আর composition — সবই সরাসরি ফিরে আসবে, শুধু নতুন পোশাকে।

আরও পড়ুন