Number Systems ও Modular Arithmetic — ঘড়ির গণিত
Number Systems and Modular Arithmetic
Base conversion, GCD, congruence আর modular inverse — যে গণিতটা hex dump, hash table, checksum, two's complement আর RSA-কে একসাথে ব্যাখ্যা করে।
আগে এটা বুঝি
একটা ঘড়ির দিকে তাকান। ৯টা বাজে, আপনি ৫ ঘণ্টা যোগ করলেন। উত্তর ১৪ নয় — ২।
আপনি সারাজীবন 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 লিখলে আমরা আসলে বলি:
গুরুত্বপূর্ণ কথাটা হলো: 10 -এ বিশেষ কিছু নেই। আমাদের দশটা
আঙুল আছে, ব্যস। যেকোনো b ≥ 2 কাজ করে।
সাধারণভাবে, base b-তে অঙ্ক dₙ … d₁ d₀ মানে:
শর্তটা লক্ষ্য করুন: প্রতিটা অঙ্ক 0 থেকে b−1। Base 2-এ
অঙ্ক দুইটা (0,1), base 16-এ ষোলোটা (0–9, a–f)।
একই সংখ্যা, চার ভাষায়:
| Base | ২০২৬ | Prefix |
|---|---|---|
| Decimal (10) | 2026 | — |
| Binary (2) | 11111101010 | 0b |
| Octal (8) | 3752 | 0o (C-তে শুধু 0) |
| Hex (16) | 7ea | 0x |
যাচাই করুন হাতে: 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) ← সবচেয়ে বেশি গুরুত্বপূর্ণ
উল্টো দিকে পড়ুন → 7eaBase b → decimal: Horner’s method।
0x7ea: শুরু 0
0 × 16 + 7 = 7
7 × 16 + 14 = 126
126 × 16 + 10 = 2026Horner-এর সুবিধা: কোনো 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) আছে যেন
দুইটা শব্দই গুরুত্বপূর্ণ: অস্তিত্ব আর অনন্যতা। অনন্যতাই modular arithmetic-কে সুসংজ্ঞায়িত করে।
GCD আর Euclid-এর অ্যালগরিদম
gcd(a, b) = সবচেয়ে বড় পূর্ণসংখ্যা যা দুটোকেই ভাগ করে।
Euclid-এর মূল অন্তর্দৃষ্টি (খ্রিস্টপূর্ব ৩০০ অব্দ, আজও সেরা):
কেন সত্য: 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আছে যেন
Extended Euclid এই x, y বের করে — একই O(log n) সময়ে।
হাতে করে দেখুন — gcd(240, 46):
সামনের দিকে Euclid:
| ধাপ | সমীকরণ |
|---|---|
| 1 | 240 = 5 × 46 + 10 |
| 2 | 46 = 4 × 10 + 6 |
| 3 | 10 = 1 × 6 + 4 |
| 4 | 6 = 1 × 4 + 2 |
| 5 | 4 = 2 × 2 + 0 → gcd = 2 |
এখন উল্টো দিকে প্রতিস্থাপন — প্রতিবার ভাগশেষটাকে আগের লাইন দিয়ে বদলান:
| উৎস | প্রতিস্থাপন |
|---|---|
| ধাপ 4 | 2 = 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 |
পুনরাবৃত্তিমূলক রূপ (যেটা আমরা কোড করব) এই উল্টো যাত্রাটা সামনের দিকেই করে ফেলে, দুইটা সহগ জোড়া টেনে নিয়ে:
Congruence — সমতার একটা শিথিল সংস্করণ
পড়ুন: “a আর b congruent modulo m”।
সমতুল্য তিনটা সংজ্ঞা (একটা বাছুন, যেটা হাতের কাজে সুবিধা):
m | (a − b)a mod m == b mod ma = b + kmকোনো পূর্ণসংখ্যাk-এর জন্য
17 ≡ 5 (mod 12) — কারণ 12 | 12। ঘড়িতে ১৭টা মানে বিকেল ৫টা।
এটা একটা equivalence relation
Relations-এর লেসনে আমরা তিনটা শর্ত শিখেছিলাম। যাচাই করুন:
| ধর্ম | যাচাই |
|---|---|
| Reflexive | m ভাগ করে (a − a) = 0 ✓ |
| Symmetric | m যদি (a−b) ভাগ করে, (b−a) ও করে ✓ |
| Transitive | (a−b) আর (b−c) ভাগ করলে (a−c) = (a−b)+(b−c) ও ভাগ করে ✓ |
তাই congruence পূর্ণসংখ্যাদের partition করে ঠিক m টা
equivalence class-এ:
[r] = যাদের ভাগশেষ r, অর্থাৎ {…, r−m, r, r+m, r+2m, …}।
Congruence যোগ ও গুণে টিকে থাকে
এটাই পুরো বিষয়টার প্রাণভোমরা। যদি a ≡ b আর c ≡ d (mod m):
প্রমাণ (গুণের জন্য). a = b + km, c = d + lm ধরুন।
দ্বিতীয় পদটা m-এর গুণিতক, তাই ac ≡ bd \pmod m
ব্যবহারিক ফল: আপনি যেকোনো সময় mod নিতে পারেন।
# এই দুইটা একই উত্তর দেয় —
(123456789 * 987654321) % 1000
(123456789 % 1000) * (987654321 % 1000) % 1000
# কিন্তু দ্বিতীয়টায় সংখ্যাগুলো কখনো ৬ অঙ্কের বেশি হয় নাবড় সংখ্যার প্রতিটা algorithm এই ধর্মের উপর দাঁড়িয়ে — মাঝপথে mod নিয়ে সংখ্যা ছোট রাখা যায়, উত্তর বদলায় না। Overflow এড়ানোর আসল কৌশল এটাই।
Modular inverse — কখন ভাগ করা যায়
Modular arithmetic-এ “ভাগ” বলে কিছু নেই। যা আছে তা হলো inverse দিয়ে গুণ।
a-র modular inverse mod m হলো সেই x যেন:
লিখি 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 = 1।
d = gcd(a,m) বাঁ পাশের দুটো পদকেই ভাগ করে, তাই d | 1, তাই
d = 1
তাই extended Euclid = modular inverse খোঁজার যন্ত্র।
একটা করে দেখি — 17⁻¹ mod 43:
| ধাপ | Euclid |
|---|---|
| 1 | 43 = 2 × 17 + 9 |
| 2 | 17 = 1 × 9 + 8 |
| 3 | 9 = 1 × 8 + 1 |
| 4 | 8 = 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), অর্থাৎ
যাচাই: 17 × 38 = 646 = 15 × 43 + 1 ✓
Fermat আর Euler — শর্টকাট দুইটা
Fermat’s little theorem:
pprime আরp ∤ aহলে
কেন বিশ্বাসযোগ্য (স্বজ্ঞা, পূর্ণ প্রমাণ নয়): a, 2a, 3a, …, (p−1)a
সংখ্যাগুলো mod p নিলে 1, 2, …, p−1-এরই একটা পুনর্বিন্যাস
হয় (কারণ a invertible, তাই গুণটা একটা bijection — functions-এর
লেসনের ভাষায়)। দুই দিকের গুণফল সমান করলে:
(p−1)! prime p-এর সাথে coprime, তাই কাটা যায়
তাৎক্ষণিক প্রয়োগ: a⁻¹ ≡ a^(p−2) (mod p)। Extended Euclid
ছাড়াই inverse — শুধু একটা modular exponentiation।
Euler’s theorem (সাধারণীকরণ):
gcd(a, n) = 1হলে
φ(n) = Euler’s totient = 1..n-এর মধ্যে কতগুলো সংখ্যা
n-এর সাথে coprime।
n | φ(n) | কেন |
|---|---|---|
p (prime) | p − 1 | সব ছোট সংখ্যাই coprime |
p^k | p^k − p^(k−1) | p-এর গুণিতকগুলো বাদ |
pq (p ≠ q prime) | (p−1)(q−1) | multiplicative |
12 = 2²·3 | 4 | {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 হলে, সমীকরণগুলো -এর একটা অনন্য সমাধান আছে modM = 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
i | mᵢ | aᵢ | Mᵢ = M/mᵢ | Mᵢ⁻¹ mod mᵢ | পদ aᵢ Mᵢ Mᵢ⁻¹ |
|---|---|---|---|---|---|
| 1 | 3 | 2 | 35 | 35 ≡ 2, 2⁻¹ = 2 | 2 × 35 × 2 = 140 |
| 2 | 5 | 3 | 21 | 21 ≡ 1, 1⁻¹ = 1 | 3 × 21 × 1 = 63 |
| 3 | 7 | 2 | 15 | 15 ≡ 1, 1⁻¹ = 1 | 2 × 15 × 1 = 30 |
যাচাই: 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-এর জন্য হয়:
কেন: 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/4।
k টা স্বাধীন witness-এ:
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-র যোগ, বিয়োগ ও গুণ আক্ষরিকভাবে mod2ⁿoperation।
uint8_t নিন। 255 + 1 কী?
উত্তর 0। এটা “overflow bug” নয় — এটা সংজ্ঞা। C standard বলে
unsigned arithmetic সংজ্ঞা অনুযায়ী 2ⁿ modulo wraps।
Signed-এর বেলায় two’s complement একই ring, শুধু প্রতিনিধি বাছাই আলাদা:
| Bit pattern | mod-256 class | unsigned পাঠ | signed পাঠ |
|---|---|---|---|
0000 0000 | [0] | 0 | 0 |
0111 1111 | [127] | 127 | 127 |
1000 0000 | [128] | 128 | −128 |
1111 1111 | [255] | 255 | −1 |
[128] থেকে [255] class গুলোর জন্য আমরা ঋণাত্মক প্রতিনিধি
বেছেছি: 255 ≡ −1, 254 ≡ −2, …, 128 ≡ −128।
এই একটা সিদ্ধান্ত থেকে যা বেরোয়:
- যোগের circuit একটাই। Signed আর unsigned যোগে CPU-র একই adder — কারণ class-এর উপর যোগ একই, শুধু আমরা উত্তরটা ভিন্নভাবে পড়ি।
- বিয়োগ = complement + যোগ।
a − b ≡ a + (2ⁿ − b), আর2ⁿ − b = (~b) + 1। তাই আলাদা subtractor লাগে না। - শূন্য একটাই। Sign-magnitude বা one’s complement-এ
+0আর−0দুইটা থাকত। Modular দৃষ্টিতে[0]একটাই class। - অসামঞ্জস্যটাও ব্যাখ্যা হয়ে যায়।
−128আছে কিন্তু+128নেই, কারণ ২৫৬টা class-কে দুই ভাগ করলে একদিকে একটা বেশি পড়ে। তাইabs(INT_MIN)undefined behaviour — উত্তরটা representable নয়।
% কত দামি — আর কখন দাম দিতে হয় না
Integer division হলো CPU-র সবচেয়ে ধীর সাধারণ পাটিগণিত।
| Operation | Latency (আধুনিক x86-64, আনুমানিক) |
|---|---|
add, sub, and, or, xor | 1 cycle |
imul (32/64-bit) | 3–5 cycle |
shl, shr | 1 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 হলে:
কেন: 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) % mCorrectness প্রমাণটা অপরিবর্তিত থাকে — কারণ আমরা দেখিয়েছি
congruence গুণে টিকে থাকে। প্রতিটা মধ্যবর্তী % m উত্তরের
equivalence class বদলায় না, শুধু প্রতিনিধিটা ছোট রাখে।
এটাই একটা প্রমাণ পুনর্ব্যবহারের নিখুঁত উদাহরণ: গঠনটা এক, তাই যুক্তিটাও এক।
খরচ: ⌊log₂ exp⌋ + 1 ধাপ, প্রতিটায় সর্বোচ্চ দুইটা গুণ।
২০৪৮-bit exponent-এ ~২০৪৮ ধাপ, ~৩০০০ গুণ। মিলিসেকেন্ডের ব্যাপার।
Hash table-এর modulus — prime কেন সাহায্য করে
bucket = h % m। m কীভাবে বাছবেন?
সমস্যাটা: বাস্তব 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 — যা অনেক কম সম্ভাব্য।
- a mod mগাণিতিক সংজ্ঞা — equivalence class
- ring buffer index(head + 1) % cap, বা & (cap−1)
- hash bucketh % num_buckets
- two's complement যোগCPU-র adder = mod 2ⁿ
- TCP sequence numbermod 2³² wraparound, PAWS
- CRC checksumGF(2)-তে বহুপদীর ভাগশেষ
- consistent hashinghash ring — mod 2³² -এর বৃত্ত
- RSA / Diffie–Hellmanmod বড় prime বা semiprime
- সময় ও তারিখসেকেন্ড mod 60, Zeller's congruence
উদাহরণ
Checksum — modular arithmetic যা প্রতিদিন আপনার ভুল ধরে
Luhn algorithm — credit card
আপনি কার্ড নম্বর টাইপ করে একটা অঙ্ক ভুল করলেন। Server-এ যাওয়ার আগেই ফর্ম লাল হয়ে গেল। কীভাবে?
নিয়ম: ডান দিক থেকে প্রতি দ্বিতীয় অঙ্ক দ্বিগুণ করুন;
দ্বিগুণ করে ৯-এর বেশি হলে অঙ্ক দুইটা যোগ করুন (সমতুল্যভাবে
9 বিয়োগ করুন)। সব যোগ করে mod 10 শূন্য হলে বৈধ।
উদাহরণ — 79927398713:
| অবস্থান (ডান থেকে) | অঙ্ক | দ্বিগুণ? | অবদান |
|---|---|---|---|
| 0 | 3 | না | 3 |
| 1 | 1 | হ্যাঁ → 2 | 2 |
| 2 | 7 | না | 7 |
| 3 | 8 | হ্যাঁ → 16 → 7 | 7 |
| 4 | 9 | না | 9 |
| 5 | 3 | হ্যাঁ → 6 | 6 |
| 6 | 7 | না | 7 |
| 7 | 2 | হ্যাঁ → 4 | 4 |
| 8 | 9 | না | 9 |
| 9 | 9 | হ্যাঁ → 18 → 9 | 9 |
| 10 | 7 | না | 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 == 0Luhn কী ধরে আর কী ধরে না:
| ভুলের ধরন | ধরা পড়ে? |
|---|---|
| যেকোনো একটা অঙ্ক ভুল | সবসময় |
| পাশাপাশি দুইটা অঙ্ক অদলবদল | প্রায় সবসময় |
09 ↔ 90 অদলবদল | না — এটাই একমাত্র ফাঁক |
| দূরের দুইটা অঙ্ক অদলবদল | না |
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 == 197 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 কে বহুপদী ধরুন:
সহগগুলো {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 # macOSLevel 7-এ আমরা tcpdump দিয়ে এই timestamp option চোখে দেখব।
RSA — কেন গণিতটা কাজ করে
এবার সবগুলো টুকরো এক জায়গায়: prime, φ, modular inverse,
Euler’s theorem, আর fast exponentiation।
Key generation
- দুইটা বড় prime বাছুন:
p,q(বাস্তবে ১০২৪ bit করে) n = p · q— এটাই modulus, প্রকাশ্যφ(n) = (p−1)(q−1)— এটা গোপন- একটা
eবাছুন যেনgcd(e, φ(n)) = 1; সাধারণতe = 65537 d = e⁻¹ mod φ(n)— extended Euclid দিয়ে
Public key (n, e), private key (n, d)।
p, q, φ(n) মুছে ফেলা হয় (বা CRT-র জন্য গোপনে রাখা হয়)।
Encryption ও decryption
দুটোই আমাদের power_mod — যার correctness ইতিমধ্যে প্রমাণিত।
কেন ফিরে আসে
ed ≡ 1 (mod φ(n)) মানে ed = 1 + kφ(n) কোনো পূর্ণসংখ্যা k-এর জন্য।
Euler’s theorem বলে m^φ(n) ≡ 1 (mod n) (যখন gcd(m,n) = 1), তাই:
gcd(m, n) ≠ 1 হলেও (অর্থাৎ m, p বা q-এর গুণিতক) CRT দিয়ে
আলাদাভাবে দেখানো যায় যে ফলাফল একই — তাই সব m \< n-এর জন্য কাজ করে।
একটা ছোট উদাহরণ, শেষ অঙ্ক পর্যন্ত
| ধাপ | মান |
|---|---|
p | 61 |
q | 53 |
n = pq | 3233 |
φ(n) = 60 × 52 | 3120 |
e | 17 (gcd(17, 3120) = 1) |
d = 17⁻¹ mod 3120 | 2753 |
যাচাই: 17 × 2753 = 46801 = 15 × 3120 + 1 ✓
Message m = 65:
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 | দীর্ঘমেয়াদে সুপারিশকৃত |
নিজে চালিয়ে দেখুন
Hex আসলে কী দেখাচ্ছে — একটা ফাইলের ভেতরে ঢুকুন
ধাপ ১ — নিজের হাতে একটা ফাইল বানিয়ে তার প্রতিটা 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 .4VxBig-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
doneLinux-এ /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 জানতেই হবে।
Modular bias — যে bug টা প্রতিটা 'random' ফাংশনে লুকিয়ে থাকে
সমস্যার কঙ্কাল। ধরুন আপনার 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-এর বিনিময়ে।
নিজে বানান
Number Theory Toolkit — extended Euclid থেকে RSA পর্যন্ত
- Extended Euclid লিখুন এবং প্রতিটা ধাপ ছাপিয়ে Bézout সহগ যাচাই করুন
- তার উপর modular inverse বানান, আর gcd ≠ 1 হলে স্পষ্টভাবে ব্যর্থ হন
- Binary exponentiation দিয়ে power_mod লিখুন — induction-এ প্রমাণিত গঠনটাই
- Miller–Rabin দিয়ে prime খুঁজুন, ৬৪-bit-এ deterministic witness set ব্যবহার করে
- সব টুকরো জোড়া দিয়ে একটা খেলনা 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 test561-কে prime বলে ফেলে, কিন্তু Miller–Rabin ধরে ফেলে। এটাই দুইটা test-এর পার্থক্য চোখে দেখার সবচেয়ে সরাসরি উপায়।modinv(6, 9)ব্যর্থ হয় কারণgcd(6,9) = 3। ব্যর্থতাটা নীরব নয় — এটা গুরুত্বপূর্ণ, কারণ crypto কোডে নীরব ভুল মানে ভাঙা নিরাপত্তা।
নিজে বাড়ান:
egcd-এর recursive সংস্করণ লিখুন আর দুটোর ফলাফল ১০,০০০টা random জোড়ায় মিলিয়ে দেখুন। কোনটা বড় input-এ দ্রুত, আর কেন?power_mod-কে Montgomery ladder রূপে লিখুন — যেখানে exponent-এর bit যাই হোক, প্রতি iteration-এ একই সংখ্যক গুণ হয়। তারপরtime.perf_counter_ns()দিয়ে দুটোর timing distribution তুলনা করুন: naive সংস্করণে exponent-এর popcount আর সময়ের correlation দেখতে পাবেন কি?- একটা Sieve of Eratosthenes যোগ করুন আর
is_prime-এর সাথে তুলনা করুন —10⁶পর্যন্ত সব prime বের করতে কোনটা দ্রুত, আর crossover কোথায়? crt-টাকে coprime নয় এমন modulus-এও কাজ করান: সমাধান আছে কেবল যদি প্রতিটা জোড়ার জন্যaᵢ ≡ aⱼ (mod gcd(mᵢ, mⱼ))। এটা যাচাই করে সাধারণীকৃত CRT লিখুন।rsa_decrypt_crt-কে বড় key-তে (bits=512,bits=1024) benchmark করুন। তাত্ত্বিক4×লাভটা কি পান? না পেলে Python-এর big-integer implementation নিয়ে কী অনুমান করা যায়?- Pollard’s rho factorization লিখুন আর দেখুন আপনার
৬৪-bit খেলনা RSA key কত সেকেন্ডে ভাঙে। তারপর
bitsবাড়াতে বাড়াতে দেখুন কোথায় আপনার laptop হার মানে — এটাই key size নির্বাচনের বাস্তব অনুভূতি দেবে। - একটা
luhn_ok,isbn13_okআরiban_okmodule লিখুন, আর প্রতিটার জন্য একটা 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 | নিয়ম |
|---|---|---|
| Python | 2 | ফল 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 দুইটা আলাদা করতে পারে
- Malleable —
cকেrᵉদিয়ে গুণ করলে plaintextrদিয়ে গুণ হয়ে যায় - ছোট
mআর ছোটeহলেmᵉn-এর চেয়ে ছোট থেকে যায়, আর সাধারণ মূল বের করেই plaintext পাওয়া যায়
তাই বাস্তবে OAEP padding লাগে, যা randomness যোগ করে।
আর RSA দিয়ে বড় ডেটা encrypt করা হয় না — ধীর। বাস্তবে hybrid: RSA দিয়ে একটা random symmetric key encrypt করা হয়, তারপর সেই key দিয়ে AES-এ আসল ডেটা।
নিজে crypto implement করবেন না — শেখার জন্য ছাড়া। Level 10-এ আমরা কেন সেটা বিস্তারিত দেখব।
বুঝেছেন কি না দেখুন
1gcd(1071, 462) বের করুন Euclid দিয়ে, তারপর extended Euclid
দিয়ে Bézout সহগ x, y বের করুন যেন 1071x + 462y = gcd।
প্রয়োগ
gcd(1071, 462) বের করুন Euclid দিয়ে, তারপর extended Euclid
দিয়ে Bézout সহগ x, y বের করুন যেন 1071x + 462y = gcd।Euclid:
| ধাপ | a | b | a mod b |
|---|---|---|---|
| 1 | 1071 | 462 | 147 |
| 2 | 462 | 147 | 21 |
| 3 | 147 | 21 | 0 |
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-এর দিক থেকে কী কী ভুল হতে পারে?
ডিজাইন
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_bucketsnum_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 বিস্তারিত দেখব।
47¹⁰⁰ mod 13 বের করুন — pow ছাড়া, হাতে।
প্রয়োগ
7¹⁰⁰ mod 13 বের করুন — pow ছাড়া, হাতে।Fermat’s little theorem ব্যবহার করি। 13 মৌলিক আর
gcd(7,13) = 1, তাই:
100 কে 12 দিয়ে ভাগ:
100 = 8 × 12 + 4তাই:
এখন 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)
9Fermat ছাড়া 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 প্রায় ৪ গুণ দ্রুত করা হয়।
5rand() % 6 দিয়ে ছক্কার ফল বের করলে কী সমস্যা, আর কেন
rejection sampling সেটা ঠিক করে?
যুক্তি
rand() % 6 দিয়ে ছক্কার ফল বের করলে কী সমস্যা, আর কেন
rejection sampling সেটা ঠিক করে?সমস্যা — modular bias।
ধরুন rand() সমানভাবে 0 থেকে RAND_MAX দেয়। যদি
RAND_MAX + 1 6-এর গুণিতক না হয়, তাহলে কিছু ফল বেশি
সম্ভাব্য হয়ে যায়।
সরল উদাহরণ — rand() দেয় 0..9 (১০টা মান), আমরা চাই 0..5:
| ফল | কোন কোন input | সংখ্যা |
|---|---|---|
| 0 | 0, 6 | 2 |
| 1 | 1, 7 | 2 |
| 2 | 2, 8 | 2 |
| 3 | 3, 9 | 2 |
| 4 | 4 | 1 |
| 5 | 5 | 1 |
0–3 -এর সম্ভাবনা 2/10 = 20%, কিন্তু 4–5 -এর 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 — সবই সরাসরি ফিরে আসবে, শুধু নতুন পোশাকে।
আরও পড়ুন
- Mathematics for Computer Science, Chapter 9 (Number Theory) — Lehman, Leighton, Meyer
- A Method for Obtaining Digital Signatures and Public-Key Cryptosystems — Rivest, Shamir, Adleman (1978) · RSA-র মূল পেপার — আশ্চর্যজনকভাবে সহজবোধ্য
- The Art of Computer Programming, Vol. 2: Seminumerical Algorithms — Donald Knuth · Euclid, প্রাথমিকতা পরীক্ষা আর random number generation-এর প্রামাণ্য আলোচনা