সরাসরি পর্বে যান
পর্ব ০৬ · ৭২-এর মধ্যে · মডিউল ১

DSA-র গণিত — log, যোগফল আর ধারা

Math for DSA — Logarithms, Summations, Series & Counting

Taught in Bangla, this lesson covers the mathematics used in algorithm analysis: logarithm rules, change of base and log n!, floor, ceiling and modular arithmetic, arithmetic, geometric and harmonic series with proofs, bounding sums with integrals, permutations, combinations and the pigeonhole principle, and a recipe for turning nested loops into summations to find their Θ bound.

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

  • logarithm-এর সংজ্ঞা, নিয়ম, ভিত্তি বদল, আর binary search-এর গভীরতা ⌈log₂(n+1)⌉ কেন
  • floor, ceiling, exponential বৃদ্ধি আর modular arithmetic-এর মৌলিক নিয়ম
  • সমান্তর, গুণোত্তর ও harmonic ধারা, বর্গের যোগফল — প্রমাণসহ, আর integral দিয়ে যোগফলের সীমা
  • permutation, combination আর pigeonhole নীতি দিয়ে সম্ভাবনার সংখ্যা গোনা
  • যেকোনো nested লুপকে যোগফলে বদলে তার Θ বের করার ধাপে ধাপে পদ্ধতি

১ · কেন গণিত — অ্যালগরিদমের হিসাবের ভাষা

পর্ব ০৪-এ আপনি দেখেছেন, একটি অ্যালগরিদম কত দ্রুত তা আমরা মাপি “ইনপুট n বাড়লে ধাপের সংখ্যা কীভাবে বাড়ে” দিয়ে, আর লিখি Big-O বা Θ দিয়ে। পর্ব ০৫-এ দেখেছেন best, worst আর average case. কিন্তু একটা প্রশ্ন বাকি থেকে গেছে — ধাপের সংখ্যাটা আসলে গুনব কীভাবে?

উত্তরটা মজার। কোনো প্রোগ্রামের সময় প্রায় পুরোটাই খরচ হয় লুপে আর রিকার্শনে। আর লুপ গুনতে গেলে যে প্রশ্নগুলো বারবার আসে, সেগুলো আসলে স্কুলের গণিতের পুরোনো বন্ধু:

Logarithm

“প্রতি ধাপে সমস্যা অর্ধেক হলে কয়টি ধাপ লাগবে?” — binary search, balanced tree-এর উচ্চতা, merge sort-এর স্তর।

log₂ n

যোগফল (Σ)

“ভেতরের লুপ প্রতিবার আলাদা সংখ্যকবার চললে মোট কতবার?” — selection sort, সব জোড়া যাচাই।

n(n+1)/2

ধারা (series)

“খরচ প্রতিবার দ্বিগুণ বা অর্ধেক হলে মোট কত?” — vector-এর বড় হওয়া, heap তৈরি।

2k+1 − 1

গণনা

“সম্ভাব্য উত্তর কয়টি?” — সব subset, সব বিন্যাস, brute force কত বড়, collision কেন অনিবার্য।

n!, C(n,k), 2n

মুদি দোকানের হিসাবের খাতার কথা ভাবুন। দোকানি প্রতিটি জিনিসের দাম আলাদা করে যোগ করতে পারেন — ঠিক উত্তর আসবে, কিন্তু সময় লাগবে। আবার অভিজ্ঞ দোকানি জানেন, “১২টা ডিম, প্রতিটা ১৫ টাকা — মানে ১৮০।” তিনি একটি সূত্র ব্যবহার করলেন। অ্যালগরিদম বিশ্লেষণে আমরা ঠিক এটাই করি: লুপটি মনে মনে চালিয়ে না দেখে একটি সূত্র দিয়ে এক লাইনে বলে দিই — “এটা মোট n(n+1)/2 বার চলবে।”

ভয় পাওয়ার কিছু নেই। এই পর্বে ক্যালকুলাস প্রায় লাগবে না, লাগবে স্কুলের বীজগণিত আর একটু ধৈর্য। প্রতিটি সূত্র আমরা প্রথমে ছোট উদাহরণে হাতে মিলিয়ে দেখব, তারপর প্রমাণ করব, তারপর কম্পিউটারে বড় n-এ যাচাই করব। যাঁরা আগে থেকেই জানেন, তাঁদের জন্য প্রতিটি অংশে একটু গভীর কথাও থাকবে।

এই কোর্সের লেখার নিয়ম: কোনো ভিত্তি উল্লেখ না করে শুধু “log n” লিখলে বুঝবেন log₂ n. স্বাভাবিক লগারিদম (ভিত্তি e ≈ 2.718) লেখা হবে ln n. কোনো কোনো বইয়ে log₂ n-কে lg n লেখা হয় — একই জিনিস।

২ · Logarithm — কতবার অর্ধেক করা যায়

প্রথমে সংজ্ঞা। Logarithm হলো সূচক বা ঘাতের উল্টো প্রশ্ন:

logb x = y  ⇔  by = x
log₂ 8 = 3, কারণ 23 = 8  ·  log₁₀ 1000 = 3  ·  log₂ 1 = 0

এভাবে পড়লে log একটু শুকনো লাগে। কম্পিউটার বিজ্ঞানে log₂ n-কে পড়ুন অনেক সহজ একটি প্রশ্ন হিসেবে:

মূল ধারণা

log₂ n ≈ n-কে কতবার অর্ধেক করলে ১-এ পৌঁছায়। উল্টো দিক থেকে: ১-কে কতবার দ্বিগুণ করলে n হয়। পূর্ণসংখ্যার ভাগে (নিচের দিকে গোল করে) অর্ধেক করতে থাকলে ধাপের সংখ্যা ঠিক ⌊log₂ n⌋.

একটি পুরোনো ফোনবুকের কথা ভাবুন, ১০০০ পাতার, নাম বর্ণানুক্রমে সাজানো। “রহিম” খুঁজতে আপনি মাঝখানের পাতা খুললেন — সেখানে “মাহমুদ”. তাহলে রহিম পরের অর্ধেকে; প্রথম ৫০০ পাতা আর দেখার দরকার নেই। আবার বাকি অংশের মাঝখান খুললেন। প্রতিবার খোঁজার জায়গা অর্ধেক হচ্ছে: ১০০০ → ৫০০ → ২৫০ → ১২৫ → … মাত্র ১০ বারের মতো খুললেই এক পাতায় পৌঁছে যাবেন। কারণ 210 = 1024 ≥ 1000. এটাই log.

অর্ধেক করার কাজটি একটি ছোট while লুপ। Pseudocode (চলক পূর্ণসংখ্যা, ভাগ মানে নিচের দিকে গোল করা ভাগ):

HALVINGS(n)            // n ≥ 1
    steps ← 0
    while n > 1
        n ← ⌊n / 2⌋
        steps ← steps + 1
    return steps           // = ⌊log₂ n⌋

এবার n = ১০০ দিয়ে লুপটি ধাপে ধাপে চালিয়ে দেখুন। “পরে” চাপুন আর ডান দিকে n আর steps-এর মান খেয়াল করুন:

1int n = 100, steps = 0;2while (n > 1) {3    n = n / 2;      // floor4    steps++;5}6printf("steps = %d\n", steps);
ভ্যারিয়েবলের মান
n
steps
2^steps
  1. শুরু। n = ১০০, steps = ০. প্রশ্ন: কতবার অর্ধেক করলে n ১-এ নামবে?
  2. ১০০ > ১, তাই লুপে ঢুকল: n = ১০০ / ২ = ৫০, steps = ১.
  3. ৫০ / ২ = ২৫, steps = ২.
  4. ২৫ / ২ = ১২.৫ — কিন্তু পূর্ণসংখ্যার ভাগে দশমিক অংশ বাদ যায়, তাই n = ১২ (এটাই floor). steps = ৩.
  5. ১২ / ২ = ৬, steps = ৪.
  6. ৬ / ২ = ৩, steps = ৫.
  7. ৩ / ২ = ১.৫ → n = ১, steps = ৬. এখন শর্ত n > ১ মিথ্যা, লুপ থামবে।
  8. ফল steps = 6. মিলিয়ে দেখুন: 26 = ৬৪ ≤ ১০০ < ১২৮ = 27, তাই log₂ ১০০ ≈ ৬.৬৪ আর ⌊log₂ ১০০⌋ = ৬. লুপটি ঠিক এই সংখ্যাটিই গুনেছে।

Binary search-এর গভীরতা: ⌈log₂(n+1)⌉

পর্ব ১৮-এ আপনি binary search বিস্তারিত শিখবেন। এখানে শুধু হিসাবটা দেখি। প্রতিটি তুলনার পর খোঁজার জায়গা অন্তত অর্ধেক হয়ে যায়। worst case-এ সর্বোচ্চ যতগুলো তুলনা লাগে, তা হলো ⌊log₂ n⌋ + 1 = ⌈log₂(n+1)⌉. কেন +১? কারণ শেষে একটি উপাদান বাকি থাকলেও সেটিকে একবার দেখতে হয়। k টি তুলনায় সর্বোচ্চ 2k − 1 টি উপাদান সামলানো যায় (১ + ২ + ৪ + … — এই ধারা অংশ ৬-এ প্রমাণ করব), তাই 2k − 1 ≥ n হওয়া চাই, অর্থাৎ k ≥ log₂(n+1).

n (উপাদান)⌊log₂ n⌋ (অর্ধেক করার ধাপ)⌈log₂(n+1)⌉ (binary search-এর সর্বোচ্চ তুলনা)linear search-এর সর্বোচ্চ তুলনা
১০৩৪১০
১,০০০৯১০১,০০০
১০ লাখ (106)১৯২০১০ লাখ
১০০ কোটি (109)২৯৩০১০০ কোটি

শেষ সারিটি ভালো করে দেখুন। বাংলাদেশের মোট জনসংখ্যার কয়েক গুণ বড় একটি সাজানো তালিকাতেও মাত্র ৩০ বার দেখলেই যেকোনো নাম পাওয়া যায় বা নিশ্চিত হওয়া যায় যে নেই। log-এর এই “ধীরে বাড়া” স্বভাবই অসংখ্য দ্রুত ডেটা স্ট্রাকচারের মূল রহস্য। আরেকটি কাজের তথ্য: একটি ধনাত্মক পূর্ণসংখ্যা n লিখতে ঠিক ⌊log₂ n⌋ + 1 টি bit লাগে। যেমন ১০০ = 1100100 — সাতটি bit.

নিচের ছোট টুলটি চালান। শুরুতে এটি কয়েকটি n-এর জন্য হিসাব ছাপবে আর ১০০-কে অর্ধেক করার প্রতিটি ধাপ বার হিসেবে আঁকবে। তারপর ঘরে নিজের পছন্দের সংখ্যা লিখে “অর্ধেক করুন” চাপুন — বারগুলো কত দ্রুত ছোট হয়ে যায়, দেখুন:

// n-কে বারবার অর্ধেক (নিচের দিকে গোল) করলে কত ধাপে 1 হয়?
function halvings(n) {
  const seq = [n];
  while (n > 1) { n = Math.floor(n / 2); seq.push(n); }
  return seq;
}
function draw() {
  const n0 = Math.max(1, Math.floor(Number(document.getElementById('num').value) || 1));
  const seq = halvings(n0);
  document.getElementById('viz').innerHTML = seq.map((v, i) =>
    `<div class="row"><span class="lab">step ${i}: ${v}</span>` +
    `<span class="bar" style="width:${Math.max(0.5, 70 * v / n0)}%"></span></div>`).join('');
  document.getElementById('sumry').textContent =
    `${seq.length - 1} halvings, floor(log2 n) = ${Math.floor(Math.log2(n0))}`;
}
document.getElementById('go').onclick = draw;
draw();
for (const n of [1, 2, 7, 8, 100, 1000000, 1000000000])
  console.log(`n=${n}: halvings=${halvings(n).length - 1}, floor(log2 n)=` +
    `${Math.floor(Math.log2(n))}, ceil(log2(n+1))=${Math.ceil(Math.log2(n + 1))}`);

আউটপুটের প্রতিটি লাইনে “halvings” আর “floor(log2 n)” হুবহু মিলে যায়: n = 7 হলে halvings=2 (৭ → ৩ → ১), n = 8 হলে halvings=3 (৮ → ৪ → ২ → ১), n = 100 হলে halvings=6, n = 1000000 হলে halvings=19, ceil(log2(n+1))=20, আর n = 1000000000 হলে halvings=29, ceil(log2(n+1))=30. শেষ কলামটিই binary search-এর সর্বোচ্চ তুলনার সংখ্যা। খেয়াল করুন n = 7 আর n = 8-এর পার্থক্য: ৮ ঠিক 2-এর ঘাত, তাই সেখানে log₂ একটি পূর্ণসংখ্যা (৩) আর ⌈log₂(n+1)⌉ এক লাফে ৪ হয়ে যায়। বারগুলোর দৈর্ঘ্য n-এর সমানুপাতিক — প্রথম কয়েক ধাপেই প্রায় সব কাজ শেষ, তারপর বাকি থাকে ছোট ছোট টুকরো।

৩ · Log-এর নিয়ম, ভিত্তি বদল আর log n!

log নিয়ে হিসাব করতে কয়েকটি নিয়মই যথেষ্ট। সবগুলো এসেছে একটি সরল সত্য থেকে: ঘাত গুণ করলে সূচক যোগ হয় (2a · 2b = 2a+b). log সূচক নিয়ে কাজ করে, তাই গুণ log-এ গিয়ে যোগ হয়ে যায়।

নিয়মউদাহরণDSA-তে কোথায়
log(ab) = log a + log blog₂ 32 = log₂ 4 + log₂ 8 = 2 + 3log n! = log 1 + log 2 + … + log n
log(a/b) = log a − log blog₂(n/2) = log₂ n − 1recursion tree-এর প্রতিটি স্তরে একটি করে কমে
log(ak) = k · log alog₂(n3) = 3 log₂ nlog(n3) আর log n একই Θ শ্রেণির
blog_b x = x2log₂ n = nrecurrence সমাধানে স্তরের সংখ্যা বসানো
alog_b c = clog_b a4log₂ n = nlog₂ 4 = n2Master theorem-এর nlog_b a (পর্ব ০৯)
loga x = logb x / logb alog₂ n = log₁₀ n / log₁₀ 2 ≈ 3.32 · log₁₀ nBig-O-তে ভিত্তি লেখা হয় না

ভিত্তি বদল: কেন O(log n)-এ ভিত্তি লেখা হয় না

শেষ সারির নিয়মটির নাম change of base. এর মানে, log₂ n আর log₁₀ n-এর মধ্যে পার্থক্য শুধু একটি ধ্রুবক গুণক (প্রায় ৩.৩২)। Big-O ধ্রুবক গুণক উপেক্ষা করে, তাই O(log₂ n) = O(log₁₀ n) = O(ln n) — সবাই শুধু O(log n). একটি B-tree (পর্ব ৩০) যদি প্রতিটি node-এ ১০০ ভাগে ভাগ করে, তার উচ্চতা log₁₀₀ n — তবু সেটিও Θ(log n). ব্যবহারিক পার্থক্য অবশ্য আছে: ১০০ কোটিতে log₂ প্রায় ৩০, আর log₁₀₀ মাত্র ৪.৫. ডিস্কে প্রতিটি ধাপ দামি হলে এই ধ্রুবকই আসল খেলা।

সাবধান — ঘাতের ভেতরে ভিত্তি গুরুত্বপূর্ণ! log-এর বাইরে ধ্রুবক গুণক উপেক্ষা করা যায়, কিন্তু সূচকে নয়। 2log₂ n = n, অথচ 4log₂ n = n2. একইভাবে 22n = 4n, যা 2n-এর ধ্রুবক গুণ নয় — 2n গুণ বড়। আরেকটি সাধারণ ভুল: log(a + b) ≠ log a + log b. যোগের log-এর কোনো সহজ নিয়ম নেই।

log n! = Θ(n log n) — দুই লাইনের প্রমাণ

Factorial n! = 1 · 2 · 3 · … · n. প্রথম নিয়ম অনুযায়ী log n! = log 1 + log 2 + … + log n. এটি কত বড়? দুই দিক থেকে চেপে ধরি:

  • ঊর্ধ্বসীমা: প্রতিটি গুণক ≤ n, তাই n! ≤ nn, অর্থাৎ log n! ≤ n log n.
  • নিম্নসীমা: শেষের অর্ধেক গুণক (n/2 থেকে n) প্রতিটি ≥ n/2, আর এমন গুণক অন্তত n/2 টি। বাকিগুলো ≥ 1. তাই n! ≥ (n/2)n/2, অর্থাৎ log n! ≥ (n/2) · log(n/2) = (n/2)(log n − 1), যা n ≥ 4 হলে ≥ (n/4) log n.

দুই সীমাই n log n-এর ধ্রুবক গুণ, তাই log n! = Θ(n log n). আরও সূক্ষ্ম হিসাব দেয় Stirling's approximation:

n! ≈ √(2πn) · (n/e)n  ⇒  log₂ n! ≈ n log₂ n − 1.44 n

এর অন্তর্নিহিত ধারণাটা সহজ: n! আসলে প্রায় n টি গুণকের গুণফল, যাদের “গড় আকার” প্রায় n/e. তাই n! মোটামুটি (n/e)n. পর্ব ২৪-এ এই সূত্রটিই প্রমাণ করবে যে শুধু তুলনা করে সাজায় এমন যেকোনো সর্টের worst case-এ অন্তত log₂ n! ≈ n log₂ n তুলনা লাগবেই — merge sort বা heapsort-এর চেয়ে ভালো (ধ্রুবক বাদে) তুলনা-সর্ট সম্ভব নয়।

৪ · Floor, ceiling, exponential আর modular arithmetic

Floor আর ceiling — গোল করার দুই দিক

অ্যালগরিদমে n টি জিনিস প্রায়ই সমান দুই ভাগে ভাগ হয় না। ৭টি জিনিসের অর্ধেক ৩.৫ — কিন্তু অর্ধেক জিনিস তো হয় না! তখন লাগে floor ⌊x⌋ (নিচের দিকে গোল, ⌊3.5⌋ = 3) আর ceiling ⌈x⌉ (ওপরের দিকে গোল, ⌈3.5⌉ = 4). কয়েকটি কাজের তথ্য:

  • ⌊n/2⌋ + ⌈n/2⌉ = n — merge sort যখন ৭টিকে ৩ আর ৪-এ ভাগ করে, কিছুই হারায় না।
  • ধনাত্মক পূর্ণসংখ্যা a, b-এর জন্য ⌈a/b⌉ = ⌊(a + b − 1)/b⌋. যেমন ২৩ জন যাত্রী, প্রতি রিকশায় ২ জন — লাগবে ⌈23/2⌉ = (23 + 1)/2 = ১২টি রিকশা। কোডে লিখুন (a + b - 1) / b, float-এর ceil নয়।
  • C++-এর / শূন্যের দিকে কাটে: -7 / 2 = −3. Python-এর // সত্যিকারের floor: -7 // 2 = −4. ঋণাত্মক সংখ্যায় দুই ভাষার উত্তর আলাদা!

Exponential — দ্বিগুণের ভয়ংকর গতি

log-এর ঠিক উল্টো হলো 2n. এর বৃদ্ধি এত দ্রুত যে মানুষের অনুভূতি তাল রাখতে পারে না। দাবার বোর্ডের প্রচলিত লোককথাটি হয়তো শুনেছেন: প্রথম ঘরে ১টি চাল, পরের ঘরে ২টি, তারপর ৪টি — প্রতি ঘরে দ্বিগুণ। ৬৪টি ঘরে মোট 264 − 1 টি চাল, যা পৃথিবীর সব ধানখেতের বহু বছরের ফলনের চেয়েও বেশি। Exponential growth চেনার জন্য কয়েকটি মান মনে রাখুন:

ঘাতমান (প্রায়)কেন কাজের
2101,024 ≈ 103“এক হাজার” — ১ KB
2201,048,576 ≈ 106n = 20-এ সব subset গোনা চলে
230≈ 1.07 × 109প্রায় ১০০ কোটি — সেকেন্ডে 108 কাজ ধরলে প্রায় ১০ সেকেন্ড
231 − 12,147,483,647৩২-bit int-এর সর্বোচ্চ মান
263 − 1≈ 9.22 × 1018long long-এর সর্বোচ্চ মান

কনটেস্টের একটি চালু নিয়ম: “এক সেকেন্ডে মোটামুটি 108 সরল কাজ।” তাহলে n = ২০ হলে 2n ≈ ১০ লাখ — নির্ভয়ে সব সম্ভাবনা দেখা যায়; n = ৪০ হলে 240 ≈ 1012 — কয়েক ঘণ্টা! n মাত্র দ্বিগুণ হলো, কিন্তু সময় বাড়ল প্রায় দশ লাখ গুণ।

Modular arithmetic — ঘড়ির অঙ্ক

দেয়ালঘড়ি ১২টার পর ১৩টা দেখায় না, আবার ১ থেকে শুরু করে। এটাই modular arithmetic: a mod m মানে a-কে m দিয়ে ভাগ করলে যে ভাগশেষ থাকে (০ থেকে m − 1). যেমন 17 mod 5 = 2. এর সবচেয়ে কাজের বৈশিষ্ট্য — যোগ, বিয়োগ আর গুণের প্রতিটি ধাপে mod নিলেও শেষ ফল বদলায় না:

(a + b) mod m = ((a mod m) + (b mod m)) mod m
(a · b) mod m = ((a mod m) · (b mod m)) mod m

এজন্যই কনটেস্টে প্রায়ই লেখা থাকে “উত্তর 109 + 7 দিয়ে mod করে দিন।” উত্তর হয়তো ১০০০ অঙ্কের সংখ্যা, কিন্তু প্রতিটি ধাপে mod নিলে সব সংখ্যা long long-এ ধরে যায় (দুটি ১০০ কোটির কম সংখ্যার গুণফল ≈ 1018 < 9.22 × 1018). দুটি সতর্কতা: ভাগের জন্য এমন সহজ নিয়ম নেই — সেখানে লাগে modular inverse; আর C++-এ -7 % 3 = −1, অথচ Python-এ -7 % 3 = 2. C++-এ নিরাপদ রূপ: ((a % m) + m) % m. GCD, sieve, দ্রুত ঘাত আর modular inverse নিয়ে পূর্ণ আলোচনা পর্ব ৬২-তে।

৫ · সমান্তর ধারা — Gauss-এর জোড়া বানানোর কৌশল

অঙ্কের ইতিহাসে একটি প্রচলিত গল্প আছে। অষ্টাদশ শতকের শেষ দিকে জার্মানির এক স্কুলে শিক্ষক ক্লাসকে ব্যস্ত রাখতে ১ থেকে ১০০ পর্যন্ত সব সংখ্যা যোগ করতে দিলেন। ছোট্ট Carl Friedrich Gauss প্রায় সঙ্গে সঙ্গেই উত্তর লিখে দিলেন — ৫০৫০. গল্পের খুঁটিনাটি (সংখ্যাগুলো ঠিক কী ছিল, Gauss-এর বয়স কত) নিয়ে ইতিহাসবিদদের মধ্যে মতভেদ আছে, কিন্তু কৌশলটি নিখুঁত: সামনের আর পেছনের সংখ্যা জোড়া বাঁধুন।

নিচের ডায়াগ্রামে n = ১০ দিয়ে কৌশলটি দেখুন। প্রতিটি ধাপে একটি জোড়া লাল দাগে চিহ্নিত হবে:

1সামনে থেকে
2
3
4
5
10পেছন থেকে
9
8
7
6
হিসাব
জোড়া
জোড়ার যোগফল
এ পর্যন্ত মোট
  1. প্রথম জোড়া: সবচেয়ে ছোট ১ আর সবচেয়ে বড় ১০ — যোগফল ১১.
  2. দ্বিতীয় জোড়া: ২ আর ৯. একটি ১ বেড়েছে, আরেকটি ১ কমেছে — যোগফল আবার ১১. মোট এখন ২২.
  3. ৩ + ৮ = ১১. প্যাটার্নটি চোখে পড়ছে? প্রতিটি জোড়ার যোগফল একই। মোট ৩৩.
  4. ৪ + ৭ = ১১, মোট ৪৪.
  5. ৫ + ৬ = ১১, মোট ৫৫. সব সংখ্যা জোড়ায় বাঁধা শেষ।
  6. সাধারণ সূত্র: n টি সংখ্যায় n/2 টি জোড়া, প্রতিটির যোগফল n + 1. তাই মোট = n(n+1)/2 = ১০ × ১১ / ২ = ৫৫. n = ১০০ হলে ১০০ × ১০১ / ২ = ৫০৫০ — Gauss-এর উত্তর।

n বিজোড় হলে কী হবে? মাঝের সংখ্যাটির জোড়া থাকে না। তাই প্রমাণটা আরেকটু পরিচ্ছন্নভাবে লিখি: যোগফল S-কে একবার সামনে থেকে, একবার পেছন থেকে লিখে দুটো যোগ করুন।

S = 1 + 2 + … + n
S = n + (n−1) + … + 1
2S = (n+1) + (n+1) + … + (n+1) = n(n+1)  ⇒  S = n(n+1)/2

এখানে n জোড় না বিজোড় তা লাগেনি — প্রতিটি কলামে যোগফল n + 1, আর কলাম n টি। এই closed form দেখে Θ বলা সহজ: n(n+1)/2 = n²/2 + n/2 = Θ(n²).

মূল ধারণা

যেকোনো সমান্তর ধারা (পরপর পদের পার্থক্য সমান): যোগফল = (প্রথম পদ + শেষ পদ) × পদসংখ্যা ÷ ২. যেমন selection sort (পর্ব ২০) প্রথম ধাপে n − 1 টি তুলনা করে, তারপর n − 2, … , শেষে ১. মোট (n−1) + (n−2) + … + 1 = n(n−1)/2 = Θ(n²)।

বর্গের যোগফল আর সাধারণ নিয়ম

তিন স্তরের লুপে প্রায়ই আসে 1² + 2² + … + n². এর সূত্র:

Σi=1n i² = n(n+1)(2n+1)/6 ≈ n³/3 = Θ(n³)

প্রমাণটা induction দিয়ে সহজ (পর্ব ০৩-এর পদ্ধতি): n = 1-এ দুই দিকেই ১; আর n-এর জন্য সত্য ধরে নিলে n(n+1)(2n+1)/6 + (n+1)² সরল করলে ঠিক (n+1)(n+2)(2n+3)/6 আসে — নিজে কাগজে মিলিয়ে দেখুন। সাধারণ নিয়মটি মনে রাখার মতো: Σ ik = Θ(nk+1), অর্থাৎ যোগ করলে ঘাত এক বাড়ে। কারণ? n টি পদ, যার অর্ধেক পদই অন্তত (n/2)k — তাই যোগফল ≥ (n/2)·(n/2)k; আবার প্রতিটি পদ ≤ nk, তাই যোগফল ≤ n · nk.

৬ · গুণোত্তর ধারা — দ্বিগুণের জাদু

গুণোত্তর ধারায় প্রতিটি পদ আগেরটির একটি নির্দিষ্ট গুণ (r). সবচেয়ে গুরুত্বপূর্ণটি r = 2:

1 + 2 + 4 + … + 2k = 2k+1 − 1 < 2k+1

প্রমাণ: যোগফল S. দুই দিকে ২ দিয়ে গুণ করলে 2S = 2 + 4 + … + 2k+1. এবার 2S − S লিখুন — মাঝের সব পদ কেটে যায়, থাকে শুধু 2k+1 − 1. Binary-তে আরও সুন্দর দেখায়: 1 + 2 + 4 + 8 = 1111₂ = 15, আর তার ঠিক পরের সংখ্যা 10000₂ = 16.

মূল ধারণা

দ্বিগুণ হওয়া ধারায় শেষ পদটি আগের সব পদের যোগফলের চেয়েও বড়। তাই পুরো যোগফল শেষ পদের দ্বিগুণেরও কম — মোট খরচ = Θ(শেষ পদ)। সাধারণ r ≠ 1-এর জন্য: 1 + r + … + rk = (rk+1 − 1)/(r − 1). আর r < 1 হলে অসীম পর্যন্ত যোগফলও সীমিত: 1 + r + r² + … = 1/(1 − r). যেমন n + n/2 + n/4 + … < 2n.

এই শেষ তথ্যটি অনেক অ্যালগরিদমের রহস্য খুলে দেয়। যদি প্রতিটি ধাপে কাজ অর্ধেক হয়ে যায় (n, তারপর n/2, তারপর n/4…), মোট কাজ 2n-এর কম — অর্থাৎ Θ(n), log n গুণ নয়। Quickselect-এর গড় সময় (পর্ব ২২) আর heap তৈরির O(n) হিসাব (পর্ব ২৩) — দুটোই এই ধারার ওপর দাঁড়িয়ে।

Dynamic array কেন দ্বিগুণ হয়

C++-এর vector বা Python-এর list-এ push_back/append করতে থাকলে জায়গা একসময় ফুরিয়ে যায়। তখন dynamic array একটি বড় নতুন জায়গা নেয় আর পুরোনো সব উপাদান সেখানে কপি করে। কপি করা দামি — তাহলে কি প্রতিটি push ধীর? ক্ষমতা (capacity) প্রতিবার দ্বিগুণ করলে কী হয়, ধাপে ধাপে দেখুন:

array_ = ফাঁকা জায়গা

            
হিসাব
push করা মান
size / capacity
এই ধাপে কপি
মোট কপি
  1. push 1: capacity ১, জায়গা আছে। কোনো কপি নেই।
  2. push 2: জায়গা ভরা! capacity ২ করে নতুন জায়গা নেওয়া হলো, পুরোনো ১টি উপাদান কপি। মোট কপি ১.
  3. push 3: আবার ভরা। capacity ৪, কপি ২টি. মোট ৩. দুটি ফাঁকা ঘর পরের জন্য রাখা আছে।
  4. push 4: ফাঁকা ঘরে বসে গেল — কপি ০. মোট এখনো ৩.
  5. push 5: ভরা ছিল, capacity ৮, কপি ৪টি. মোট ৭.
  6. push 6, 7, 8: তিনটিই ফাঁকা ঘরে — কোনো কপি নেই। মোট এখনো ৭. দামি ধাপের পর অনেকগুলো সস্তা ধাপ আসে।
  7. push 9: capacity ১৬, কপি ৮টি. মোট ১৫.
  8. হিসাব: ৯টি push-এ মোট কপি ১ + ২ + ৪ + ৮ = ১৫ = 24 − 1 < ২ × ৯ = ১৮. গুণোত্তর ধারার কারণে n টি push-এ মোট কপি সবসময় < 2n — গড়ে প্রতি push-এ O(1).

সাধারণভাবে n টি push-এ শেষ resize-এর সময় কপি হয় 2k < n টি উপাদান, আর আগের সবগুলো মিলিয়ে 1 + 2 + … + 2k < 2k+1 < 2n. নতুন লেখা n টি ধরলে মোট কাজ < 3n. তাই একটি push কখনো কখনো Θ(n) সময় নিলেও গড়ে Θ(1) — একে বলে amortized O(1). বিস্তারিত আসবে পর্ব ১০ আর পর্ব ৬০-এ।

দ্বিগুণ না করে প্রতিবার ১০টি করে জায়গা বাড়ালে? তখন কপি হয় 10 + 20 + 30 + … — এটি সমান্তর ধারা! n টি push-এ প্রায় n/10 বার resize, মোট কপি প্রায় n²/20 = Θ(n²)। গুণোত্তর বনাম সমান্তর বৃদ্ধি — এক লাইনের পার্থক্য, কিন্তু ১০ লাখ উপাদানে কয়েক মিলিসেকেন্ড বনাম কয়েক মিনিট।

৭ · Harmonic ধারা আর integral দিয়ে যোগফলের সীমা

তৃতীয় গুরুত্বপূর্ণ ধারাটি harmonic series:

Hn = 1 + 1/2 + 1/3 + … + 1/n ≈ ln n + 0.5772 = Θ(log n)

এটি এক অদ্ভুত ধারা। পদগুলো ছোট হতে হতে শূন্যের দিকে যায়, তবু যোগফল কোনো সীমায় থামে না — যত খুশি বড় হয়। কিন্তু বাড়ে অসম্ভব ধীরে: Hn ১০ ছাড়াতে প্রায় ১২ হাজার পদ লাগে। কেন Θ(log n), তা দেখার একটি সুন্দর উপায় হলো পদগুলোকে দল বাঁধা — দলের আকার ১, ২, ৪, ৮…:

1 + (1/2 + 1/3) + (1/4 + … + 1/7) + (1/8 + … + 1/15) + …
প্রতিটি দল ≤ 1 (প্রতিটি পদ ≤ দলের প্রথম পদ)  ·  প্রতিটি দল > 1/2

n পর্যন্ত মোটামুটি log₂ n টি দল হয়। প্রতিটি দলের যোগফল ½ থেকে ১-এর মধ্যে, তাই Hn-ও (½)·log₂ n থেকে log₂ n + 1-এর মধ্যে আটকে থাকে — Θ(log n). দ্বিতীয় অসমতাটিই দেখায় যে ধারাটি অসীমে যায়: যত দল, তত বার অন্তত ½ যোগ হয়।

Integral দিয়ে যোগফল চেপে ধরা

আরও নিখুঁত সীমা পাওয়ার সাধারণ কৌশল হলো যোগফলকে ক্ষেত্রফল হিসেবে দেখা। f যদি কমতে থাকা ফাংশন হয় (যেমন 1/x), তাহলে প্রতিটি পদ f(i) হলো চওড়ায় ১ একটি আয়তক্ষেত্র, যা বক্ররেখার নিচের ক্ষেত্রফলের সাথে তুলনা করা যায়:

∫1n+1 dx/x ≤ Hn ≤ 1 + ∫1n dx/x  ⇒  ln(n+1) ≤ Hn ≤ 1 + ln n

বাড়তে থাকা f-এর জন্য দিক উল্টে যায়: ∫0n f(x)dx ≤ Σi=1n f(i) ≤ ∫1n+1 f(x)dx. যেমন Σ i² প্রায় ∫ x² dx = n³/3 — অংশ ৫-এর সূত্রের প্রধান পদ এটাই। Σ log i (অর্থাৎ log n!) প্রায় ∫ ln x dx = n ln n − n — Stirling-এর প্রধান অংশ। ক্যালকুলাস না জানলেও চলবে; শুধু মনে রাখুন: মসৃণ ফাংশনের যোগফল আর integral একই হারে বাড়ে।

Harmonic ধারা বাস্তবে কোথায় আসে? এমন লুপে, যেখানে ভেতরের লুপ i-এর ধাপে লাফায়: i = 1-এ n বার, i = 2-এ n/2 বার, i = 3-এ n/3 বার… মোট n · Hn = Θ(n log n). Quicksort-এর গড় তুলনা প্রায় 2n ln n (পর্ব ২২), আর “সব রকম কুপন জোগাড় করতে গড়ে কত কেনা লাগে” — সেটিও n · Hn.

সূত্রগুলো কম্পিউটারে যাচাই

সূত্র প্রমাণ করা হলো, এবার বড় n-এ লুপ চালিয়ে মিলিয়ে দেখি। নিচের প্রোগ্রাম চারটি n-এর জন্য Σ i আর Σ i² লুপে যোগ করে সূত্রের সাথে মেলায়, Hn আর ln n পাশাপাশি দেখায়, আর শেষে কয়েকটি k-এর জন্য 2-এর ঘাতের যোগফল যাচাই করে। আপনার ভাষার ট্যাব বেছে নিন — দুটির আউটপুট হুবহু এক:

#include <cmath>
#include <cstdio>
#include <initializer_list>
int main() {
    for (long long n : {10LL, 100LL, 1000LL, 100000LL}) {   // loop sums vs formulas
        long long s1 = 0, s2 = 0;  double h = 0.0;
        for (long long i = 1; i <= n; i++) {
            s1 += i;
            s2 += i * i;
            h += 1.0 / i;
        }
        bool ok = s1 == n * (n + 1) / 2 && s2 == n * (n + 1) * (2 * n + 1) / 6;
        std::printf("n=%lld sum=%lld squares=%lld %s\n", n, s1, s2, ok ? "OK" : "BAD");
        double ln = std::log(double(n));
        std::printf("   H(n)=%.6f ln(n)=%.6f diff=%.6f\n", h, ln, h - ln);
    }
    for (int k : {3, 10, 20, 40}) {           // 1 + 2 + 4 + ... + 2^k
        long long g = 0;  for (int i = 0; i <= k; i++) g += 1LL << i;
        std::printf("k=%d sum=%lld 2^(k+1)-1=%lld\n", k, g, (1LL << (k + 1)) - 1);
    }
}

আউটপুটের প্রথম অংশ: n=10 sum=55 squares=385 OK, n=100 sum=5050 squares=338350 OK, … n=100000 sum=5000050000 squares=333338333350000 OK — চারটি n-এই লুপ আর সূত্র হুবহু মিলেছে। H(n) আর ln(n)-এর পার্থক্য (diff) ক্রমে কমে 0.626383 → 0.582207 → 0.577716 → 0.577221 — এগিয়ে যাচ্ছে Euler-Mascheroni ধ্রুবক γ ≈ 0.5772-এর দিকে; পার্থক্য প্রায় 1/(2n). শেষ অংশে k=3 sum=15 2^(k+1)-1=15 থেকে k=40 sum=2199023255551 2^(k+1)-1=2199023255551 পর্যন্ত গুণোত্তর সূত্রও মিলেছে। খেয়াল করুন long long: n = 100000-এ n(n+1) প্রায় 1010, যা ৩২-bit int-এ ধরত না, আর বর্গের যোগফল প্রায় 3.3 × 1014.

import math

# 1) loop sums vs closed-form formulas
for n in (10, 100, 1000, 100000):
    s1 = s2 = 0
    h = 0.0
    for i in range(1, n + 1):
        s1 += i
        s2 += i * i
        h += 1.0 / i
    ok = s1 == n * (n + 1) // 2 and s2 == n * (n + 1) * (2 * n + 1) // 6
    print("n=%d sum=%d squares=%d %s" % (n, s1, s2, "OK" if ok else "BAD"))
    ln = math.log(n)
    print("   H(n)=%.6f ln(n)=%.6f diff=%.6f" % (h, ln, h - ln))

# 2) geometric: 1 + 2 + 4 + ... + 2^k == 2^(k+1) - 1
for k in (3, 10, 20, 40):
    g = sum(1 << i for i in range(k + 1))
    print("k=%d sum=%d 2^(k+1)-1=%d" % (k, g, (1 << (k + 1)) - 1))

আউটপুট C++-এর সাথে অক্ষরে অক্ষরে এক — একই ক্রমে একই double যোগ হয়েছে বলে H(n)-এর ছয় দশমিক পর্যন্ত মানও মেলে। Python-এর int-এর কোনো সীমা নেই, তাই overflow-এর ভয় নেই; কিন্তু খেয়াল করুন সূত্রে // (পূর্ণসংখ্যার ভাগ) লেখা হয়েছে — / লিখলে ফল float হয়ে যেত, আর বিশাল n-এ float-এর নির্ভুলতা হারিয়ে তুলনা ভুল হতে পারত। C++-এর 1LL << i আর Python-এর 1 << i দুটোই 2i.

৮ · গণনার মৌলিক কৌশল — বিন্যাস, সমাবেশ আর কবুতরের খোপ

অনেক অ্যালগরিদমের প্রথম প্রশ্ন: “সব সম্ভাব্য উত্তর পরীক্ষা করলে কতগুলো দেখতে হবে?” এর উত্তর দেয় গণনার কয়েকটি মৌলিক নিয়ম।

  • গুণের নিয়ম: একটি কাজ a ভাবে, তারপর আরেকটি b ভাবে করা গেলে দুটো মিলিয়ে a × b ভাবে। bKash PIN-এর ৫টি অঙ্ক, প্রতিটি ১০ রকম — মোট 105 = এক লাখ রকম PIN.
  • Subset: n টি জিনিসের প্রতিটিকে “নেব / নেব না” — মোট 2n টি subset. Backtracking (পর্ব ৫৬) আর bitmask DP (পর্ব ৫৫)-এর আকার এটাই।
  • Permutation (বিন্যাস — ক্রম গুরুত্বপূর্ণ): n টি আলাদা জিনিস সারিতে সাজানোর উপায় n!. n টি থেকে k টি নিয়ে সাজানোর উপায় n!/(n−k)!. পরীক্ষার খাতা কোন ক্রমে সাজানো হবে — সেটি বিন্যাস।
  • Combination (সমাবেশ — ক্রম গুরুত্বহীন): n টি থেকে k টি বাছার উপায় C(n, k) = n! / (k!(n−k)!). ক্রিকেট দলের ১৫ জন থেকে ১১ জন বাছা — কে কোথায় দাঁড়াল তা নয়, কে দলে আছে তাই গুরুত্বপূর্ণ।

DSA-তে সবচেয়ে বেশি আসে C(n, 2) = n(n−1)/2 — n টি জিনিস থেকে জোড়ার সংখ্যা। একটি অনুষ্ঠানে n জন সবাই সবার সাথে একবার করে হাত মেলালে মোট হাত মেলানো C(n, 2). for i আর তার ভেতরে for j = i+1 লুপ ঠিক এই সব জোড়াই দেখে — তাই সেটি Θ(n²)। লক্ষ করুন, এটিও অংশ ৫-এর সমান্তর ধারা: (n−1) + (n−2) + … + 0.

যা গুনছেনসূত্রn = 10n = 20
সব জোড়াC(n, 2)৪৫১৯০
সব subset2n১,০২৪১০,৪৮,৫৭৬
সব বিন্যাসn!৩৬,২৮,৮০০≈ 2.4 × 1018

শেষ সারিটি দেখে বুঝবেন, কেন traveling salesman-এর মতো সমস্যায় সব ক্রম পরীক্ষা করা n = ২০-এই অসম্ভব (পর্ব ৫৭ ও ৬৪)। সারণির একটি মনে রাখার নিয়ম: n ≤ ১০–১১ হলে n! চলে, n ≤ ২০–২৫ হলে 2n চলে — কনটেস্টে ইনপুটের সীমা দেখেই অনেক সময় বোঝা যায় কোন পদ্ধতি চাওয়া হচ্ছে।

Pigeonhole — কবুতরের খোপের নীতি

এটি গণিতের সবচেয়ে সরল, অথচ আশ্চর্য রকম শক্তিশালী নীতিগুলোর একটি। Pigeonhole principle: n টি খোপে n + 1 টি কবুতর রাখলে অন্তত একটি খোপে দুটি কবুতর থাকবেই। আরও সাধারণভাবে, m টি জিনিস n টি বাক্সে রাখলে কোনো একটি বাক্সে অন্তত ⌈m/n⌉ টি থাকবে। এর প্রয়োগ:

  • ৩৬৭ জন মানুষের মধ্যে অন্তত দুজনের জন্মদিন একই (বছরে সর্বোচ্চ ৩৬৬টি তারিখ)।
  • Hash table-এ (পর্ব ৩২) যতগুলো ঘর আছে তার চেয়ে বেশি key ঢোকালে collision অনিবার্য — hash function যত ভালোই হোক।
  • কোনো compression প্রোগ্রাম সব ফাইল ছোট করতে পারে না: n-bit লম্বা ফাইল 2n টি, কিন্তু তার চেয়ে ছোট সব bit-string মিলিয়ে মাত্র 2n − 1 টি।
  • কনটেস্টের একটি ক্লাসিক: n টি পূর্ণসংখ্যার যেকোনো array-তে এমন একটি পাশাপাশি অংশ আছে যার যোগফল n দিয়ে বিভাজ্য। কারণ n টি prefix sum-এর (পর্ব ১৭) n mod-মান, সাথে শূন্য-দৈর্ঘ্যের prefix-এর ০ — মোট n + 1 টি মান, অথচ ভাগশেষ মাত্র n রকম।

৯ · লুপ থেকে যোগফল — বিশ্লেষণের রেসিপি

এবার সব যন্ত্রপাতি একসাথে কাজে লাগাই। যেকোনো লুপের time complexity বের করার রেসিপি:

  1. সবচেয়ে ভেতরের কাজটি (একটি তুলনা, একটি যোগ) এক একক ধরুন।
  2. ভেতরের লুপ কতবার চলে, তা বাইরের লুপের চলক (i)-এর ফাংশন হিসেবে লিখুন।
  3. বাইরের লুপের সব মানের ওপর Σ লিখুন।
  4. পরিচিত সূত্র দিয়ে closed form বের করুন — বা অন্তত দুই দিক থেকে সীমা দিন।
  5. প্রধান পদটি রেখে Θ লিখুন।

একটি উদাহরণ ধাপে ধাপে দেখুন — ভেতরের লুপ j = i থেকে শুরু হয়, তাই প্রতিবার একটু কম চলে:

1int count = 0;2for (int i = 1; i <= 4; i++)3    for (int j = i; j <= 4; j++)4        count++;5printf("count = %d\n", count);
ভ্যারিয়েবলের মান
i
inner runs
count
  1. শুরু: count = ০. প্রশ্ন: count++ মোট কতবার চলবে?
  2. i = ১: j চলে ১ থেকে ৪ — ভেতরের লুপ ৪ বার. count = ৪.
  3. i = ২: j চলে ২ থেকে ৪ — ৩ বার. count = ৭.
  4. i = ৩: j চলে ৩ থেকে ৪ — ২ বার. count = ৯.
  5. i = ৪: j শুধু ৪ — ১ বার. count = ১০.
  6. যোগফলে রূপান্তর: সাধারণ n-এ i-তম ধাপে ভেতরের লুপ চলে n − i + 1 বার। মোট Σi=1n (n − i + 1) = n + (n−1) + … + 1 = n(n+1)/2. n = ৪-এ ৪ × ৫ / ২ = ১০ — আউটপুটের সাথে মিলেছে। তাই এই লুপ Θ(n²)।

কয়েকটি বহুল প্রচলিত লুপ-প্যাটার্ন আর তাদের যোগফল একসাথে রাখুন — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এগুলোই ঘুরেফিরে আসে:

লুপযোগফলফল
for i in 1..n: for j in 1..nΣ n = n · nΘ(n²)
for i in 1..n: for j in i..nΣ (n − i + 1) = n(n+1)/2Θ(n²)
for (i = 1; i < n; i *= 2)⌈log₂ n⌉ বারΘ(log n)
for i in 1..n: for (j = 1; j < n; j *= 2)n · ⌈log₂ n⌉Θ(n log n)
for i in 1..n: for (j = i; j <= n; j += i)Σ ⌊n/i⌋ ≈ n · HnΘ(n log n)
for (i = n; i > 0; i /= 2): for j in 1..in + n/2 + n/4 + … < 2nΘ(n)
for i in 1..n: for j in 1..i: for k in 1..jΣ i(i+1)/2 = n(n+1)(n+2)/6Θ(n³)

শেষের আগের সারিটি অনেককে চমকে দেয়: দুটি nested লুপ, অথচ সময় Θ(n)! কারণ বাইরের লুপ log n বার চললেও ভেতরের কাজ গুণোত্তর ধারায় কমছে। আর পঞ্চম সারিটি দেখতে প্রায় Θ(n²) মনে হলেও আসলে harmonic. নিচের প্রোগ্রাম তিনটি প্যাটার্ন সত্যিই চালিয়ে ধাপ গোনে, তারপর সূত্রের সাথে মেলায়:

#include <cmath>
#include <cstdio>
#include <initializer_list>
int main() {
    for (long long n : {16LL, 1000LL, 5000LL}) {
        long long halv = 0, pairs = 0, jumps = 0;
        for (long long i = n; i > 1; i /= 2) halv++;          // keep halving
        for (long long i = 0; i < n; i++)                     // every pair i < j
            for (long long j = i + 1; j < n; j++) pairs++;
        for (long long i = 1; i <= n; i++)                    // j jumps by i
            for (long long j = i; j <= n; j += i) jumps++;
        std::printf("n=%lld halvings=%lld floor(log2 n)=%d\n",
                    n, halv, int(std::floor(std::log2(double(n)))));
        std::printf("   pairs=%lld n(n-1)/2=%lld\n", pairs, n * (n - 1) / 2);
        std::printf("   jumps=%lld n*ln(n)=%.0f\n", jumps, double(n) * std::log(double(n)));
    }
}

আউটপুট: n = 16-এ halvings=4 floor(log2 n)=4, pairs=120 n(n-1)/2=120, jumps=50 n*ln(n)=44; n = 1000-এ halvings=9, pairs=499500, jumps=7069 n*ln(n)=6908; n = 5000-এ halvings=12, pairs=12497500, jumps=43376 n*ln(n)=42586. অর্ধেক করার লুপ আর জোড়ার লুপ সূত্রের সাথে হুবহু মিলেছে। লাফানো লুপের (j += i) গণনা n ln n-এর খুব কাছে — পার্থক্য মাত্র প্রায় 0.15n. এটি n · Hn ≈ n ln n + 0.58n-এর চেয়েও একটু কম, কারণ ⌊n/i⌋ নিচের দিকে গোল হয়; সূক্ষ্ম হিসাবে Σ ⌊n/i⌋ = n ln n + (2γ − 1)n + O(√n), আর 2γ − 1 ≈ 0.15. লক্ষ করুন n ৫ গুণ বাড়তে (১০০০ → ৫০০০) জোড়া বেড়েছে প্রায় ২৫ গুণ (Θ(n²)), কিন্তু লাফানো লুপ বেড়েছে মাত্র ৬ গুণের একটু বেশি (Θ(n log n)).

আরও গভীরে: Stirling-এর পূর্ণ রূপ ln n! = n ln n − n + ½ ln(2πn) + O(1/n), তাই তুলনা-সর্টের নিম্নসীমা আসলে n log₂ n − 1.44n + O(log n) — merge sort এর খুব কাছাকাছি পৌঁছে যায়। log-এর চেয়েও ধীরে বাড়ে iterated logarithm log* n (কতবার log নিলে ≤ 1 হয়; ২৬৫৫৩৬ পর্যন্ত সংখ্যায় এটি ৫-এর বেশি নয়) আর union-find-এর বিশ্লেষণে আসা inverse Ackermann ফাংশন α(n) (পর্ব ৩৩)। আর যোগফল-integral তুলনার সূক্ষ্ম রূপ হলো Euler–Maclaurin সূত্র, যা থেকেই Hn = ln n + γ + 1/(2n) − … পাওয়া যায়।

পরীক্ষা ও ইন্টারভিউতে:

  • প্রশ্ন: for (i = 1; i < n; i *= 2) কতবার চলে? — উত্তর: ⌈log₂ n⌉ বার, Θ(log n).
  • প্রশ্ন: log n! কোন Θ-শ্রেণির, প্রমাণসহ? — উত্তর: Θ(n log n); n! ≤ nn আর n! ≥ (n/2)n/2 থেকে।
  • প্রশ্ন: vector-এর push_back গড়ে O(1) কেন? — উত্তর: capacity দ্বিগুণ হয়, তাই মোট কপি 1 + 2 + 4 + … < 2n.
  • প্রশ্ন: O(log₂ n) আর O(log₁₀ n)-এর পার্থক্য কী? — উত্তর: Big-O-তে কোনো পার্থক্য নেই; change of base-এ শুধু ধ্রুবক গুণক ≈ 3.32.
  • প্রশ্ন: 1 + 1/2 + … + 1/n কত বড়? — উত্তর: ≈ ln n + 0.5772, অর্থাৎ Θ(log n); ধারাটি অসীমে যায়, কিন্তু খুব ধীরে।

১০ · AI-এর যুগে — AI-এর গণিত যাচাই করবেন কীভাবে

“এই কোডের complexity কত?” — প্রশ্নটি AI-কে করলে কয়েক সেকেন্ডে আত্মবিশ্বাসী উত্তর আসে। সমস্যা হলো, এই আত্মবিশ্বাস সবসময় হিসাবের ওপর দাঁড়িয়ে থাকে না। AI প্রায়ই লুপের আকার দেখে অনুমান করে (“দুটি nested লুপ, তাই O(n²)”) — অথচ আপনি অংশ ৯-এ দেখেছেন, দুটি nested লুপ Θ(n), Θ(n log n) বা Θ(n²) — যেকোনোটি হতে পারে। এই পর্বের গণিতই আপনার যাচাইয়ের অস্ত্র।

এই বিষয়ে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকীভাবে ধরবেন
harmonic লুপ (j += i)-কে O(n²) বলাঅংশ ৯-এর মতো Σ n/i লিখুন; ছোট n-এ গোনার প্রোগ্রাম চালান
⌊log₂ n⌋ আর ⌈log₂(n+1)⌉ গুলিয়ে ফেলা — এক বেশি বা এক কমn = 1, 7, 8-এর মতো ছোট মানে হাতে গুনুন
n * (n + 1) / 2 int-এ লেখা — বড় n-এ overflown-এর সর্বোচ্চ মান বসিয়ে দেখুন ফল 231 − 1 ছাড়ায় কি না
modular হিসাবে সরাসরি / দিয়ে ভাগ, বা ঋণাত্মক % ভুলে যাওয়াছোট সংখ্যায় mod ছাড়া হিসাবের সাথে মেলান
“geometric বলে O(log n)” — যোগফল আর পদসংখ্যা গুলিয়ে ফেলাপ্রশ্ন করুন: ধাপ কতগুলো, আর প্রতিটি ধাপের কাজ কত — দুটো আলাদা

AI-কে শুধু “complexity কত?” না জিজ্ঞেস করে তাকে হিসাব দেখাতে বাধ্য করুন। তাহলে কোন ধাপে ভুল, আপনি নিজেই ধরতে পারবেন:

নিচের কোডের time complexity বের করো, কিন্তু শুধু উত্তর দিও না: [এখানে কোড পেস্ট করুন] ১. প্রতিটি লুপ কতবার চলে, বাইরের লুপের চলকের ফাংশন হিসেবে লেখো। ২. মোট কাজকে একটি summation (Σ) হিসেবে লেখো। ৩. কোন পরিচিত ধারা (সমান্তর, গুণোত্তর, harmonic) ব্যবহার করলে, নাম বলে closed form দাও। ৪. n = 1, 2, 8 আর 10-এর জন্য তোমার সূত্রে ঠিক কতবার চলবে, সংখ্যায় বলো — আমি হাতে গুনে মেলাব। ৫. কোনো ধাপে নিশ্চিত না হলে স্পষ্ট করে বলো।

AI-এর দেওয়া হিসাব পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Logarithm কেন আবিষ্কার হয়েছিল, আর ৪০০ বছর পর কম্পিউটার বিজ্ঞানে log₂ সব জায়গায় কেন?

Logarithm জন্মেছিল একটি খুব ব্যবহারিক কষ্ট থেকে। ষোড়শ-সপ্তদশ শতকে জ্যোতির্বিদ আর নাবিকদের বিশাল বিশাল সংখ্যা হাতে গুণ-ভাগ করতে হতো — গ্রহের অবস্থান, সমুদ্রে জাহাজের পথ। একটি ভুল মানে দিনের পর দিনের পরিশ্রম নষ্ট। স্কটল্যান্ডের John Napier ১৬১৪ সালে logarithm-এর ধারণা ও সারণি প্রকাশ করেন। কৌশলটি ঠিক আমাদের প্রথম নিয়মটি: log(ab) = log a + log b. দুটি বিশাল সংখ্যা গুণ করার বদলে সারণি থেকে তাদের log দেখে যোগ করুন, তারপর উল্টো সারণি থেকে ফল পড়ুন। গুণ কঠিন, যোগ সহজ — logarithm কঠিন কাজকে সহজ কাজে বদলে দিল। পরে Henry Briggs ভিত্তি ১০-এর সারণি তৈরি করেন, আর এই নীতির ওপর ভর করে slide rule প্রায় সাড়ে তিনশো বছর প্রকৌশলীদের হাতের যন্ত্র ছিল — ইলেকট্রনিক ক্যালকুলেটর আসার আগ পর্যন্ত।

কম্পিউটার বিজ্ঞানে log ফিরে এলো ভিন্ন কারণে। এখানে প্রশ্নটা “গুণকে যোগে বদলানো” নয়, বরং “বারবার ভাগ করা”। কম্পিউটার binary-তে চলে, আর অনেক অ্যালগরিদম সমস্যাকে দুই ভাগে ভাঙে। তাই log₂ সবখানে: একটি সংখ্যা লিখতে কতগুলো bit লাগে (⌊log₂ n⌋ + 1), binary search কতবার তুলনা করে, balanced binary tree কত উঁচু (পর্ব ২৮-২৯), merge sort-এর recursion কত স্তর (পর্ব ২১), heap-এ একটি উপাদান কত ধাপ ওপরে-নিচে যায় (পর্ব ২৩)। Information theory-তেও log₂: n টি সমান সম্ভাব্য উত্তরের মধ্যে একটি চিহ্নিত করতে অন্তত log₂ n bit তথ্য লাগে — এজন্যই “২০টি হ্যাঁ/না প্রশ্নে” ১০ লাখের মধ্যে একটি জিনিস খুঁজে বের করা যায়।

আরেকটি গভীর দিক: log আসলে “আকার” মাপার স্বাভাবিক উপায়। ১০ লাখ আর ২০ লাখের পার্থক্য বিশাল মনে হলেও, log₂-এ তা মাত্র ১. এজন্য O(log n) অ্যালগরিদম ডেটা বাড়লেও প্রায় একই গতিতে চলে — ব্যাংকের লেনদেনের ডেটাবেস কয়েক কোটি সারিতেও মিলিসেকেন্ডে একটি হিসাব খুঁজে দেয়।

মূল উপলব্ধি: logarithm-এর দুই রূপ — “গুণকে যোগে বদলানো” আর “কতবার ভাগ করা যায়” — আসলে একই ধারণা। একটি জ্যোতির্বিদদের সময় বাঁচিয়েছিল, অন্যটি আজ প্রতিটি সার্চ আর ডেটাবেসের গতির মূলে।

প্র ০২Gauss-এর গল্পটি কতটা সত্য? আর “জোড়া বানানো” কৌশল অ্যালগরিদম বিশ্লেষণে আর কোথায় কাজে লাগে?

Carl Friedrich Gauss (১৭৭৭–১৮৫৫) ইতিহাসের শ্রেষ্ঠ গণিতবিদদের একজন। ১ থেকে ১০০ যোগের গল্পটি সবচেয়ে পরিচিত রূপে এসেছে তাঁর মৃত্যুর পর লেখা একটি স্মৃতিকথা থেকে, যার লেখক Gauss-এর পরিচিত Wolfgang Sartorius von Waltershausen. সেই বর্ণনায় নির্দিষ্ট সংখ্যা ছিল না; “১ থেকে ১০০” রূপটি পরে জনপ্রিয় হয়েছে। ইতিহাসবিদেরা বিভিন্ন সংস্করণ খুঁজে দেখেছেন — কোথাও সংখ্যাগুলো আরও বড় সমান্তর ধারা, কোথাও শিক্ষকের নাম বা Gauss-এর বয়স ভিন্ন। তাই সৎভাবে বলা যায়: ঘটনাটি সম্ভবত সত্যের ওপর দাঁড়িয়ে, কিন্তু খুঁটিনাটি অলংকৃত। আর সূত্রটি Gauss-এর আবিষ্কারও নয় — সমান্তর ধারার যোগফল প্রাচীন যুগ থেকেই গণিতবিদদের জানা ছিল। গল্পটির আসল মূল্য তার শিক্ষায়: সমস্যাকে অন্যভাবে সাজালে কঠিন কাজ এক লাইনের হিসাব হয়ে যায়।

“সামনে-পেছনে জোড়া বাঁধা” কৌশলটি অ্যালগরিদমে বারবার আসে। একটি অনুষ্ঠানে সবার সাথে সবার হাত মেলানোর সংখ্যা, bubble বা selection sort-এর তুলনার সংখ্যা, সব জোড়ার দূরত্ব হিসাব — সবই n(n−1)/2. দুই প্রান্ত থেকে কাছে আসার একই চিন্তা আছে two pointer কৌশলে (পর্ব ১৬): সাজানো array-তে একটি পয়েন্টার শুরুতে, একটি শেষে — যেমন Gauss সবচেয়ে ছোট আর সবচেয়ে বড় সংখ্যা জোড়া বেঁধেছিলেন।

আরও গভীরে, Gauss-এর কৌশল একটি সাধারণ নীতির উদাহরণ — “একই জিনিস দুইভাবে গোনা” (double counting). একই যোগফলকে সামনে থেকে আর পেছন থেকে লিখে যোগ করা, বা একটি গ্রাফের সব degree যোগ করলে প্রতিটি edge দুবার গোনা হয় (তাই যোগফল = 2E, পর্ব ৩৯) — এগুলো এক জাতের যুক্তি। Amortized analysis-এর aggregate পদ্ধতিও (পর্ব ৬০) মূলত মোট কাজকে অন্য দিক থেকে গোনা।

মূল উপলব্ধি: গল্পের খুঁটিনাটি নিয়ে সতর্ক থাকুন, কিন্তু কৌশলটি মনে রাখুন। একটি যোগফল কঠিন মনে হলে জিজ্ঞেস করুন: পদগুলোকে অন্য ক্রমে সাজালে, বা একই জিনিস অন্য দিক থেকে গুনলে কি সহজ হয়?

প্র ০৩Harmonic ধারার পদগুলো শূন্যের দিকে যায়, তবু যোগফল অসীম — এটা কীভাবে সম্ভব, আর অ্যালগরিদমে এর প্রভাব কী?

অনেকের সহজাত ধারণা: “পদগুলো যদি শূন্যের দিকে যায়, যোগফল একসময় থেমে যাবে।” Harmonic ধারা এই ধারণাকে ভুল প্রমাণ করে। এর প্রথম পরিচিত প্রমাণ দেন চতুর্দশ শতকের ফরাসি পণ্ডিত Nicole Oresme, আর যুক্তিটি ঠিক অংশ ৭-এর দল-বাঁধার কৌশল: 1/3 + 1/4 > 1/4 + 1/4 = 1/2; 1/5 থেকে 1/8 পর্যন্ত চারটি পদ প্রতিটি ≥ 1/8, তাই যোগফল ≥ 1/2; এভাবে প্রতিটি দ্বিগুণ-আকারের দল অন্তত ½ যোগ করে। দল অসীম সংখ্যক, তাই যোগফলও অসীম। পরে Euler দেখান যে Hn − ln n একটি নির্দিষ্ট ধ্রুবকের দিকে যায়, যার নাম এখন Euler-Mascheroni ধ্রুবক γ ≈ 0.5772 — আমাদের প্রোগ্রামের diff কলামে আপনি ঠিক এটাই দেখেছেন।

তবে “অসীম” মানে দ্রুত নয়। Hn ১০ ছাড়ায় n = ১২,৩৬৭-এ, আর ২০ ছাড়াতে লাগে কয়েক কোটি পদ। এই অতি ধীর বৃদ্ধিই অ্যালগরিদমে আশীর্বাদ:

  • গুণিতকের লুপ: প্রতিটি i-এর জন্য তার সব গুণিতক দেখা (যেমন সব সংখ্যার ভাজক তালিকা বানানো) মোট n · Hn = Θ(n log n) — দেখতে nested হলেও প্রায় রৈখিক।
  • Quicksort-এর গড়: দুটি উপাদান i আর j তুলনা হওয়ার সম্ভাবনা 2/(j − i + 1); সব জোড়ায় যোগ করলে harmonic ধারা আসে, আর গড় তুলনা প্রায় 2n ln n ≈ 1.39 n log₂ n (পর্ব ২২)।
  • Coupon collector: n রকম স্টিকার সব জোগাড় করতে গড়ে n · Hn বার কিনতে হয় — randomized অ্যালগরিদমের বিশ্লেষণে এই হিসাব প্রায়ই আসে।
  • এলোমেলো array-তে “নতুন সর্বোচ্চ” কতবার: i-তম উপাদান তখন পর্যন্ত সবচেয়ে বড় হওয়ার সম্ভাবনা 1/i, তাই গড়ে Hn বার — max খোঁজার লুপে variable আপডেট মাত্র Θ(log n) বার।

মূল উপলব্ধি: “অসীম” আর “বড়” এক জিনিস নয়, আর “পদ ছোট হচ্ছে” মানেই “যোগফল সীমিত” নয়। কোন ধারা কত দ্রুত বাড়ে — সীমিত (গুণোত্তর, r < 1), লগারিদমিক (harmonic), নাকি বহুপদী (সমান্তর) — তা চিনতে পারাই বিশ্লেষণের মূল দক্ষতা।

প্র ০৪Dynamic array ঠিক দ্বিগুণই কেন হবে? ১.৫ গুণ বা প্রতিবার নির্দিষ্ট পরিমাণ বাড়ালে কী হয়?

অংশ ৬-এ দেখেছেন, যোগ করে বাড়ালে (প্রতিবার +k) মোট কপি সমান্তর ধারা — Θ(n²/k). k যত বড়ই নিন, n যথেষ্ট বড় হলে এটি দ্বিঘাত। তাই গুণ করে বাড়ানোই একমাত্র উপায়, যাতে push-এর গড় খরচ O(1) থাকে। কিন্তু গুণকটি ঠিক ২ হতে হবে, এমন কোনো কথা নেই। যেকোনো গুণক r > 1 হলে মোট কপি একটি গুণোত্তর ধারা, যার যোগফল প্রায় n · 1/(r − 1). r = 2 হলে প্রায় n, r = 1.5 হলে প্রায় 2n, r = 1.125 হলে প্রায় 8n — সবই Θ(n), শুধু ধ্রুবক আলাদা।

তাহলে ট্রেড-অফটি কী? বড় গুণক মানে কম কপি, কিন্তু বেশি অব্যবহৃত মেমরি — দ্বিগুণের পরপরই প্রায় অর্ধেক জায়গা খালি পড়ে থাকে। ছোট গুণক মানে মেমরি কম নষ্ট, কিন্তু বেশি বার কপি। বাস্তবের লাইব্রেরিগুলো তাই আলাদা পথ নিয়েছে: GCC-র libstdc++ আর Clang-এর libc++-এ std::vector সাধারণত দ্বিগুণ হয়, Microsoft-এর লাইব্রেরি নেয় ১.৫ গুণ, আর CPython-এর list বাড়ায় প্রায় ১২.৫% আর একটি ছোট ধ্রুবক যোগ করে। (এগুলো বাস্তবায়নের খুঁটিনাটি, ভাষার মানদণ্ডে লেখা নিয়ম নয় — সময়ের সাথে বদলাতে পারে।)

১.৫-এর পক্ষে একটি সূক্ষ্ম যুক্তিও প্রচলিত: দ্বিগুণ করলে নতুন জায়গা সবসময় আগের সব ছেড়ে দেওয়া জায়গার যোগফলের চেয়ে বড় (কারণ 1 + 2 + … + 2k < 2k+1 — আমাদের গুণোত্তর সূত্র!), তাই memory allocator পুরোনো ছেড়ে দেওয়া টুকরোগুলো জোড়া দিয়ে কখনো নতুন array-র জন্য ব্যবহার করতে পারে না। গুণক golden ratio (≈ 1.618)-এর চেয়ে ছোট হলে কয়েক ধাপ পর ছেড়ে দেওয়া জায়গাগুলো মিলে নতুন চাহিদা মেটাতে পারে। বাস্তবে allocator কীভাবে কাজ করে তার ওপর এর লাভ নির্ভর করে।

মূল উপলব্ধি: গণিত বলে দেয় কোনটি অসম্ভব (যোগ করে বাড়িয়ে O(1) গড় পাওয়া), আর কোনগুলো সমান ভালো (যেকোনো গুণক r > 1). তাদের মধ্যে থেকে বেছে নেওয়া প্রকৌশলের কাজ — মেমরি বনাম সময়ের ট্রেড-অফ।

প্র ০৫গণনা আর pigeonhole দিয়ে কীভাবে প্রমাণ করা যায় যে কোনো কাজ “এর চেয়ে দ্রুত সম্ভবই নয়”?

অ্যালগরিদম বিশ্লেষণের সবচেয়ে সুন্দর ফলগুলোর কয়েকটি হলো নিম্নসীমা — কোনো অ্যালগরিদম, যত চতুরই হোক, অমুক সংখ্যক ধাপের কমে কাজটি করতে পারবে না। এর অনেকগুলোর মূলে আছে সরল গণনা।

তুলনা-সর্টের নিম্নসীমা: n টি আলাদা সংখ্যার n! রকম ক্রম সম্ভব, আর সর্টকে বুঝতে হবে আসলে কোনটি। প্রতিটি তুলনার উত্তর শুধু “হ্যাঁ” বা “না” — তাই k টি তুলনায় সর্বোচ্চ 2k টি আলাদা পরিস্থিতি আলাদা করা যায়। Pigeonhole অনুযায়ী 2k ≥ n! না হলে দুটি আলাদা ক্রম একই উত্তর-সারি পাবে, আর সর্ট অন্তত একটিকে ভুল সাজাবে। তাই k ≥ log₂ n! = Θ(n log n) — এই পর্বের সূত্র! n = ৩ হলে 3! = ৬, log₂ ৬ ≈ ২.৫৮, তাই worst case-এ অন্তত ৩টি তুলনা লাগবেই। পূর্ণ প্রমাণ decision tree দিয়ে পর্ব ২৪-এ।

Hashing আর collision: সম্ভাব্য key অসংখ্য, কিন্তু hash মান সীমিত — pigeonhole বলে collision অনিবার্য। আরও চমকপ্রদ হলো জন্মদিনের প্যারাডক্স: মাত্র ২৩ জন মানুষ থাকলেই দুজনের জন্মদিন মিলে যাওয়ার সম্ভাবনা ৫০%-এর বেশি (কারণ জোড়ার সংখ্যা C(23, 2) = ২৫৩, অনেক বড়)। সাধারণভাবে m টি সম্ভাব্য মানে মোটামুটি √m টি key-তেই collision আশা করা যায় — এজন্য cryptographic hash-এ ১২৮ বা ২৫৬ bit লাগে।

Compression-এর সীমা: অংশ ৮-এ দেখেছেন, কোনো lossless compression সব ফাইল ছোট করতে পারে না। যে প্রোগ্রাম কিছু ফাইল ছোট করে, সে অন্য কিছু ফাইল বড় করতে বাধ্য। ZIP যে টেক্সট ফাইলকে অনেক ছোট করে কিন্তু ইতিমধ্যে compressed ভিডিও প্রায় ছোট করতে পারে না — তার মূলে এই গণনা।

লক্ষ করুন, এই যুক্তিগুলো কোনো নির্দিষ্ট অ্যালগরিদমের কথা বলে না — সব সম্ভাব্য অ্যালগরিদমের কথা বলে। এই শক্তিই নিম্নসীমাকে এত মূল্যবান করে: এটি বলে দেয় কখন আরও ভালো খোঁজা বন্ধ করা উচিত, আর কখন নিয়মটাই বদলাতে হবে (যেমন counting sort তুলনা না করে সংখ্যার মান ব্যবহার করে n log n-এর সীমা এড়ায়)।

মূল উপলব্ধি: “কতগুলো সম্ভাব্য উত্তর” আর “প্রতিটি ধাপ কতটুকু তথ্য দেয়” — এই দুটি সংখ্যা গুনলেই প্রায়ই একটি অটল নিম্নসীমা পাওয়া যায়।

অনুশীলন

কাগজ-কলম নিন। প্রথমে নিজে চেষ্টা করুন, তারপর উত্তর মেলান।

  1. হাতে trace করুন: for (int i = n; i > 0; i /= 2) count++; — n = ৩৭ হলে লুপ কতবার চলে? i-এর প্রতিটি মান লিখুন, তারপর উত্তরটিকে log দিয়ে প্রকাশ করুন।

    i-এর মান: ৩৭ → ১৮ → ৯ → ৪ → ২ → ১ → (০, লুপ শেষ)। লুপের ভেতরে ঢোকে ৬ বার: i = ৩৭, ১৮, ৯, ৪, ২, ১. সূত্রে: ⌊log₂ ৩৭⌋ + 1 = ৫ + ১ = ৬ (কারণ 25 = ৩২ ≤ ৩৭ < ৬৪)। অংশ ২-এর লুপের সাথে পার্থক্যটি খেয়াল করুন — সেখানে শর্ত ছিল n > 1, তাই i = ১-এ ঢুকত না এবং উত্তর হতো ⌊log₂ n⌋ = ৫. শর্তের এই ছোট্ট পার্থক্যই “এক বেশি, এক কম” ভুলের উৎস।

  2. তিন স্তরের লুপ: for i in 1..n: for j in 1..i: for k in 1..j: count++ — count-এর closed form বের করুন, আর n = ৩-এ হাতে মিলিয়ে দেখুন।

    সবচেয়ে ভেতরের লুপ j বার চলে। তাই নির্দিষ্ট i-এর জন্য মোট 1 + 2 + … + i = i(i+1)/2. এবার i-এর ওপর যোগ: Σi=1n i(i+1)/2 = ½(Σ i² + Σ i) = ½[n(n+1)(2n+1)/6 + n(n+1)/2] = n(n+1)(n+2)/6. এটি আসলে C(n+2, 3) — n + 2 টি থেকে ৩টি বাছার উপায়। n = ৩-এ: সূত্রে ৩ × ৪ × ৫ / ৬ = ১০; হাতে: i = ১ → ১, i = ২ → ১ + ২ = ৩, i = ৩ → ১ + ২ + ৩ = ৬; মোট ১০. ফল Θ(n³), প্রধান পদ n³/6.

  3. কোড লিখুন: একটি ফাংশন লিখুন যা 1 + 2 + … + n-এর মান mod 1,000,000,007 ফেরত দেয়, যেখানে n সর্বোচ্চ 1018. লুপ চলবে না (কেন?), আর overflow যেন না হয়।

    লুপে 1018 ধাপ লাগবে — এক সেকেন্ডে 108 হলে কয়েকশো বছর! তাই n(n+1)/2 সূত্র। কিন্তু n(n+1) প্রায় 1036, যা long long-এ ধরে না। সমাধান: আগে ২ দিয়ে ভাগ করুন (n আর n+1-এর একটি অবশ্যই জোড়), তারপর দুটিকে আলাদা করে mod নিয়ে গুণ করুন — প্রতিটি mod-এর পর মান 109-এর কম, তাই গুণফল < 1.1 × 1018, নিরাপদ।

    long long sum_mod(long long n) {
        const long long M = 1000000007;
        long long a = n, b = n + 1;
        if (a % 2 == 0) a /= 2; else b /= 2;   // divide BEFORE taking mod
        return (a % M) * (b % M) % M;
    }

    ভুল সমাধান হলো (n % M) * ((n + 1) % M) % M / 2 — mod নেওয়ার পর ২ দিয়ে ভাগ করা যায় না (ভাগের জন্য modular inverse লাগে, পর্ব ৬২)।

  4. নিম্নসীমা আন্দাজ করুন: ১০টি আলাদা সংখ্যা শুধু তুলনা করে সাজাতে হবে। worst case-এ কমপক্ষে কতগুলো তুলনা যেকোনো অ্যালগরিদমকে করতেই হবে? (10! = 3,628,800)

    প্রয়োজন 2k ≥ 10! = ৩৬,২৮,৮০০. 221 = ২০,৯৭,১৫২ (কম), 222 = ৪১,৯৪,৩০৪ (যথেষ্ট)। তাই k ≥ ⌈log₂ 10!⌉ = ⌈২১.৭৯⌉ = ২২টি তুলনা। তুলনার জন্য Stirling: 10 log₂ 10 − 1.44 × 10 ≈ ৩৩.২ − ১৪.৪ = ১৮.৮ — প্রধান পদগুলো ঠিক দিক দেখায়, তবে ছোট n-এ বাদ দেওয়া পদের (½ log₂(2πn) ≈ ৩.০) প্রভাব আছে, যোগ করলে প্রায় ২১.৮ — আসল মানের প্রায় হুবহু। Merge sort ১০টিতে worst case-এ ২৫টি তুলনা করে — নিম্নসীমার বেশ কাছে।

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

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

ক্রমএকটি nested লুপের complexity বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. সবচেয়ে ভেতরের কাজটিকে এক একক ধরা
  2. ভেতরের লুপ কতবার চলে, তা বাইরের চলক i-এর ফাংশনে লেখা
  3. বাইরের লুপের সব মানের ওপর Σ লেখা
  4. পরিচিত ধারার সূত্রে closed form বা দুই দিকের সীমা বের করা
  5. প্রধান পদ রেখে Θ লেখা

১০ লাখ (106) উপাদানের একটি সাজানো array-তে binary search করলে worst case-এ সর্বোচ্চ কতগুলো তুলনা লাগে?

  • ২০
  • ১,০০০
  • ৫,০০,০০০
  • ৬

1 + 2 + 4 + 8 + … + 2k-এর মান কত?

  • 2k
  • k(k+1)/2
  • 2k+1 − 1
  • 2k+1 + 1

Hn = 1 + 1/2 + 1/3 + … + 1/n-এর বৃদ্ধির হার কোনটি?

  • Θ(1) — একটি সীমায় থেমে যায়
  • Θ(log n)
  • Θ(√n)
  • Θ(n)

log(n!) কোন Θ-শ্রেণিতে পড়ে?

  • Θ(log n)
  • Θ(n)
  • Θ(n²)
  • Θ(n log n)

লুপটির time complexity কত? for (i = 1; i <= n; i++) for (j = i; j <= n; j += i) count++;

  • Θ(n²)
  • Θ(n)
  • Θ(n log n)
  • Θ(log n)

একটি অ্যালগরিদমের সময় O(log₂ n), আরেকটির O(log₁₀ n). Big-O-র দৃষ্টিতে সম্পর্ক কী?

  • log₁₀ n অ্যালগরিদমটি এক শ্রেণি দ্রুত
  • দুটো একই শ্রেণির — O(log n)
  • log₂ n অ্যালগরিদমটি n বাড়লে অনেক বেশি ধীর হয়ে যায়
  • তুলনা করা সম্ভব নয়

শব্দকোষ

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

মনে রাখুন

  1. log₂ n = কতবার অর্ধেক করলে ১ হয়; binary search-এ সর্বোচ্চ ⌈log₂(n+1)⌉ তুলনা; Big-O-তে log-এর ভিত্তি গুরুত্বহীন।
  2. 1 + 2 + … + n = n(n+1)/2 = Θ(n²); Σ ik = Θ(nk+1).
  3. 1 + 2 + … + 2k = 2k+1 − 1 — দ্বিগুণ ধারায় শেষ পদই সব; তাই vector-এর push গড়ে O(1).
  4. Hn ≈ ln n আর log n! = Θ(n log n); j += i লুপ Θ(n log n).
  5. লুপ → Σ → closed form → Θ; আর AI-এর হিসাব সবসময় ছোট n-এ হাতে গুনে ও counter বসিয়ে যাচাই।

পরের পর্বে: লুপ গোনা শিখলেন। কিন্তু যে ফাংশন নিজেকেই ডাকে, তার ধাপ গুনবেন কীভাবে? পর্ব ০৭-এ রিকার্শন আর call stack — যেখানে আজকের log আর গুণোত্তর ধারা আবার ফিরে আসবে।

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

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