এই পর্বে যা শিখবেন
- এলোমেলোতা কেন worst-case ইনপুটকে হারায়, আর Las Vegas ও Monte Carlo অ্যালগরিদমের পার্থক্য
- Expectation, indicator variable, স্বাধীনতা, Markov-এর অসমতা আর union bound — আর এগুলো দিয়ে randomized quicksort-এর O(n log n) প্রমাণ
- Freivalds-এর matrix যাচাই, Miller–Rabin primality test আর Karger-এর min cut হাতে trace করা, আর পুনরাবৃত্তি দিয়ে ভুলের সম্ভাবনা কমানো
- Reservoir sampling আর Fisher–Yates shuffle-এর সঠিকতার প্রমাণ, আর সহজ shuffle কেন পক্ষপাতদুষ্ট
- Universal hashing, birthday paradox, আর PRNG বনাম cryptographic RNG — কোথায় কোনটি ব্যবহার করবেন
১ · এলোমেলোতা কেন সাহায্য করে — প্রতিপক্ষ, লটারি আর খাতা যাচাই
এ পর্যন্ত প্রায় সব অ্যালগরিদম ছিল deterministic: একই ইনপুটে প্রতিবার হুবহু একই ধাপ। এর একটি দুর্বলতা আছে। কেউ যদি আপনার কোড জানেন, তিনি ঠিক সেই ইনপুট বানাতে পারেন, যেখানে আপনার অ্যালগরিদম সবচেয়ে ধীর। পর্ব ২২-এ দেখেছেন, সবসময় প্রথম উপাদানকে pivot নিলে quick sort সাজানো অ্যারেতে O(n²) হয়ে যায়। পর্ব ৩২-এ দেখেছেন hash flooding: একই ঘরে পড়ে এমন হাজার হাজার key পাঠিয়ে সার্ভার থামিয়ে দেওয়া। এমন ইচ্ছাকৃত খারাপ ইনপুটকে বলে adversarial input.
Randomized অ্যালগরিদম চলার সময় নিজে এলোমেলো সংখ্যা নেয় — যেন মুদ্রা ছোড়ে। প্রতিপক্ষ ইনপুট বেছে নেন আগে, কিন্তু মুদ্রা পড়ে পরে। তাই তিনি জানেন না কোন pivot বা কোন hash function বাছা হবে। আমরা যে নিশ্চয়তা দিই, তা হয় এমন: “প্রতিটি ইনপুটের জন্য, আমাদের মুদ্রার ওপর গড়ে, খরচ এত” — এটিই expected running time. পর্ব ৫-এর average case-এর সাথে পার্থক্যটি মনে রাখুন: সেখানে ধরে নিই ইনপুট এলোমেলো; এখানে ইনপুট নিয়ে কোনো অনুমান নেই।
উপমা ১ — লটারির ড্র। পাড়ার ক্রিকেট টুর্নামেন্টে কোন দল কার সাথে খেলবে, তা আয়োজক নিজে ঠিক করলে সন্দেহ ওঠে। তাই সবার সামনে বাক্স থেকে কাগজ তোলা হয়। এলোমেলো ড্র কাউকে আগে থেকে সুবিধা দেয় না। Randomized অ্যালগরিদমও তা-ই করে: ইনপুট যে-ই পাঠান, সিদ্ধান্ত হয় ড্রয়ে।
উপমা ২ — নিরীক্ষকের খাতা যাচাই। একটি বোর্ড পরীক্ষায় একজন পরীক্ষক ৫০০টি খাতা দেখেছেন। প্রধান পরীক্ষক সব খাতা আবার দেখেন না; এলোমেলোভাবে ২০টি তুলে মিলিয়ে দেখেন। পরীক্ষক যদি এক-চতুর্থাংশ খাতায় ভুল নম্বর দিয়ে থাকেন, তাহলে ২০টির একটিতেও ভুল না পাওয়ার সম্ভাবনা (3/4)20 ≈ 0.003 — হাজারে তিনবার। পরীক্ষক জানেন না কোন খাতা তোলা হবে, তাই “ভালো খাতাগুলো ওপরে রাখা” কৌশল কাজ করে না। এই পর্বের Freivalds অ্যালগরিদম ঠিক এই ধারণা: পুরো কাজ আবার না করে এলোমেলো নমুনায় যাচাই।
এলোমেলোতা আরও দুটি উপহার দেয়। সরলতা: treap বা skip list (পর্ব ৩৮) red-black tree-র চেয়ে অনেক ছোট কোডে একই প্রত্যাশিত গতি দেয়। গতি: কিছু সমস্যায় জানা সবচেয়ে ভালো deterministic পদ্ধতির চেয়ে randomized পদ্ধতি অনেক দ্রুত বা সরল — যেমন matrix গুণফল যাচাই বা বড় সংখ্যার primality test.
🎰 Las Vegas
উত্তর সবসময় সঠিক; শুধু চলার সময় এলোমেলো। প্রত্যাশিত সময়ের সীমা দেওয়া হয়। উদাহরণ: randomized quicksort, quickselect, treap, skip list.
“ভুল কখনো নয়, দেরি হতে পারে”🎲 Monte Carlo
সময় নির্দিষ্ট; উত্তর ছোট, নিয়ন্ত্রিত সম্ভাবনায় ভুল। পুনরাবৃত্তি করে ভুলের সম্ভাবনা যত খুশি কমানো যায়। উদাহরণ: Freivalds, Miller–Rabin, Karger.
“সময়মতো শেষ, কদাচিৎ ভুল”Las Vegas আর Monte Carlo — দুটোই জুয়ার শহরের নাম; নামকরণের গল্প ভাবনার প্রশ্ন ২-এ। মনে রাখার সহজ উপায়: Las Vegas-এ আপনি জেতা পর্যন্ত খেলেন (সময় অনিশ্চিত, ফল নিশ্চিত); Monte Carlo-তে নির্দিষ্ট দান খেলে উঠে পড়েন (সময় নিশ্চিত, ফল অনিশ্চিত)।
২ · সম্ভাবনার যন্ত্রপাতি — expectation, indicator আর দুটি সীমা
এই পর্বের প্রতিটি প্রমাণ পাঁচ-ছয়টি সরল হাতিয়ারে দাঁড়িয়ে। প্রতিটি একবার পরিষ্কার করে নিই।
Random variable আর expectation. Random variable X হলো এলোমেলো পরীক্ষার ফল থেকে একটি সংখ্যা — যেমন “quicksort-এ মোট তুলনা”. তার expected value E[X] = Σ x · P(X = x), অর্থাৎ বহুবার চালালে গড়ে যা আসে। একটি ছক্কার E = (1 + 2 + … + 6)/6 = 3.5.
Indicator variable. কোনো ঘটনা A ঘটলে IA = 1, না ঘটলে 0 — এটি indicator random variable. এর মূল গুণ: E[IA] = P(A). কোনো জিনিস “গোনা” মানে অনেকগুলো indicator যোগ করা।
Linearity of expectation. যেকোনো random variable-এর জন্য E[X + Y] = E[X] + E[Y] — তারা স্বাধীন হোক বা না হোক. এটিই linearity of expectation, এই পর্বের সবচেয়ে শক্তিশালী হাতিয়ার। জটিল জিনিসের গড় বের করতে সেটিকে indicator-এর যোগফল হিসেবে লিখুন, প্রতিটির সম্ভাবনা বের করুন, যোগ করুন — ঘটনাগুলোর পারস্পরিক সম্পর্ক নিয়ে মাথা ঘামাতে হয় না।
উদাহরণ — দাওয়াতের জুতা। বিয়ের দাওয়াতে n জন অতিথি দরজায় জুতা খুলে রাখলেন। ফেরার সময় অন্ধকারে প্রত্যেকে এলোমেলো একজোড়া জুতা পরে বাড়ি গেলেন (জুতার একটি এলোমেলো permutation). গড়ে কতজন নিজের জুতা পেলেন? অতিথি i নিজের জুতা পেলে Ii = 1; P = 1/n. তাই E[মোট] = n × 1/n = ১ — n যত বড়ই হোক! ঘটনাগুলো স্বাধীন নয় (একজন অন্যের জুতা নিলে বাকিদের সম্ভাবনা বদলায়), তবু linearity খাটে।
স্বাধীনতা. দুটি ঘটনা independent হলে P(A ও B) = P(A) · P(B). আলাদা আলাদা মুদ্রা নিক্ষেপ স্বাধীন — তাই k বার স্বাধীনভাবে চালানো পরীক্ষার প্রতিটিতে ব্যর্থ হওয়ার সম্ভাবনা গুণফল। Amplification-এর পুরো ভিত্তি এটি। সাবধান: linearity-তে স্বাধীনতা লাগে না, কিন্তু গুণফলের নিয়মে লাগে।
Markov-এর অসমতা. X ≥ 0 হলে যেকোনো a > 0-এর জন্য P(X ≥ a) ≤ E[X]/a. এটিই Markov's inequality. যুক্তি এক লাইনের: X ≥ a · IX≥a, দুই পাশে expectation নিন। ব্যবহার: একটি Las Vegas অ্যালগরিদমের প্রত্যাশিত সময় T হলে, 2T সময়ের বেশি লাগার সম্ভাবনা ≤ 1/2. তাই “2T ধাপ পর থামাও, না পারলে ‘জানি না’ বলো” — Las Vegas থেকে Monte Carlo বানানো যায়।
Union bound. P(A1 বা A2 বা … বা Am) ≤ P(A1) + … + P(Am) — ঘটনাগুলো স্বাধীন না হলেও। এটিই union bound: “অনেকগুলো খারাপ ঘটনার যেকোনো একটি” ঘটার সম্ভাবনা বের করতে। পর্ব ৩৮-এ skip list-এর উচ্চতা এভাবে বেঁধেছিলেন।
Chernoff bound — শুধু নামটুকু. অনেকগুলো স্বাধীন 0/1 variable-এর যোগফল তার গড় থেকে বেশি দূরে যাওয়ার সম্ভাবনা সূচকীয় হারে কমে: গড় μ হলে, 0 < δ ≤ 1-এর জন্য P(X ≥ (1 + δ)μ) ≤ e−δ²μ/3. এটি Chernoff bound. Markov শুধু গড় জানে, তাই দুর্বল; Chernoff স্বাধীনতা কাজে লাগায়, তাই অনেক শক্ত। “৫০টি স্বাধীন চালানোর সংখ্যাগরিষ্ঠ ভোট প্রায় কখনো ভুল নয়” — এমন দাবি Chernoff দিয়েই প্রমাণ হয়।
গড় চাইলে: indicator-এ ভাঙুন + linearity. “খারাপ কিছু ঘটার” সম্ভাবনার সীমা চাইলে: একটি variable হলে Markov, অনেক ঘটনা হলে union bound, স্বাধীন পুনরাবৃত্তি হলে গুণফল বা Chernoff.
৩ · Las Vegas — randomized quicksort-এর প্রমাণ, quickselect আর treap
পর্ব ২২-এ randomized quicksort দেখেছেন: প্রতিটি ধাপে pivot এলোমেলোভাবে বাছা। এখন indicator দিয়ে তার প্রত্যাশিত তুলনা-সংখ্যা প্রমাণ করি — প্রমাণটি বিশ্ববিদ্যালয়, চাকরি আর ইন্টারভিউর পরীক্ষায় খুব প্রচলিত।
সাজানো ক্রমে উপাদানগুলোর নাম দিন z1 < z2 < … < zn (সব আলাদা ধরি)। i < j হলে Xij = 1, যদি zi আর zj কখনো সরাসরি তুলনা হয়। দুটি উপাদান সর্বোচ্চ একবার তুলনা হয় (তুলনা হয় শুধু pivot-এর সাথে, আর pivot পরে আর কোনো ভাগে থাকে না)। তাই মোট তুলনা X = Σi<j Xij.
মূল প্রশ্ন: P(zi আর zj তুলনা হয়) কত? সেট Zij = {zi, zi+1, …, zj} দেখুন, এতে j − i + 1 টি উপাদান। এই সেট থেকে প্রথম যে উপাদান pivot হয়, সে-ই সিদ্ধান্ত নেয়। (ক) সে zi বা zj হলে দুজনের তুলনা হয়। (খ) সে মাঝের কোনো zk হলে zi যায় বাঁ ভাগে, zj ডান ভাগে — আর কখনো দেখা হয় না। সেট থেকে কেউ pivot হওয়ার আগ পর্যন্ত পুরো সেট একই ভাগে থাকে, আর pivot এলোমেলো, তাই সেটের প্রত্যেকের “প্রথম হওয়ার” সম্ভাবনা সমান। ফলে
এখন linearity: E[X] = Σi<j 2/(j − i + 1). d = j − i ধরলে, প্রতিটি i-এর জন্য ভেতরের যোগফল ≤ 2(1/2 + 1/3 + … + 1/n) < 2Hn, যেখানে Hn ≈ ln n হলো harmonic সংখ্যা (পর্ব ৬)। n টি i মিলিয়ে
সঠিক মান 2(n + 1)Hn − 4n. এটি প্রতিটি ইনপুটের জন্য — সাজানো, উল্টো, সব একই। Worst case এখনো O(n²) (প্রতিবার সবচেয়ে ছোটটি pivot হলে), কিন্তু তার জন্য প্রতিপক্ষকে আমাদের মুদ্রা আগে থেকে জানতে হবে। উত্তর সবসময় সঠিক সাজানো অ্যারে — তাই এটি Las Vegas. জায়গা: ছোট ভাগে আগে recursion করলে O(log n) stack.
Randomized quickselect — প্রত্যাশিত O(n)
k-তম ছোট উপাদান চাই (পর্ব ২২-এর quickselect). এলোমেলো pivot দিয়ে partition করুন, তারপর শুধু যে ভাগে k-তম আছে সেখানে recursion. সংক্ষিপ্ত যুক্তি: pivot যদি মাঝের অর্ধেকে পড়ে (rank n/4 থেকে 3n/4-এর মধ্যে; সম্ভাবনা 1/2), পরের ভাগের আকার ≤ 3n/4. তাই প্রতি “ভালো pivot” পেতে গড়ে ২টি চেষ্টা, প্রতিটি চেষ্টা O(বর্তমান আকার)। মোট প্রত্যাশিত কাজ ≤ 2c(n + 3n/4 + (3/4)²n + …) = 8cn = O(n) — একটি geometric series. Worst case O(n²); deterministic O(n) চাইলে পর্ব ২২-এর median of medians.
Treap আর skip list — এলোমেলো ভারসাম্য
পর্ব ৩৮-এ treap আর skip list দেখেছেন। একই ধরনের indicator-প্রমাণ সেখানে দেখিয়েছিল যে xi হলো xk-এর ancestor সম্ভাবনা 1/(|k − i| + 1)-এ, তাই প্রত্যাশিত গভীরতা < 2 ln n. লক্ষ করুন, quicksort-এর “তুলনা হয়” আর treap-এর “ancestor হয়” একই ঘটনা — treap আসলে quicksort-এর recursion tree. দুটোই Las Vegas: খোঁজার উত্তর সবসময় সঠিক, শুধু tree-র আকৃতি এলোমেলো।
৪ · Monte Carlo — Freivalds-এর matrix যাচাই আর amplification
সমস্যা: তিনটি n × n matrix A, B, C দেওয়া। C = A·B কি না? সরাসরি গুণ করে মেলাতে সাধারণ পদ্ধতিতে Θ(n³), আর সবচেয়ে উন্নত তাত্ত্বিক পদ্ধতিতেও প্রায় n2.37 (পর্ব ৪৯-এর matrix multiplication exponent). ১৯৭৭ সালে লাটভিয়ার Rūsiņš Freivalds একটি অসাধারণ সরল উপায় দেন — Freivalds' algorithm:
- একটি এলোমেলো vector r ∈ {0, 1}n বাছুন — প্রতিটি ঘর আলাদা মুদ্রা।
- B·r হিসাব করুন (n² গুণ), তারপর A·(B·r) (আরও n²), আর C·r (আরও n²)।
- A·(B·r) ≠ C·r হলে বলুন “C ভুল” — এটি নিশ্চিত. সমান হলে বলুন “সম্ভবত ঠিক”.
প্রতিটি চেষ্টা Θ(n²) — পুরো গুণফলের চেয়ে অনেক কম। নিচের ডায়াগ্রামে C-তে একটিমাত্র ঘর ভুল (row 1, column 2-এ 3-এর জায়গায় 4); r-এর মানগুলো এই পর্বের প্রোগ্রামের seed থেকে আসা।
| ট্রায়াল | |
| r | |
| ফল | |
| মোট গুণ (n = 3) |
- দাবি. কেউ বলছেন C = A·B. পুরো গুণফল না করে যাচাই করব: এলোমেলো r নিয়ে দুই পাশে r দিয়ে গুণ। A·B·r দুইভাবে লেখা যায় — A·(B·r), যা দুটি matrix-vector গুণ।
- ট্রায়াল ১, B·r. r = (0, 1, 0) শুধু column 1 বেছে নেয়, তাই B·r = B-এর column 1 = (0, 1, 2). খরচ n² = ৯টি গুণ।
- তুলনা. A·(B·r) = (2, 7, 2), আর C·r = C-এর column 1 = (2, 7, 2). সমান! কারণ ভুল ঘরটি column 2-এ, আর r2 = 0 সেটিকে “দেখে” না। এবার ভুল ধরা পড়ল না।
- ট্রায়াল ২. r = (0, 0, 0): দুই পাশেই শূন্য vector — কিছুই শেখা গেল না। Zero vector আসার সম্ভাবনা 1/8; এটি ভুল ধরতে না পারার একটি কারণ।
- ট্রায়াল ৩, B·r. r = (1, 0, 1): column 0 আর column 2 যোগ — B·r = (1 + 2, 3 + 0, 0 + 1) = (3, 3, 1).
- অসমান! A·(B·r) = (9, 6, 7), কিন্তু C·r = (9, 7, 7) — row 1-এ অমিল। এখন C ≠ A·B নিশ্চিত; এই উত্তরে কোনো সম্ভাবনা নেই। মোট ৮১টি গুণ, যেখানে পুরো গুণফলে লাগত n³ = ২৭ — ছোট n-এ লাভ নেই, কিন্তু n = 1000-এ প্রতি ট্রায়ালে 3 × 106 বনাম 109.
কেন ভুল ধরার সম্ভাবনা অন্তত 1/2
D = A·B − C ধরুন। C ভুল হলে D ≠ 0, তাই D-এর কোনো ঘর dij ≠ 0. অ্যালগরিদম ভুল ধরে না শুধু যখন D·r = 0. D·r-এর i-তম ঘর = dij rj + S, যেখানে S বাকি সব r-এর ওপর নির্ভর করে। বাকি সব r আগে ঠিক হয়ে গেছে ধরুন — তখন S একটি নির্দিষ্ট সংখ্যা। rj-এর দুটি মানের (0 আর 1) জন্য dij rj + S দুটি আলাদা সংখ্যা (কারণ dij ≠ 0), তাই সর্বোচ্চ একটি শূন্য হতে পারে। rj স্বাধীন মুদ্রা, তাই P(D·r = 0) ≤ 1/2. ∎ (এই যুক্তির নাম principle of deferred decisions — একটি মুদ্রা সবার শেষে ছুড়ছি ধরে নেওয়া।)
উত্তর একমুখী: “অসমান” সবসময় সঠিক, ভুল হতে পারে শুধু “সম্ভবত ঠিক”. এমন অ্যালগরিদমের ভুলকে বলে one-sided error. এখন amplification: k বার স্বাধীন r দিয়ে চালান, একবারও অসমান হলে “ভুল” বলুন। ভুল C সব k বার পার পাওয়ার সম্ভাবনা ≤ (1/2)k — স্বাধীনতার গুণফল। k = 20-এ ≤ 2−20 ≈ 10−6, k = 40-এ প্রায় 10−12; মোট খরচ Θ(k n²).
সংখ্যা উপচে পড়া (overflow) সাবধান। বড় matrix-এ A·(B·r)-এর ঘর int-এর সীমা ছাড়াতে পারে, আর দুই পাশ আলাদাভাবে উপচালে মিথ্যা “অসমান” আসতে পারে। long long, অথবা কোনো বড় মৌলিক সংখ্যা p দিয়ে mod-এ হিসাব করুন (পর্ব ৬২) — mod-এ ভুল ধরার সম্ভাবনা তখনো ≥ 1/2, কারণ ওপরের যুক্তি যেকোনো field-এ খাটে — শর্ত শুধু, A·B − C-এর অন্তত একটি ঘর p দিয়ে বিভাজ্য নয় (সব ঘরের পরম মান p-এর চেয়ে ছোট হলে এটি নিশ্চিত)।
৫ · Miller–Rabin — বড় সংখ্যাটি মৌলিক কি না
RSA-র মতো cryptography-তে কয়েকশো অঙ্কের মৌলিক সংখ্যা লাগে। √n পর্যন্ত ভাগ করে দেখা (trial division) সেখানে অসম্ভব — 10300-এর বর্গমূল 10150. দরকার একটি দ্রুত primality test. এখানে শুধু মূল ধারণা; modular exponentiation আর সংখ্যাতত্ত্বের ভিত্তি পর্ব ৬২-এ।
Fermat-এর ধারণা. n মৌলিক হলে, n দিয়ে বিভাজ্য নয় এমন যেকোনো a-র জন্য an−1 ≡ 1 (mod n) — Fermat's little theorem. তাই কোনো a-তে এটি না মিললে n নিশ্চিত যৌগিক। এটি Fermat test. সমস্যা: কিছু যৌগিক সংখ্যা n-এর সাথে সহমৌলিক প্রতিটি a-তে এই পরীক্ষা পাস করে — Carmichael number. সবচেয়ে ছোটটি 561 = 3 × 11 × 17, আর ১৯৯৪ সালে Alford, Granville ও Pomerance প্রমাণ করেন এমন সংখ্যা অসীম।
Miller–Rabin-এর বাড়তি পরীক্ষা. মৌলিক p-তে x² ≡ 1 (mod p) হলে x ≡ ±1 — এর বাইরে 1-এর কোনো “বর্গমূল” নেই। তাই n − 1 = 2s · d লিখুন (d বিজোড়), আর ক্রমটি দেখুন: ad, a2d, a4d, …, a2sd (mod n). n মৌলিক হলে ক্রমটি হয় 1 দিয়ে শুরু, নয়তো শেষ 1-এর ঠিক আগে কোথাও n − 1 আসে। না হলে a হলো n-এর যৌগিকতার “সাক্ষী” (witness).
| n = 561, a = 2 | হিসাব (mod 561) | সিদ্ধান্ত |
|---|---|---|
| Fermat | 2560 ≡ 1 | পাস — “মৌলিক মনে হচ্ছে” (ভুল) |
| Miller–Rabin: 560 = 24 · 35 | 235 ≡ 263 → 263² ≡ 166 → 166² ≡ 67 → 67² ≡ 1 | 1 এল 67 থেকে, 560 থেকে নয় — 67 হলো 1-এর অদ্ভুত বর্গমূল, তাই যৌগিক, নিশ্চিত |
Miller–Rabin-এর মূল উপপাদ্য (Rabin ১৯৮০, আর স্বাধীনভাবে Monier ১৯৮০): n বিজোড় যৌগিক হলে 2 থেকে n − 2-এর মধ্যে অন্তত 3/4 অংশ a সাক্ষী। তাই এলোমেলো a-তে একটি চেষ্টায় যৌগিক n-কে “মৌলিক” বলার সম্ভাবনা ≤ 1/4, আর k টি স্বাধীন a-তে ≤ 4−k. খরচ প্রতি চেষ্টায় O(log n) টি modular গুণ (binary exponentiation). আবার one-sided error: “যৌগিক” সবসময় সত্য, “সম্ভবত মৌলিক” কদাচিৎ মিথ্যা।
64-bit সংখ্যার জন্য deterministic রূপ — সাবধানে বলা কথা। সব n < 264-এর জন্য base হিসেবে প্রথম ১২টি মৌলিক সংখ্যা (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37) পরীক্ষা করলেই উত্তর নিশ্চিত সঠিক — এটি এলোমেলো যুক্তি নয়, কম্পিউটারে ব্যাপক অনুসন্ধানে যাচাই করা একটি প্রকাশিত ফল। আরও কম base-এর সেট-ও জানা আছে, কিন্তু কোনো সেট ব্যবহারের আগে উৎস যাচাই করুন; “শুধু 2 আর 3 যথেষ্ট” জাতীয় দাবি ভুল (যেমন 1373653 যৌগিক, তবু base 2 আর 3 দুটোতেই পাস করে)। আর 64-bit-এ a·b mod n হিসাবে গুণফল 128 bit পর্যন্ত যায় — C++-এ __int128 ব্যবহার না করলে overflow-এ নীরবে ভুল উত্তর।
৬ · Karger-এর min cut — এলোমেলো edge চেপে চেপে
একটি সংযুক্ত, undirected গ্রাফকে দুই ভাগ করতে সবচেয়ে কম কয়টি edge কাটতে হয়? এটি global minimum cut — পর্ব ৪৭-এর s-t min cut-এর মতো নির্দিষ্ট source আর sink নেই। নেটওয়ার্কের সবচেয়ে দুর্বল জায়গা খোঁজা, ছবিকে অংশে ভাগ করা — এমন কাজে লাগে। ১৯৯৩ সালে David Karger একটি আশ্চর্য সরল অ্যালগরিদম দেন — Karger's algorithm:
Pseudocode — one run of Karger's contraction (graph may have parallel edges)
while the graph has more than 2 vertices:
pick an edge (u, v) uniformly at random
contract it: merge u and v into one supernode,
keep all their other edges (parallel edges stay),
delete the edges between u and v (they would be self-loops)
return the number of edges left between the 2 supernodesEdge contraction মানে দুটি vertex-কে একটি “supernode”-এ জুড়ে দেওয়া। ফলে একই জোড়ার মধ্যে একাধিক edge (parallel edge) তৈরি হয় — গ্রাফটি একটি multigraph. নিচে দুটি ত্রিভুজ, মাঝে একটিমাত্র সেতু-edge c-d; min cut = 1.
| এই ধাপে | |
| supernode / edge | |
| এই ধাপে c-d বাঁচার সম্ভাবনা | |
| ফল |
- শুরু. ৬টি vertex, ৭টি edge. Min cut হলো {a, b, c} বনাম {d, e, f} — শুধু c-d কাটলেই চলে। Karger জানে না কোনটি min cut; সে শুধু এলোমেলো edge চাপে।
- a-b চাপা. ৭টির মধ্যে a-b এল (প্রতিটির সম্ভাবনা 1/7). a আর b মিলে [ab]; a-c আর b-c এখন দুটি parallel edge ab-c. c-d এখনো আছে — বাঁচার সম্ভাবনা ছিল 6/7.
- e-f চাপা. ৬টির মধ্যে e-f. এখন [ef], আর d থেকে দুটি parallel edge d-ef. c-d বাঁচল 5/6 সম্ভাবনায়।
- একটি ab-c চাপা. ৫টির মধ্যে দুটি ab-c-র একটি এল। [abc] তৈরি হলো; অন্য ab-c edge এখন নিজের সাথেই যুক্ত (self-loop), তাই মুছে ফেলা হলো। বাকি ৩টি edge.
- একটি d-ef চাপা. ৩টির মধ্যে দুটি d-ef, একটি abc-d (মূল c-d). d-ef এল (সম্ভাবনা 2/3), [def] তৈরি। দুটি supernode বাকি, মাঝে ১টি edge — সঠিক min cut! এই পথে c-d বাঁচার সম্ভাবনা ছিল 6/7 · 5/6 · 4/5 · 2/3 = 8/21 ≈ 0.38.
- অসফল পথ. ধাপ ৩-এর অবস্থায় যদি ৫টির মধ্যে c-d আসত (1/5), তাহলে c আর d এক supernode-এ — min cut-এর দুই পাশ মিশে গেল। বাকি ৪টি edge-এর যেকোনো একটি চাপলে শেষে দুটি supernode-এর মাঝে ২টি edge: উত্তর 2, যা ভুল। Monte Carlo: সময় নির্দিষ্ট, উত্তর কখনো কখনো বড়।
সাফল্যের সম্ভাবনা ≥ 2/(n(n − 1)) — প্রমাণের রূপরেখা
একটি নির্দিষ্ট min cut C ধরুন, তার আকার c. প্রতিটি vertex-এর degree ≥ c (নইলে সেই vertex-কে একা আলাদা করাই c-র চেয়ে ছোট cut হতো), তাই মোট edge ≥ nc/2. প্রথম ধাপে C-র একটি edge চাপার সম্ভাবনা ≤ c / (nc/2) = 2/n. Contraction কোনো cut-কে ছোট করে না, তাই i টি supernode থাকা অবস্থাতেও min cut ≥ c আর edge ≥ ic/2; C-র edge চাপার সম্ভাবনা ≤ 2/i. C পুরো চালানোয় বেঁচে থাকলে উত্তর ঠিক C. তাই
গুণফলটি telescoping: লব আর হর প্রায় সব কেটে যায়। n = 6-এ 2/30 = 1/15 — ওপরের গ্রাফে আসল সম্ভাবনা এর চেয়ে বেশি, কারণ সীমাটি সবচেয়ে খারাপ গ্রাফের জন্য। একটি সুন্দর উপজাত: যেকোনো গ্রাফে min cut-এর সংখ্যা ≤ n(n − 1)/2, কারণ প্রতিটি min cut-এর সম্ভাবনা ≥ 2/(n(n − 1)), আর এই সম্ভাবনাগুলোর যোগফল ≤ 1.
পুনরাবৃত্তি. একবারে সাফল্যের সম্ভাবনা ছোট, তাই N = (n(n − 1)/2) · ln n বার স্বাধীনভাবে চালিয়ে সবচেয়ে ছোট উত্তরটি নিন। সব চালানো ব্যর্থ হওয়ার সম্ভাবনা ≤ (1 − 2/(n(n−1)))N ≤ e−ln n = 1/n (কারণ 1 − x ≤ e−x). একটি চালানো সরলভাবে O(n²) সময়ে, বা edge-গুলো এলোমেলো ক্রমে সাজিয়ে DSU দিয়ে Kruskal-এর মতো প্রায় O(m) সময়ে করা যায় — তাই মোট O(n⁴ log n) পর্যন্ত। ১৯৯৩ সালেই Karger ও Clifford Stein দেখান, শুরুর ধাপগুলোতে ঝুঁকি কম বলে সেগুলো একবার করে শুধু শেষের দিকে বারবার চালালে মোট O(n² log³ n) — Karger–Stein অ্যালগরিদম।
৭ · ন্যায্য নমুনা আর ন্যায্য shuffle — reservoir sampling আর Fisher–Yates
Reservoir sampling — অজানা দৈর্ঘ্যের stream থেকে k টি
একটি bKash-ধাঁচের সার্ভারে সারাদিন লেনদেন আসছে — একটার পর একটা, মোট কত আসবে আগে জানা নেই, আর সব মেমরিতে রাখা সম্ভব নয়। দিনশেষে ঠিক k টি লেনদেন চাই, যেখানে প্রতিটি লেনদেনের নমুনায় থাকার সম্ভাবনা সমান। এমন ডেটাকে বলে data stream, আর সমাধান reservoir sampling (Algorithm R): প্রথম k টি সরাসরি রাখুন। i-তম উপাদান (i > k) এলে 0 থেকে i − 1-এর মধ্যে একটি এলোমেলো j নিন; j < k হলে reservoir-এর j-তম ঘরে নতুনটি বসান, নইলে বাদ দিন। জায়গা O(k), প্রতি উপাদানে O(1) সময়, ডেটার ওপর মাত্র একবার হাঁটা।
| i (১ থেকে গোনা) | |
| এলোমেলো j | |
| নতুনটি ঢোকার সম্ভাবনা k/i | |
| reservoir |
- i = 1. Reservoir-এ জায়গা খালি, 10 সরাসরি ঘর 0-তে।
- i = 2. 20 ঘর 1-এ। এখন reservoir ভরা; এ পর্যন্ত দেখা দুটিরই থাকার সম্ভাবনা 2/2 = ১ — ঠিক যেমন হওয়া উচিত।
- i = 3. j নেওয়া হলো {0, 1, 2} থেকে: j = 1 < 2, তাই 30 ঢুকল ঘর 1-এ, 20 বের হলো। 30-এর ঢোকার সম্ভাবনা ছিল 2/3.
- i = 4. j = 3 — শুধু 0 বা 1 হলে ঢুকত, তাই 40 বাদ। ঢোকার সম্ভাবনা ছিল 2/4.
- i = 5. j = 0 < 2: 50 ঢুকল ঘর 0-তে, 10 বের হলো। লক্ষ করুন, কোন পুরোনোটি বের হবে তা-ও এলোমেলো (j-ই ঠিক করে)।
- i = 6. j = 4, 60 বাদ। Stream শেষ, নমুনা {50, 30}. প্রমাণ বলছে, ছয়টির প্রত্যেকের শেষ নমুনায় থাকার সম্ভাবনা ঠিক 2/6 = 1/3 — শুরুর 10 আর শেষের 60 সমান সুযোগ পেয়েছে।
প্রমাণ — প্রত্যেকে k/n সম্ভাবনায় থাকে. একটি উপাদান i (i > k) ধরুন। সে ঢোকে k/i সম্ভাবনায়। পরের প্রতিটি ধাপ t-তে (t = i + 1, …, n) সে বের হয় শুধু যদি নতুনটি ঢোকে (k/t) আর ঠিক তার ঘরটি বাছা হয় (1/k) — সম্ভাবনা 1/t; তাই টিকে থাকে (t − 1)/t সম্ভাবনায়। মুদ্রাগুলো স্বাধীন, তাই
প্রথম k টির জন্য শুরুতে সম্ভাবনা ১, তারপর একই telescoping গুণফল k/(k+1) ⋯ (n−1)/n = k/n. ∎ আরও শক্ত কথাও সত্য: n টির মধ্য থেকে যেকোনো k-সদস্যের সেট সমান সম্ভাবনায় আসে। Jeffrey Vitter ১৯৮৫ সালে এই পদ্ধতির বিশ্লেষণ আর দ্রুততর রূপ প্রকাশ করেন; Knuth Algorithm R-কে Alan Waterman-এর বলে উল্লেখ করেন।
Fisher–Yates shuffle — প্রতিটি permutation সমান সম্ভাবনায়
লুডুর গুটি, তাসের প্যাকেট, অনলাইন পরীক্ষার প্রশ্নের ক্রম বা লটারির তালিকা — এলোমেলো সাজানো চাই, যেখানে n! টি ক্রমের প্রত্যেকটির সম্ভাবনা ঠিক 1/n!. Fisher–Yates shuffle (Durstenfeld-এর ১৯৬৪ সালের কম্পিউটার রূপ): i = n − 1 থেকে 1 পর্যন্ত, 0 থেকে i-এর মধ্যে এলোমেলো j নিয়ে a[i] আর a[j] অদলবদল করুন। ঘর i একবার ঠিক হলে আর কখনো ছোঁয়া হয় না — যেন বাক্স থেকে একটি করে কাগজ তুলে সারির শেষ থেকে বসানো। Θ(n) সময়, O(1) বাড়তি জায়গা।
| i | |
| j-এর পরিসর → j | |
| এ পর্যন্ত সম্ভাব্য পথ |
- শুরু. A B C D; এখনো কোনো ঘর চূড়ান্ত নয়। i চলবে 3, 2, 1.
- i = 3. j আসে চারটি ঘরের যেকোনোটি থেকে — এখানে j = 1. a[3] আর a[1] অদলবদল: A D C B. শেষ ঘরে B চূড়ান্ত; প্রতিটি অক্ষরের শেষে বসার সম্ভাবনা সমান 1/4.
- i = 2. j আসে {0, 1, 2} থেকে — j = 2, মানে নিজের সাথেই অদলবদল। এটি বাদ দিলে চলবে না: “যে আছে সে-ই থাকুক”-ও একটি বৈধ সমান-সম্ভাবনার পছন্দ।
- i = 1. j আসে {0, 1} থেকে — j = 0: D A C B. ঘর 0-তে যে বাকি, সে নিজেই চূড়ান্ত।
- গণনা. সম্ভাব্য পছন্দের ক্রম 4 × 3 × 2 = 24, আর প্রতিটি ক্রম আলাদা permutation দেয় — 4! টি permutation-এর প্রত্যেকটি ঠিক একবার, তাই সবাই সমান 1/24. সহজ (naive) পদ্ধতিতে 44 = 256টি সমান-সম্ভাবনার পথ; 256 কে 24 দিয়ে ভাগ করা যায় না, তাই সবাই সমান হতেই পারে না।
পরিচিত ভুল — naive shuffle. “প্রতিটি i-তে 0 থেকে n − 1-এর যেকোনো j নিয়ে অদলবদল” দেখতে নির্দোষ, কিন্তু পক্ষপাতদুষ্ট। এর nn টি সমান-সম্ভাবনার পথ। n ≥ 3-এ n − 1 সংখ্যাটি n! কে ভাগ করে কিন্তু nn-এর সাথে সহমৌলিক, তাই n! কখনো nn-কে ভাগ করে না — পথগুলোকে n! টি সমান দলে ভাগ করা অসম্ভব। নিচের প্রোগ্রাম n = 3-এ ২৭টি পথের সবগুলো গুনে দেখায়, তারপর fixed seed-এর একটি PRNG দিয়ে ৬০০০০ বার shuffle করে।
#include <cstdint>
#include <cstdio>
#include <initializer_list>
#include <string>
#include <utility>
#include <vector>
using namespace std;
uint64_t state = 1977; // fixed seed: same numbers on every run
int rnd(int n) { // xorshift64, then a number in 0..n-1
state ^= state << 13; state ^= state >> 7; state ^= state << 17; return int(state % n); }
template <class F> string naive(F choice) { // swap position i with ANY position
string a = "ABC";
for (int i = 0; i < 3; ++i) swap(a[i], a[choice(i, 3)]);
return a;
}
template <class F> string fisher_yates(F choice) { // swap position i with a position 0..i only
string a = "ABC";
for (int i = 2; i > 0; --i) swap(a[i], a[choice(i, i + 1)]);
return a;
}
void table(const string& title, const vector<string>& f1, const vector<string>& f2) {
printf("%s\nperm naive fisher-yates\n", title.c_str());
for (string p : {"ABC", "ACB", "BAC", "BCA", "CAB", "CBA"}) {
int c1 = 0, c2 = 0;
for (auto& s : f1) c1 += s == p;
for (auto& s : f2) c2 += s == p;
printf("%s %6d %12d\n", p.c_str(), c1, c2);
}
}
int main() {
vector<string> nv, fy; // 1) exact: every sequence of choices once
for (int c = 0; c < 27; ++c) nv.push_back(naive([&](int i, int) { return i == 0 ? c % 3 : i == 1 ? c / 3 % 3 : c / 9; }));
for (int a = 0; a < 3; ++a) for (int b = 0; b < 2; ++b) fy.push_back(fisher_yates([&](int i, int) { return i == 2 ? a : b; }));
table("all choice sequences: naive 27, fisher-yates 6", nv, fy);
const int T = 60000; nv.clear(); fy.clear(); // 2) simulation with the fixed-seed generator
for (int t = 0; t < T; ++t) nv.push_back(naive([](int, int n) { return rnd(n); }));
for (int t = 0; t < T; ++t) fy.push_back(fisher_yates([](int, int n) { return rnd(n); }));
table("simulation: " + to_string(T) + " shuffles each (fair = " + to_string(T / 6) + ")", nv, fy);
}আউটপুট:
all choice sequences: naive 27, fisher-yates 6
perm naive fisher-yates
ABC 4 1
ACB 5 1
BAC 5 1
BCA 5 1
CAB 4 1
CBA 4 1
simulation: 60000 shuffles each (fair = 10000)
perm naive fisher-yates
ABC 8870 9927
ACB 11110 10160
BAC 11236 9896
BCA 11030 9851
CAB 8771 10078
CBA 8983 10088
প্রথম সারণি কোনো এলোমেলোতা ছাড়াই সব সম্ভাব্য পছন্দের ক্রম গোনে। Naive-এর ২৭টি পথ ছয়টি permutation-এ 4, 5, 5, 5, 4, 4 ভাগে পড়ে — ACB, BAC, BCA পায় 5/27 ≈ 18.5%, বাকিরা 4/27 ≈ 14.8%; ন্যায্য হতো 1/6 ≈ 16.7%. Fisher–Yates-এর ৬টি পথ ছয়টি permutation-এ ঠিক একটি করে। দ্বিতীয় সারণি একই ঘটনা পরীক্ষায় দেখায়: naive-এর ACB/BAC/BCA ১১০৩০ থেকে ১১২৩৬ (প্রত্যাশিত 60000 × 5/27 ≈ ১১১১১), Fisher–Yates-এর সবগুলো ১০০০০-এর কাছাকাছি, ছোট ওঠানামা শুধু নমুনার। এলোমেলো সংখ্যা আসে xorshift64 নামের ছোট PRNG থেকে, নির্দিষ্ট seed 1977 দিয়ে — তাই প্রতিবার, আর দুই ভাষায়, হুবহু একই সংখ্যা। state % n-এ সামান্য পক্ষপাত আছে (264 কে n ভাগ না করলে), কিন্তু n = 3-এ তা প্রায় 10−19 স্তরের, এখানে অদৃশ্য।
from itertools import product
MASK, state = (1 << 64) - 1, 1977 # fixed seed: same numbers on every run
def rnd(n): # xorshift64, then a number in 0..n-1
global state
state ^= (state << 13) & MASK; state ^= state >> 7; state ^= (state << 17) & MASK
return state % n
def naive(choice): # swap position i with ANY position
a = list("ABC")
for i in range(3): j = choice(i, 3); a[i], a[j] = a[j], a[i]
return "".join(a)
def fisher_yates(choice): # swap position i with a position 0..i only
a = list("ABC")
for i in range(2, 0, -1): j = choice(i, i + 1); a[i], a[j] = a[j], a[i]
return "".join(a)
PERMS = ["ABC", "ACB", "BAC", "BCA", "CAB", "CBA"]
def table(title, f1, f2):
print(title); print("perm naive fisher-yates")
for p in PERMS: print(f"{p} {f1.count(p):6d} {f2.count(p):12d}")
# 1) exact: every possible sequence of choices, each equally likely
nv = [naive(lambda i, n: c[i]) for c in product(range(3), repeat=3)]
fy = [fisher_yates(lambda i, n: c[2 - i]) for c in product(range(3), range(2))]
table(f"all choice sequences: naive {len(nv)}, fisher-yates {len(fy)}", nv, fy)
# 2) simulation with the fixed-seed generator
T = 60000
nv = [naive(lambda i, n: rnd(n)) for _ in range(T)]
fy = [fisher_yates(lambda i, n: rnd(n)) for _ in range(T)]
table(f"simulation: {T} shuffles each (fair = {T // 6})", nv, fy)আউটপুট C++-এর সাথে হুবহু এক:
all choice sequences: naive 27, fisher-yates 6
perm naive fisher-yates
ABC 4 1
ACB 5 1
BAC 5 1
BCA 5 1
CAB 4 1
CBA 4 1
simulation: 60000 shuffles each (fair = 10000)
perm naive fisher-yates
ABC 8870 9927
ACB 11110 10160
BAC 11236 9896
BCA 11030 9851
CAB 8771 10078
CBA 8983 10088
Python-এর int অসীম বড় হতে পারে, তাই প্রতিটি বাঁ-shift-এর পর & MASK দিয়ে 64 বিটে কাটা হয়েছে — নইলে C++-এর uint64_t-এর সাথে সংখ্যা মিলত না। itertools.product সব পছন্দের ক্রম তৈরি করে; choice ফাংশনটি একবার একটি নির্দিষ্ট ক্রম থেকে, একবার PRNG থেকে সংখ্যা দেয় — একই shuffle কোড দুই কাজে। খরচ: প্রতিটি shuffle Θ(n), মোট Θ(T · n).
পরীক্ষাগার — Freivalds আর reservoir একসাথে
দ্বিতীয় প্রোগ্রামটি অংশ ৪-এর matrix নিয়ে Freivalds চালায় (প্রথম তিনটি r ডায়াগ্রামের মতোই), তারপর হাজার হাজার বার চালিয়ে ভুল ধরার হার মাপে। শেষে stream 1..10 থেকে k = 3-এর reservoir sampling ৩০০০০ বার চালিয়ে দেখে প্রতিটি সংখ্যা কতবার নমুনায় এসেছে।
#include <array>
#include <cstdio>
#include <vector>
using namespace std;
unsigned long long state = 1977; // fixed seed: same numbers on every run
int rnd(int n) { // xorshift64, then a number in 0..n-1
state ^= state << 13; state ^= state >> 7; state ^= state << 17; return int(state % n); }
using V = array<int, 3>; using M = array<V, 3>;
const M A = {{{1, 2, 0}, {0, 1, 3}, {2, 0, 1}}}, B = {{{1, 0, 2}, {3, 1, 0}, {0, 2, 1}}};
const M good = {{{7, 2, 2}, {3, 7, 3}, {2, 2, 5}}}; // the true product A*B
const M bad = {{{7, 2, 2}, {3, 7, 4}, {2, 2, 5}}}; // one wrong entry: row 1, column 2
V mul(const M& m, const V& v) { V o{}; for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) o[i] += m[i][j] * v[j]; return o; }
bool freivalds(const M& C, int k, bool show = false) { // true = "C looks like A*B"
for (int t = 1; t <= k; ++t) {
V r; for (int& x : r) x = rnd(2); // random 0/1 vector
bool ok = mul(A, mul(B, r)) == mul(C, r); // O(n^2) work, no matrix product
if (show) printf(" trial %d: r = %d %d %d -> %s\n", t, r[0], r[1], r[2], ok ? "equal" : "DIFFER: C != A*B");
if (!ok) return false;
}
return true;
}
vector<int> reservoir(int n, int k) { // Algorithm R on the stream 1..n, k slots
vector<int> res; // the reservoir: at most k items
for (int i = 1; i <= n; ++i)
if (i <= k) res.push_back(i);
else if (int j = rnd(i); j < k) res[j] = i; // j in 0..i-1, so P(j < k) = k/i
return res;
}
int main() {
puts("Freivalds on the wrong C, k = 10:"); freivalds(bad, 10, true);
int one = 0, ten = 0, rej = 0, cnt[11] = {};
for (int i = 0; i < 10000; ++i) one += !freivalds(bad, 1);
for (int i = 0; i < 10000; ++i) ten += freivalds(bad, 10);
for (int i = 0; i < 1000; ++i) rej += !freivalds(good, 10);
printf("k = 1 : wrong C caught in %d of 10000 runs (guarantee: at least half)\n", one);
printf("k = 10: wrong C slipped through %d of 10000 runs (bound: 10000 / 2^10 = 9.8)\n", ten);
printf("correct C rejected in %d of 1000 runs\n", rej);
for (int i = 0; i < 30000; ++i) for (int x : reservoir(10, 3)) ++cnt[x];
printf("reservoir k = 3 from 1..10, 30000 runs (fair = 9000 each):\n "); for (int x = 1; x <= 10; ++x) printf(" %d", cnt[x]); printf("\n");
}আউটপুট:
Freivalds on the wrong C, k = 10:
trial 1: r = 0 1 0 -> equal
trial 2: r = 0 0 0 -> equal
trial 3: r = 1 0 1 -> DIFFER: C != A*B
k = 1 : wrong C caught in 5043 of 10000 runs (guarantee: at least half)
k = 10: wrong C slipped through 8 of 10000 runs (bound: 10000 / 2^10 = 9.8)
correct C rejected in 0 of 1000 runs
reservoir k = 3 from 1..10, 30000 runs (fair = 9000 each):
9098 9102 8995 8948 8942 9011 9109 8922 9024 8849
প্রথম তিনটি ট্রায়াল ডায়াগ্রামের সাথে হুবহু মেলে: দুবার “সমান”, তৃতীয়বার ধরা পড়ল। এক ট্রায়ালে ধরা পড়ল ১০০০০-এর মধ্যে ৫০৪৩ বার — এখানে ভুল ঘর একটি, তাই ধরা পড়ে ঠিক যখন r2 = 1, সম্ভাবনা ঠিক 1/2; অসমতার “অন্তত” অংশটি অন্য ভুলের জন্য। k = 10-এ ১০০০০ বারে মাত্র ৮ বার ভুল C পার পেয়েছে — সীমা 10000/1024 ≈ 9.8-এর কাছাকাছি। সঠিক C একবারও বাতিল হয়নি: one-sided error. Reservoir-এর দশটি সংখ্যাই ৯০০০-এর কাছে (৮৮৪৯ থেকে ৯১০৯) — 1..10-এর প্রত্যেকে প্রায় k/n = 3/10 সম্ভাবনায়। খরচ: প্রতি Freivalds ট্রায়াল Θ(n²), প্রতি reservoir চালানো Θ(n) সময় আর Θ(k) জায়গা।
MASK, state = (1 << 64) - 1, 1977 # fixed seed: same numbers on every run
def rnd(n): # xorshift64, then a number in 0..n-1
global state
state ^= (state << 13) & MASK; state ^= state >> 7; state ^= (state << 17) & MASK
return state % n
A = [[1, 2, 0], [0, 1, 3], [2, 0, 1]]
B = [[1, 0, 2], [3, 1, 0], [0, 2, 1]]
good = [[7, 2, 2], [3, 7, 3], [2, 2, 5]] # the true product A*B
bad = [[7, 2, 2], [3, 7, 4], [2, 2, 5]] # one wrong entry: row 1, column 2
def mul(M, v): return [sum(M[i][j] * v[j] for j in range(3)) for i in range(3)]
def freivalds(C, k, show=False): # True = "C looks like A*B"
for t in range(1, k + 1):
r = [rnd(2) for _ in range(3)] # random 0/1 vector
ok = mul(A, mul(B, r)) == mul(C, r) # O(n^2) work, no matrix product
if show: print(f" trial {t}: r = {r[0]} {r[1]} {r[2]} ->", "equal" if ok else "DIFFER: C != A*B")
if not ok: return False
return True
print("Freivalds on the wrong C, k = 10:"); freivalds(bad, 10, True)
one = sum(not freivalds(bad, 1) for _ in range(10000))
ten = sum(freivalds(bad, 10) for _ in range(10000))
print(f"k = 1 : wrong C caught in {one} of 10000 runs (guarantee: at least half)")
print(f"k = 10: wrong C slipped through {ten} of 10000 runs (bound: 10000 / 2^10 = 9.8)")
print(f"correct C rejected in {sum(not freivalds(good, 10) for _ in range(1000))} of 1000 runs")
def reservoir(stream, k): # Algorithm R: one pass, k slots
res = []
for i, x in enumerate(stream, 1):
if i <= k: res.append(x)
else:
j = rnd(i) # 0..i-1, so P(j < k) = k/i
if j < k: res[j] = x
return res
cnt = [0] * 11
for _ in range(30000):
for x in reservoir(range(1, 11), 3): cnt[x] += 1
print("reservoir k = 3 from 1..10, 30000 runs (fair = 9000 each):")
print(" ", *cnt[1:])আউটপুট C++-এর সাথে হুবহু এক:
Freivalds on the wrong C, k = 10:
trial 1: r = 0 1 0 -> equal
trial 2: r = 0 0 0 -> equal
trial 3: r = 1 0 1 -> DIFFER: C != A*B
k = 1 : wrong C caught in 5043 of 10000 runs (guarantee: at least half)
k = 10: wrong C slipped through 8 of 10000 runs (bound: 10000 / 2^10 = 9.8)
correct C rejected in 0 of 1000 runs
reservoir k = 3 from 1..10, 30000 runs (fair = 9000 each):
9098 9102 8995 8948 8942 9011 9109 8922 9024 8849
দুই ভাষায় একই ফলের শর্ত: PRNG একই, আর rnd ডাকা হয় একই ক্রমে — যেমন reservoir-এ শুধু i > k হলে। sum(not freivalds(...) for ...) True/False-কে 1/0 হিসেবে যোগ করে। লক্ষ করুন, ভুল ধরা পড়লে freivalds সঙ্গে সঙ্গে ফেরে; ভুল C-তে গড়ে প্রায় ২টি ট্রায়ালেই কাজ শেষ, শুধু সঠিক C-তে পুরো k টি লাগে।
৮ · Hashing, birthday paradox আর এলোমেলো সংখ্যার উৎস
এলোমেলো seed-এর hash — universal hashing
পর্ব ৩২-এ দেখেছেন: যেকোনো একটি নির্দিষ্ট hash function-এর জন্য খারাপ key-এর সেট আছে। সমাধান universal hashing (Carter ও Wegman, ১৯৭৯): প্রোগ্রাম শুরুতে একটি পরিবার থেকে এলোমেলোভাবে hash function বেছে নেয়, যেমন h(x) = ((a·x + b) mod p) mod m, যেখানে a, b এলোমেলো। তখন যেকোনো দুটি আলাদা key-র collision-এর সম্ভাবনা ≤ 1/m, আর indicator + linearity দিয়ে একটি খোঁজার প্রত্যাশিত খরচ O(1 + n/m). Python 3.3 থেকে প্রতিটি প্রক্রিয়ায় string-এর hash এলোমেলো seed-এ চলে; কনটেস্টে unordered_map-এর বিরুদ্ধে anti-hash test এড়াতেও একই কৌশল — এলোমেলো seed-এর custom hash.
Birthday paradox — collision আসে প্রত্যাশার আগেই
একটি ক্লাসে কতজন হলে দুজনের একই জন্মদিনের সম্ভাবনা অর্ধেক ছাড়ায়? উত্তর মাত্র ২৩ (প্রায় ৫০.৭%) — এটি birthday paradox. Indicator দিয়ে সহজ ব্যাখ্যা: n জন, m = 365 দিন। প্রতিটি জোড়ার জন্য একটি indicator, P = 1/m. জোড়া C(n, 2) টি, তাই প্রত্যাশিত মিলে-যাওয়া জোড়া = n(n − 1)/(2m). n = 23-এ 253/365 ≈ 0.69. জোড়ার সংখ্যা n²-এর মতো বাড়ে, তাই মিল আসে প্রায় √m জনেই। Hashing-এ এর মানে: m টি ঘরে মাত্র প্রায় 1.18√m টি key-তেই collision-এর সম্ভাবনা অর্ধেক। 64-bit hash-এ প্রায় ৫০০ কোটি key-তেই collision-এর সম্ভাবনা অর্ধেক ছাড়ায় — তাই কনটেস্টে string hashing-এ (পর্ব ৫৮) দুটি mod বা 64-bit hash নেওয়া হয়, আর একই কারণে cryptographic hash-এর আউটপুট লম্বা রাখা হয়।
PRNG বনাম cryptographic RNG — কোনটি কোথায়
কম্পিউটারের pseudorandom number generator একটি সূত্র: seed থেকে একটি লম্বা, এলোমেলো-দেখতে ধারা। Seed জানলে পুরো ধারা জানা। এই পর্বের xorshift বা C++-এর mt19937 (Mersenne Twister) অ্যালগরিদম আর simulation-এর জন্য চমৎকার — দ্রুত, আর নির্দিষ্ট seed-এ reproducible. কিন্তু নিরাপত্তার জন্য একেবারে অচল: Mersenne Twister-এর মাত্র ৬২৪টি পরপর 32-bit আউটপুট দেখে তার পুরো ভেতরের অবস্থা বের করা যায়, তারপর ভবিষ্যতের প্রতিটি সংখ্যা।
নিরাপত্তার token-এ শুধু অপারেটিং সিস্টেমের CSPRNG. পাসওয়ার্ড রিসেট লিংক, OTP, session ID, API key বা লটারির বিজয়ী — যেখানে কেউ অনুমান করে লাভবান হতে পারেন — সেখানে ব্যবহার করুন cryptographically secure PRNG: Python-এ secrets মডিউল (যেমন secrets.token_urlsafe(32)), Linux-এ getrandom() বা /dev/urandom, Windows-এ BCryptGenRandom. কখনো rand(), random.random(), mt19937 বা “ঘড়ির সময় দিয়ে seed” নয়। C++-এর std::random_device সাধারণত OS-এর উৎস ব্যবহার করে, কিন্তু standard তা বাধ্যতামূলক করে না — নিরাপত্তার কোডে প্ল্যাটফর্মের ডকুমেন্টেশন দেখে নিশ্চিত হোন, অথবা পরীক্ষিত cryptography লাইব্রেরি ব্যবহার করুন।
৯ · তুলনা, ইতিহাস আর তত্ত্ব
| অ্যালগরিদম | ধরন | সময় | ভুলের সম্ভাবনা |
|---|---|---|---|
| Randomized quicksort | Las Vegas | প্রত্যাশিত O(n log n), worst O(n²) | ০ |
| Randomized quickselect | Las Vegas | প্রত্যাশিত O(n), worst O(n²) | ০ |
| Treap / skip list | Las Vegas | প্রতি কাজে প্রত্যাশিত O(log n) | ০ |
| Freivalds (k বার) | Monte Carlo, one-sided | Θ(k n²) | ≤ 2−k |
| Miller–Rabin (k টি base) | Monte Carlo, one-sided | O(k log n) টি modular গুণ | ≤ 4−k |
| Karger (N বার) | Monte Carlo, one-sided | প্রতি চালানো O(n²) | N = C(n,2) ln n-এ ≤ 1/n |
| Reservoir / Fisher–Yates | নির্ভুল নমুনা | Θ(n) | ০ (আউটপুট নিজেই এলোমেলো) |
রূপান্তর. Las Vegas → Monte Carlo: Markov দিয়ে, প্রত্যাশিত সময়ের দ্বিগুণে থামিয়ে (ব্যর্থতা ≤ 1/2). Monte Carlo → Las Vegas: উত্তর দ্রুত যাচাই করা গেলে, সঠিক উত্তর না পাওয়া পর্যন্ত বারবার চালান। প্রতিবার সাফল্যের সম্ভাবনা p হলে প্রত্যাশিত চেষ্টা 1/p — geometric distribution.
ইতিহাস. Michael O. Rabin ১৯৭৬ সালে “Probabilistic Algorithms” প্রবন্ধে randomized অ্যালগরিদমকে একটি পদ্ধতি হিসেবে তুলে ধরেন — এর একটি উদাহরণ ছিল সমতলের সবচেয়ে কাছের দুই বিন্দু খোঁজা। একই বছর Gary Miller দেখান, Riemann hypothesis-এর একটি সাধারণ রূপ সত্য হলে একটি deterministic primality test সম্ভব; Rabin ১৯৮০ সালে সেটিকে এলোমেলো base-এর শর্তহীন পরীক্ষায় রূপ দেন — আজকের Miller–Rabin. Robert Solovay আর Volker Strassen ১৯৭৭ সালে Jacobi symbol-ভিত্তিক আরেকটি randomized primality test প্রকাশ করেন। একই ১৯৭৭ সালে Rūsiņš Freivalds matrix গুণফল যাচাইয়ের পদ্ধতি দেন। David Karger contraction অ্যালগরিদম প্রকাশ করেন ১৯৯৩ সালে, আর একই বছর Stein-এর সাথে দ্রুততর রূপ। ২০০২ সালে IIT Kanpur-এর Manindra Agrawal, Neeraj Kayal ও Nitin Saxena (AKS) প্রমাণ করেন primality test deterministic polynomial সময়ে সম্ভব — তবু বাস্তবে আজও Miller–Rabin চলে, কারণ এটি অনেক দ্রুত।
আরও গভীরে — BPP, RP, ZPP আর derandomization: Complexity তত্ত্বে (পর্ব ৬৪) এলোমেলোতার শ্রেণিগুলো John Gill ১৯৭৭ সালে সংজ্ঞায়িত করেন। RP: polynomial সময়, “না” উত্তর সবসময় সঠিক, “হ্যাঁ”-তে অন্তত 1/2 সম্ভাবনায় ধরা পড়ে (one-sided — Freivalds বা Miller–Rabin-এর ধাঁচ)। ZPP: প্রত্যাশিত polynomial সময়ের Las Vegas; ZPP = RP ∩ co-RP. BPP: দুই দিকেই ভুল সম্ভব, কিন্তু প্রতিবার ≤ 1/3 — সংখ্যাগরিষ্ঠ ভোট আর Chernoff দিয়ে যত খুশি কমানো যায়। বড় খোলা প্রশ্ন: P = BPP কি? ১৯৯৭ সালে Impagliazzo ও Wigderson দেখান, একটি বিশ্বাসযোগ্য circuit lower bound সত্য হলে P = BPP — অর্থাৎ এলোমেলোতা হয়তো মূলত সুবিধা, অপরিহার্য শক্তি নয়। এলোমেলো অ্যালগরিদমকে deterministic বানানোর এই চেষ্টার নাম derandomization; ছোট উদাহরণ method of conditional expectations, আর বড় উদাহরণ AKS.
পরীক্ষা ও ইন্টারভিউতে:
- Las Vegas আর Monte Carlo-র পার্থক্য কী? একটি করে উদাহরণ দিন। — Las Vegas: সবসময় সঠিক, সময় এলোমেলো (randomized quicksort); Monte Carlo: সময় নির্দিষ্ট, ছোট সম্ভাবনায় ভুল (Miller–Rabin).
- Randomized quicksort-এ zi আর zj তুলনার সম্ভাবনা কত, আর কেন? — 2/(j − i + 1): Zij থেকে প্রথম pivot হতে হবে zi বা zj-কে।
- অজানা দৈর্ঘ্যের linked list থেকে একবার হেঁটে একটি এলোমেলো node বাছুন। — k = 1-এর reservoir sampling: i-তম node-কে 1/i সম্ভাবনায় উত্তর বানান; প্রত্যেকে 1/n.
- একটি অ্যারে ন্যায্যভাবে shuffle করুন — আর “প্রতি i-তে যেকোনো j-র সাথে swap” কেন ভুল? — Fisher–Yates (j ∈ [0, i]); naive-এর nn টি পথ n! টি সমান দলে ভাগ হয় না।
- Freivalds-এ ভুলের সম্ভাবনা 10−9-এর নিচে নামাতে কত বার? — 2−k ≤ 10−9 ⇒ k = 30 (230 ≈ 1.07 × 109).
১০ · AI-এর যুগে randomized অ্যালগরিদম — এলোমেলো কোড, নির্দিষ্ট যাচাই
Randomized কোডের ভুল সবচেয়ে চতুরভাবে লুকায়। পক্ষপাতদুষ্ট shuffle-ও “এলোমেলো দেখায়”; ভুল Miller–Rabin হাজার সংখ্যায় ঠিক উত্তর দিয়ে একটি বিশেষ যৌগিক সংখ্যায় ভুল করে। AI সহকারী এসব কোড মুহূর্তে লেখে, আর প্রায়ই আত্মবিশ্বাসী কিন্তু অসম্পূর্ণ দাবি করে — “এটি O(n log n)”, “এই base-গুলো যথেষ্ট”.
Randomized কোডে AI সাধারণত যেসব ভুল করে
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
Shuffle-এ j = rand() % n (naive), বা Fisher–Yates-এ j-এর পরিসর 0..i−1 (Sattolo-র রূপ — শুধু চক্রাকার permutation দেয়) | n = 3-এ ৬০০০০ বার চালিয়ে ছয়টি permutation গুনুন; সবগুলো প্রায় সমান আর কোনোটি শূন্য নয় তো? |
| Reservoir-এ j-কে 0..i−1-এর বদলে 1..i বা 0..i থেকে নেওয়া | ছোট stream-এ হাজার বার চালিয়ে প্রতিটি উপাদানের হার k/n কি না দেখুন |
Miller–Rabin-এ “base 2, 3 যথেষ্ট” বা 64-bit গুণে __int128 নেই | 1373653, 3215031751-এর মতো পরিচিত strong pseudoprime আর 264-এর কাছের বড় মৌলিক সংখ্যায় একটি ধীর কিন্তু নিশ্চিত পদ্ধতির সাথে মেলান |
| “Randomized quicksort worst case O(n log n)”, “Karger সবসময় min cut দেয়” | সঠিক বাক্য চান: প্রত্যাশিত বনাম worst case; Monte Carlo-র ভুলের সম্ভাবনা আর পুনরাবৃত্তির সংখ্যা |
নিরাপত্তার token-এ random বা mt19937, ঘড়ির সময়ের seed | প্রশ্ন করুন: আক্রমণকারী seed অনুমান করতে পারবেন কি? উত্তর হ্যাঁ হলে secrets/OS CSPRNG |
| প্রতিবার নতুন seed, তাই ব্যর্থ পরীক্ষা আর আবার ঘটানো যায় না | পরীক্ষায় নির্দিষ্ট seed, আর seed লগে লিখে রাখা |
AI-কে কোড লিখে দিতে না বলে, আপনার কোড, trace আর প্রমাণ যাচাই করতে বলুন:
Randomized কোড যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Primality test-এর অর্ধশতাব্দী — Miller, Rabin, Solovay–Strassen থেকে AKS; deterministic polynomial পদ্ধতি থাকা সত্ত্বেও কেন বাস্তবে আজও Miller–Rabin?
১৯৭০-এর দশকে public-key cryptography জন্ম নেয়: ১৯৭৬ সালে Diffie–Hellman, ১৯৭৭ সালে Rivest, Shamir ও Adleman-এর RSA. RSA-র চাবি বানাতে কয়েকশো অঙ্কের দুটি মৌলিক সংখ্যা লাগে, আর এমন সংখ্যা খুঁজতে দ্রুত primality test অপরিহার্য। ঠিক তখনই কয়েকটি পদ্ধতি আসে। Gary Miller ১৯৭৬ সালে একটি deterministic পরীক্ষা দেন, কিন্তু তার সঠিকতা নির্ভর করত generalized Riemann hypothesis-এর ওপর — গণিতের একটি অপ্রমাণিত অনুমান। Solovay ও Strassen ১৯৭৭ সালে একটি এলোমেলো পরীক্ষা দেন, যার ভুলের সম্ভাবনা প্রতি base-এ ≤ 1/2. Rabin ১৯৮০ সালে Miller-এর পরীক্ষাকে এলোমেলো base দিয়ে শর্তহীন করেন, আর প্রমাণ করেন ভুলের সম্ভাবনা ≤ 1/4 — একই বছর Louis Monier-ও স্বাধীনভাবে।
দীর্ঘদিনের প্রশ্ন ছিল: এলোমেলোতা ছাড়া কি polynomial সময়ে primality বলা যায়? ২০০২ সালে IIT Kanpur-এর অধ্যাপক Manindra Agrawal আর তাঁর দুই স্নাতক ছাত্র Neeraj Kayal ও Nitin Saxena “PRIMES is in P” প্রবন্ধে হ্যাঁ বলেন। ফলটি তাত্ত্বিকভাবে যুগান্তকারী, আর পরে বেশ কয়েকটি বড় পুরস্কার পায়।
তবু বাস্তবে AKS প্রায় ব্যবহার হয় না। এর exponent উন্নত রূপেও প্রায় log⁶ n-এর কাছে, যেখানে Miller–Rabin-এর একটি base প্রায় log³ n (সাধারণ গুণে)। আর Miller–Rabin-এর ভুলের সম্ভাবনা ৪০টি base-এ 4−40 ≈ 10−24 — একটি কম্পিউটারের হার্ডওয়্যারে মহাজাগতিক রশ্মির কারণে বিট উল্টে যাওয়ার সম্ভাবনার চেয়েও অনেক কম। তাই OpenSSL-এর মতো লাইব্রেরি, আর অনেক ভাষার বড়-সংখ্যার লাইব্রেরি, Miller–Rabin (অনেক সময় অন্য একটি পরীক্ষার সাথে মিলিয়ে) ব্যবহার করে। ৬৪-বিট সংখ্যার জন্য নির্দিষ্ট base-এর সেট দিয়ে এটিকে পুরোপুরি deterministic করা যায়, যা কনটেস্টে প্রচলিত।
মূল উপলব্ধি: তত্ত্বে “deterministic polynomial” একটি বিশাল অর্জন, কিন্তু প্রকৌশলে প্রশ্ন হলো দ্রুততা আর নিয়ন্ত্রিত ঝুঁকি। ভুলের সম্ভাবনা যখন হার্ডওয়্যারের ভুলের চেয়েও কম, তখন এলোমেলো পদ্ধতিই বাস্তবে “নিশ্চিত”.
প্র ০২“Monte Carlo” আর “Las Vegas” নাম কোথা থেকে এল, আর এই দুই জাতকে কীভাবে একটি থেকে অন্যটিতে রূপান্তর করা যায়?
দ্বিতীয় বিশ্বযুদ্ধের সময় আর তার পরে যুক্তরাষ্ট্রের Los Alamos গবেষণাগারে নিউট্রন কীভাবে পদার্থের ভেতর দিয়ে ছড়ায়, তা সমীকরণে সমাধান করা কঠিন ছিল। গণিতবিদ Stanisław Ulam অসুস্থ অবস্থায় solitaire খেলতে খেলতে ভাবেন: সম্ভাবনা হিসাব না করে অনেকবার খেলাটি “চালিয়ে” দেখলেই তো হয়। John von Neumann-এর সাথে মিলে তিনি এই ধারণা কম্পিউটারে প্রয়োগ করেন, আর সহকর্মী Nicholas Metropolis নাম দেন “Monte Carlo” — মোনাকোর বিখ্যাত ক্যাসিনোর নামে, যেখানে Ulam-এর এক আত্মীয় জুয়া খেলতে যেতেন বলে গল্প প্রচলিত। ১৯৪৯ সালে Metropolis ও Ulam “The Monte Carlo Method” নামে প্রবন্ধ প্রকাশ করেন। এই simulation অর্থের Monte Carlo simulation আর অ্যালগরিদম অর্থের Monte Carlo একই পরিবারের: নির্দিষ্ট পরিমাণ এলোমেলো কাজ, উত্তর আনুমানিক বা কদাচিৎ ভুল।
“Las Vegas অ্যালগরিদম” নামটি অনেক পরের — ১৯৭৯ সালে László Babai graph isomorphism-এর একটি কাজে এই নাম দেন, Monte Carlo-র পাল্টা হিসেবে: জুয়ার শহর, কিন্তু এখানে আপনি নিশ্চিত জিতে ওঠেন, শুধু কখন তা অনিশ্চিত।
দুই জাতের সম্পর্ক সুন্দর। Las Vegas → Monte Carlo সবসময় সম্ভব: প্রত্যাশিত সময় T হলে Markov-এর অসমতায় 2T ধাপের বেশি লাগার সম্ভাবনা ≤ 1/2; তাই 2T-তে থামিয়ে “জানি না” বললে একটি নির্দিষ্ট-সময়ের অ্যালগরিদম পাওয়া যায়, ব্যর্থতা ≤ 1/2. উল্টো দিকে শর্ত লাগে: Monte Carlo-র উত্তর যদি দ্রুত যাচাই করা যায়, তাহলে যাচাইয়ে পাস না করা পর্যন্ত বারবার চালালে Las Vegas — সাফল্যের সম্ভাবনা p হলে প্রত্যাশিত 1/p বার। যেমন একটি সংখ্যার উৎপাদক খোঁজার এলোমেলো পদ্ধতি; ভাগ করে দেখলেই উত্তর যাচাই হয়। কিন্তু Karger-এর উত্তর “এটিই সবচেয়ে ছোট cut” দ্রুত যাচাইয়ের সহজ উপায় নেই, তাই সেটি Monte Carlo-ই থাকে।
মূল উপলব্ধি: দুই নামের পেছনে একটি প্রশ্ন: অনিশ্চয়তা কোথায় রাখবেন — সময়ে, না উত্তরে? যাচাই সহজ হলে উত্তরের অনিশ্চয়তাকে সময়ের অনিশ্চয়তায় বদলানো যায়।
প্র ০৩Karger-এর অ্যালগরিদম এত বিস্ময়কর কেন — যেখানে flow দিয়ে min cut খুঁজতে বড় যন্ত্রপাতি লাগে, সেখানে “এলোমেলো edge চাপো” কীভাবে কাজ করে?
Global min cut খোঁজার প্রচলিত deterministic পথ ছিল পর্ব ৪৭-এর max-flow: একটি vertex s ঠিক করে বাকি প্রতিটি t-র জন্য s-t min cut বের করা, মোট n − 1 বার flow — ভারী যন্ত্রপাতি, লম্বা কোড। ১৯৯০-এর দশকের শুরুতে Stanford-এর পিএইচডি ছাত্র David Karger দেখান, flow-এর কোনো ধারণা ছাড়াই, শুধু “এলোমেলো edge চাপো” দিয়ে সমস্যাটি সমাধান করা যায়। ১৯৯৩ সালে SODA কনফারেন্সে প্রকাশিত এই ফলের সৌন্দর্য হলো এর বিশ্লেষণ: মাত্র কয়েক লাইনের telescoping গুণফল।
কেন কাজ করে, তার অন্তর্দৃষ্টি: min cut-এর edge কম, আর বাকি edge বেশি. এলোমেলোভাবে edge বাছলে বেশিরভাগ সময় “ভেতরের” edge আসে, যা একই পাশের vertex-গুলোকে জোড়ে — এতে min cut অক্ষত থাকে। যত vertex কমে, ঝুঁকি বাড়ে (শেষ ধাপগুলোতে 2/3, 2/4 …), তাই একবারে সাফল্যের সম্ভাবনা মাত্র 2/(n(n − 1)) পর্যন্ত নামতে পারে। এখান থেকে Karger ও Clifford Stein-এর পর্যবেক্ষণ: ঝুঁকি শেষের দিকে কেন্দ্রীভূত, তাই n/√2 পর্যন্ত একবার চেপে, বাকি অংশটি দুবার আলাদাভাবে recursion-এ চালান। এতে মোট সময় O(n² log³ n), সাথে সাফল্যের সম্ভাবনা অনেক বেশি।
একই সময়ে deterministic পথও এগিয়েছে: Mechthild Stoer আর Frank Wagner ১৯৯৭ সালে একটি সরল deterministic অ্যালগরিদম প্রকাশ করেন, O(nm + n² log n) সময়ে, flow ছাড়াই। পরে Karger নিজে প্রায় রৈখিক সময়ের এলোমেলো অ্যালগরিদম দেন, আর সাম্প্রতিক বছরগুলোতে গবেষকরা প্রায় রৈখিক সময়ের deterministic অ্যালগরিদমও খুঁজে পেয়েছেন। Karger-এর একটি উপজাতও গুরুত্বপূর্ণ: যেকোনো গ্রাফে min cut-এর সংখ্যা ≤ C(n, 2) — সম্ভাবনার যুক্তি দিয়ে একটি গণনার উপপাদ্য।
মূল উপলব্ধি: এলোমেলোতা কখনো কখনো জটিল কাঠামো ছাড়াই সমস্যার মূল গঠন কাজে লাগায় — এখানে “min cut-এর edge বিরল”. আর সম্ভাবনার যুক্তি শুধু অ্যালগরিদম নয়, গণনার উপপাদ্যও প্রমাণ করে।
প্র ০৪অনলাইন পোকারের shuffle ফাঁস আর 52! — একটি ভুল shuffle আর দুর্বল seed কীভাবে টাকার খেলা ভেঙে দিল?
১৯৯০-এর দশকের শেষে Reliable Software Technologies নামের একটি নিরাপত্তা প্রতিষ্ঠানের গবেষকরা (Brad Arkin-সহ) একটি জনপ্রিয় অনলাইন পোকার সফটওয়্যারের প্রকাশিত shuffle কোড বিশ্লেষণ করেন, আর ১৯৯৯ সালে “How We Learned to Cheat at Online Poker” শিরোনামে ফল প্রকাশ করেন। তাঁরা দুটি সমস্যা পান। প্রথমত, shuffle-টি ছিল ঠিক এই পর্বের naive ধাঁচের — প্রতিটি অবস্থানকে যেকোনো অবস্থানের সাথে অদলবদল — তাই কিছু তাসের বিন্যাস অন্যগুলোর চেয়ে বেশি সম্ভাব্য। দ্বিতীয় আর গুরুতর সমস্যা: PRNG-এর seed আসত মধ্যরাত থেকে পার হওয়া মিলিসেকেন্ড থেকে। একদিনে মাত্র প্রায় ৮ কোটি ৬৪ লাখ মিলিসেকেন্ড, তাই সম্ভাব্য প্যাকেটও সর্বোচ্চ ততগুলো। সার্ভারের ঘড়ির সাথে নিজের ঘড়ি মিলিয়ে, হাতের কয়েকটি তাস দেখে তাঁরা কয়েক লাখ সম্ভাবনায় নামিয়ে আনেন, তারপর বাকি সবার তাস আর আসন্ন তাস হিসাব করে ফেলেন।
সংখ্যাগুলো ভাবুন। ৫২টি তাসের বিন্যাস 52! ≈ 8 × 1067 — প্রায় 2226. প্রতিটি বিন্যাস সম্ভব করতে PRNG-এর অবস্থায় অন্তত ২২৬ বিট এলোমেলোতা লাগে। একটি 32-bit seed থেকে আসে সর্বোচ্চ 232 ≈ ৪৩০ কোটি আলাদা প্যাকেট — 52!-এর তুলনায় শূন্যের কাছাকাছি একটি ভগ্নাংশ। অর্থাৎ shuffle অ্যালগরিদম নিখুঁত হলেও seed ছোট হলে বেশিরভাগ বিন্যাস কখনো আসেই না।
শিক্ষা তিনটি স্তরে। অ্যালগরিদম: Fisher–Yates ব্যবহার করুন, আর পরিসর 0..i ঠিক রাখুন। উৎস: অনুমানযোগ্য seed (ঘড়ি, process ID) কখনো নয়, cryptographic RNG থেকে যথেষ্ট বিট নিন। যাচাই: frequency test আর সব পথ গোনা — যেমন এই পর্বের প্রোগ্রাম। একই ধরনের পক্ষপাত দেখা গেছে “এলোমেলো তুলনা ফাংশন দিয়ে sort করে shuffle” কৌশলেও, যা প্রায়ই ওয়েবসাইটে ব্যবহৃত হয় এবং sort অ্যালগরিদম ভেদে অসমান ফল দেয়।
মূল উপলব্ধি: “এলোমেলো দেখায়” আর “সমান সম্ভাবনার” এক জিনিস নয়। ন্যায্য shuffle-এর জন্য সঠিক অ্যালগরিদম আর যথেষ্ট বড়, অনুমান-অযোগ্য এলোমেলোতা — দুটোই লাগে।
প্র ০৫Debian OpenSSL-এর ২০০৮ সালের ঘটনা — এলোমেলোতার উৎস দুর্বল হলে নিখুঁত cryptography-ও কীভাবে ভেঙে পড়ে?
২০০৬ সালে Debian Linux-এর একজন রক্ষণাবেক্ষণকারী OpenSSL-এর কোডে একটি memory-checking টুলের সতর্কবার্তা দেখে দুটি লাইন সরিয়ে দেন। লাইনগুলো দেখতে অপ্রয়োজনীয় ছিল, কিন্তু আসলে সেগুলোই এলোমেলো pool-এ অপারেটিং সিস্টেমের এলোমেলোতা যোগ করত। ফলে Debian আর Ubuntu-তে তৈরি সব চাবির এলোমেলোতা কার্যত শুধু process ID-র ওপর নির্ভর করতে লাগল — Linux-এ ডিফল্টভাবে যা সর্বোচ্চ 32768. অর্থাৎ একটি নির্দিষ্ট ধরন আর আকারের SSH বা SSL চাবি মাত্র কয়েক হাজার রকমের হতে পারত।
প্রায় দুই বছর পর, ২০০৮ সালের মে মাসে, Luciano Bello সমস্যাটি খুঁজে পান, আর Debian নিরাপত্তা সতর্কবার্তা প্রকাশ করে (CVE-2008-0166). আক্রমণকারী সব সম্ভাব্য চাবির তালিকা আগেই বানিয়ে রাখতে পারতেন, আর কোনো সার্ভারের public key দেখে মিলিয়ে private key বের করে ফেলতেন। RSA-র গণিত একটুও ভাঙেনি — ভেঙেছিল এলোমেলোতার উৎস। লাখো চাবি বাতিল করে নতুন বানাতে হয়েছিল।
এই পর্বের ভাষায়: randomized অ্যালগরিদমের সব নিশ্চয়তা ধরে নেয় মুদ্রাগুলো সত্যিই এলোমেলো আর প্রতিপক্ষের অজানা। Randomized quicksort-এ দুর্বল PRNG বড়জোর ধীর করে; cryptography-তে তা সম্পূর্ণ ধ্বংস ডেকে আনে। তাই দুটি জগৎ আলাদা রাখুন। অ্যালগরিদম আর simulation-এ দ্রুত PRNG আর নির্দিষ্ট seed — reproducibility-র জন্য। নিরাপত্তায় অপারেটিং সিস্টেমের CSPRNG, যেটি hardware-এর অনিশ্চিত ঘটনা থেকে এলোমেলোতা জমায় আর cryptographic নকশায় ছড়িয়ে দেয়। তত্ত্বের দিক থেকেও প্রশ্নটি গভীর: derandomization গবেষণা জিজ্ঞেস করে, কম এলোমেলো বিট দিয়ে কতটা কাজ চলে — আর pseudorandomness-এর তত্ত্ব cryptography আর complexity-কে একসূত্রে বাঁধে।
মূল উপলব্ধি: এলোমেলোতা একটি সম্পদ, আর তার মান যাচাই করতে হয়। অ্যালগরিদম যত নিখুঁতই হোক, অনুমানযোগ্য মুদ্রায় তার নিশ্চয়তা শূন্য।
অনুশীলন
খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।
-
হাতে trace করুন: (ক) অ্যারে
P Q R S T-এ Fisher–Yates চালান, যেখানে i = 4, 3, 2, 1-এর জন্য এলোমেলো j যথাক্রমে 2, 3, 0, 0. শেষ অ্যারে কী? (খ) Reservoir sampling, k = 2, stream5 8 1 9 4; i = 3, 4, 5-এ এলোমেলো j (0 থেকে i − 1) যথাক্রমে 0, 1, 3. শেষ নমুনা কী?(ক) শুরু P Q R S T. i = 4, j = 2: a[4] ↔ a[2] → P Q T S R. i = 3, j = 3: নিজের সাথে → P Q T S R. i = 2, j = 0: a[2] ↔ a[0] → T Q P S R. i = 1, j = 0: a[1] ↔ a[0] → Q T P S R. মোট সম্ভাব্য পথ 5 × 4 × 3 × 2 = 120 = 5!.
(খ) প্রথম দুটি: [5, 8]. i = 3 (1), j = 0 < 2 → ঘর 0-তে 1: [1, 8]. i = 4 (9), j = 1 < 2 → ঘর 1-এ 9: [1, 9]. i = 5 (4), j = 3, 2-এর কম নয় → বাদ। শেষ নমুনা {1, 9}; প্রতিটি উপাদানের থাকার সম্ভাবনা 2/5.
-
হিসাব করুন: (ক) Freivalds-এ ভুলের সম্ভাবনা 10−6-এর নিচে নামাতে কমপক্ষে কত বার? n = 2000-এ মোট কতগুলো গুণ (প্রায়)? (খ) ১০ vertex-এর গ্রাফে Karger একবারে সফল হওয়ার সম্ভাবনার নিম্নসীমা কত, আর ব্যর্থতা ≤ 1/n করতে কত বার চালাতে হবে (ln 10 ≈ 2.3)? (গ) Miller–Rabin-এ ১০টি এলোমেলো base নিলে একটি যৌগিক সংখ্যা পার পাওয়ার সম্ভাবনা সর্বোচ্চ কত?
(ক) 2−k < 10−6 ⇒ k ≥ 20 (220 = 1048576). প্রতি ট্রায়ালে 3n² = 1.2 × 107 গুণ, তাই মোট প্রায় 2.4 × 108 — পুরো গুণফলের n³ = 8 × 109-এর চেয়ে প্রায় ৩৩ গুণ কম।
(খ) 2/(n(n − 1)) = 2/90 = 1/45. N = C(10, 2) · ln 10 = 45 × 2.3 ≈ ১০৪ বার; তখন ব্যর্থতা ≤ e−ln 10 = 1/10.
(গ) প্রতি base-এ ≤ 1/4, স্বাধীন base-এ গুণফল: 4−10 = 2−20 ≈ 9.5 × 10−7.
-
ভেবে বলুন: (ক) ১০০০টি key-কে 106 ঘরের একটি hash table-এ এলোমেলোভাবে রাখলে প্রত্যাশিত collision-জোড়া কত? (খ) n = 4-এ naive shuffle-এর পথ কতগুলো, আর কেন সেটি কখনো ন্যায্য হতে পারে না? (গ) একটি Las Vegas অ্যালগরিদমের প্রত্যাশিত সময় 5 সেকেন্ড। ৫০ সেকেন্ডের বেশি লাগার সম্ভাবনা সম্পর্কে কী বলা যায়?
(ক) প্রতিটি জোড়ার জন্য indicator, P = 1/m. C(1000, 2) = 499500, তাই প্রত্যাশিত collision-জোড়া = 499500/106 ≈ 0.5. মাত্র ১০০০টি key, অথচ ঘর দশ লাখ — তবু প্রায় ৪০% সম্ভাবনায় (1 − e−0.5 ≈ 0.39) অন্তত একটি collision: birthday paradox.
(খ) 44 = 256 টি সমান-সম্ভাবনার পথ, permutation 4! = 24. প্রতিটি permutation-এর সম্ভাবনা হবে (পথের সংখ্যা)/256; সবাই 1/24 হতে হলে প্রত্যেকে 256/24 টি পথ পেত, যা পূর্ণসংখ্যা নয়। (3 সংখ্যাটি 24-কে ভাগ করে, 256-কে করে না।)
(গ) Markov: P(T ≥ 50) ≤ E[T]/50 = 5/50 = 1/10. স্বাধীন চালানো না জানলে এর চেয়ে ভালো বলা যায় না; কিন্তু 10 সেকেন্ডে থামিয়ে নতুন মুদ্রায় আবার শুরু করলে প্রতি চেষ্টায় ব্যর্থতা ≤ 1/2, তাই পাঁচ চেষ্টার পরও ব্যর্থ থাকার সম্ভাবনা ≤ 1/32 — স্বাধীনতা ব্যবহার করে অনেক শক্ত সীমা।
-
কোড লিখুন: Karger-এর একটি চালানো লিখুন DSU দিয়ে (পর্ব ৩৩): edge-এর তালিকা Fisher–Yates দিয়ে এলোমেলো করুন, তারপর Kruskal-এর মতো ক্রমে edge নিয়ে union করুন, যতক্ষণ না দুটি component থাকে। শেষে যত edge দুই component-এর মাঝে, সেটিই এই চালানোর cut. অনেকবার চালিয়ে ক্ষুদ্রতমটি নিন।
এলোমেলো ক্রমে edge নেওয়া আর প্রতি ধাপে এলোমেলো edge চাপা একই বণ্টন দেয় (যে edge দুটি একই supernode-এর মাঝে পড়ে, সেটি self-loop — union-এ বাদ যায়)।
rnd(n)যেকোনো PRNG;findpath compression-সহ।// one run of Karger with DSU; edges = list of (u, v), vertices 0..n-1 vector<int> par; int find(int x) { return par[x] == x ? x : par[x] = find(par[x]); } int kargerOnce(int n, vector<pair<int,int>> edges) { for (int i = (int)edges.size() - 1; i > 0; --i) swap(edges[i], edges[rnd(i + 1)]); // Fisher-Yates par.resize(n); for (int v = 0; v < n; ++v) par[v] = v; int comps = n; for (auto [u, v] : edges) { if (comps == 2) break; int a = find(u), b = find(v); if (a != b) { par[a] = b; --comps; } // contract this edge } int cut = 0; for (auto [u, v] : edges) cut += find(u) != find(v); // edges between the 2 supernodes return cut; } // best = min over N = n*(n-1)/2 * ln(n) runs of kargerOnce(n, edges)একটি চালানো O(m α(n)) — shuffle O(m), union-find প্রায় ধ্রুবক। অংশ ৬-এর দুই-ত্রিভুজ গ্রাফে কয়েকশো বার চালালে প্রায় প্রতিবারের ক্ষুদ্রতম উত্তর 1; যাচাই করতে ছোট গ্রাফে সব 2n−1 − 1 টি বিভাজন brute force-এ গুনে মেলান।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমবিজোড় সংখ্যা n-এর জন্য Miller–Rabin-এর একটি base-এর পরীক্ষার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- n − 1 কে 2s · d আকারে লেখা, যেখানে d বিজোড়
- 2 থেকে n − 2-এর মধ্যে একটি এলোমেলো base a বাছা
- x = ad mod n হিসাব করা; x = 1 বা n − 1 হলে এই base-এ পাস
- x-কে বারবার বর্গ করা (সর্বোচ্চ s − 1 বার); কখনো n − 1 এলে পাস
- কোনোটিই না ঘটলে n-কে নিশ্চিতভাবে যৌগিক ঘোষণা করা
নিচের কোনটি Monte Carlo অ্যালগরিদম — নির্দিষ্ট সময়ে শেষ হয়, কিন্তু ছোট সম্ভাবনায় ভুল উত্তর দিতে পারে?
- Randomized quicksort
- Freivalds-এর matrix গুণফল যাচাই
- Treap-এ একটি key খোঁজা
- Randomized quickselect
Randomized quicksort-এ সাজানো ক্রমের z3 আর z8 (সব উপাদান আলাদা) কখনো সরাসরি তুলনা হওয়ার সম্ভাবনা কত?
- 1/6
- 1/2
- 1/3
- 2/n, যেখানে n অ্যারের আকার
Reservoir sampling-এ (k = 5) মোট 100 টি উপাদানের একটি stream এল। 37-তম উপাদানটি শেষ নমুনায় থাকার সম্ভাবনা কত?
- 5/37
- 1/100
- অবস্থানের ওপর নির্ভর করে — পরে আসা উপাদান কম সুযোগ পায়
- 1/20
“প্রতিটি i = 0..n−1-এ 0..n−1 থেকে এলোমেলো j নিয়ে a[i] আর a[j] অদলবদল” — n = 3-এ এই shuffle ন্যায্য নয় কেন?
- কিছু permutation কখনোই আসে না
- 33 = 27টি সমান পথ, যা 3! = 6 দিয়ে বিভাজ্য নয় — তাই কিছু permutation বেশি আসে
- PRNG যথেষ্ট ভালো নয়; ভালো জেনারেটর দিলেই ঠিক হয়ে যায়
- j = i হলে নিজের সাথে অদলবদল হয়, যা নিষিদ্ধ
একটি ওয়েবসাইটের পাসওয়ার্ড-রিসেট লিংকের token বানাতে কোন উৎসটি উপযুক্ত?
- C++-এর
mt19937, ঘড়ির সময় দিয়ে seed - Python-এর
random.getrandbits(128) - Python-এর
secrets.token_urlsafe(32)বা অপারেটিং সিস্টেমের CSPRNG - নির্দিষ্ট seed-এর xorshift64, যাতে পুনরুৎপাদন করা যায়
n vertex-এর একটি গ্রাফে Karger-এর contraction অ্যালগরিদমের একটি চালানো একটি নির্দিষ্ট min cut খুঁজে পাওয়ার সম্ভাবনার নিম্নসীমা কত?
- 2 / (n(n − 1))
- 1/2
- 1/n!
- ১ — একবারেই নিশ্চিত
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Las Vegas = সবসময় সঠিক, সময় এলোমেলো (quicksort, quickselect, treap); Monte Carlo = সময় নির্দিষ্ট, ছোট সম্ভাবনায় ভুল (Freivalds, Miller–Rabin, Karger).
- গড়ের জন্য indicator + linearity: quicksort-এ P(zi, zj তুলনা) = 2/(j − i + 1) → প্রত্যাশিত < 2n Hn ≈ 1.39 n log₂ n.
- Amplification: one-sided ভুল ≤ 1/2 → k বারে ≤ 2−k; Miller–Rabin প্রতি base-এ ≤ 1/4; Karger একবারে ≥ 2/(n(n − 1)), তাই C(n, 2) ln n বার।
- Reservoir: i-তম উপাদান k/i সম্ভাবনায় ঢোকে, শেষে প্রত্যেকে k/n. Fisher–Yates: j ∈ [0, i], ঠিক n! টি পথ; naive shuffle-এর nn পথ পক্ষপাতদুষ্ট।
- অ্যালগরিদমে নির্দিষ্ট seed-এর PRNG (reproducibility); নিরাপত্তায় শুধু OS-এর CSPRNG (
secrets,getrandom).
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৬২ · Number theory — GCD, sieve আর modular arithmeticপরবর্তী পর্বModular exponentiation আর মৌলিক সংখ্যার গণিত।
- ডেটা স্ট্রাকচার ও অ্যালগরিদম · কোর্স হোম৭২টি পর্বপুরো কোর্সের রোডম্যাপ — নয়টি মডিউল।
- পর্ব ২২ · Quick sort, pivot আর quickselectআগের ভিত্তিRandom pivot, partition আর median of medians.
- পর্ব ৩৮ · Treap আর skip listআগের ভিত্তিএলোমেলোতা দিয়ে ভারসাম্য — Las Vegas কাঠামো।
- পর্ব ৩২ · Hash tableআগের ভিত্তিUniversal hashing, collision আর hash flooding.
- Python প্রোগ্রামিং · নিরাপদ Pythonপাশের ট্র্যাকsecrets মডিউল আর নিরাপত্তার ভালো অভ্যাস।
- C++ প্রোগ্রামিং · STL algorithmপাশের ট্র্যাকstd::shuffle, std::sample আর random জেনারেটরের সাথে কাজ।