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

বৃদ্ধির হার আর Big-O, Ω, Θ

Growth of Functions and Asymptotic Notation — Big-O, Big-Omega, Big-Theta

Taught in Bangla, this lesson covers growth of functions from constant to factorial and formal definitions of Big-O, Big-Omega and Big-Theta using c and n₀, plus little-o, little-omega and the limit test. It gives rules for sums, products and log bases, derives the time complexity of five loop patterns, measures doubling experiments and clears up common misconceptions.

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

  • প্রচলিত বৃদ্ধির হারগুলো চেনা, আর n দেখে আগেই বলা — কোন অ্যালগরিদম সময়সীমার মধ্যে চলবে
  • O, Ω, Θ, o, ω-এর আনুষ্ঠানিক সংজ্ঞা, আর c ও n₀ বেছে নিয়ে প্রমাণ লেখা
  • Limit test দিয়ে দুটি ফাংশনের বৃদ্ধি তুলনা করা
  • যেকোনো লুপের কাঠামো দেখে ধাপ গুনে time complexity বের করা
  • প্রচলিত ভুল ধারণা এড়ানো, আর AI-এর বলা complexity নিজে যাচাই করা

১ · একই কাজ, দুই পদ্ধতি — কোনটি “দ্রুত”?

একটি বোর্ড পরীক্ষার ফল প্রকাশের দিনের কথা ভাবুন। শিক্ষা বোর্ডের সার্ভারে লাখ লাখ শিক্ষার্থী একসাথে রোল নম্বর দিয়ে ফল খুঁজছেন। দুজন প্রোগ্রামার দুটি সমাধান লিখেছেন। প্রথম জনের প্রোগ্রাম nটি রেকর্ডের জন্য প্রায় 100n ধাপ নেয়। দ্বিতীয় জনের প্রোগ্রাম নেয় n² ধাপ। কোনটি ভালো?

ছোট ইনপুটে উত্তরটা উল্টো মনে হতে পারে। নিচের সারণিটি দেখুন:

n (রেকর্ড)প্রথম পদ্ধতি: 100nদ্বিতীয় পদ্ধতি: n²কে জিতল
101,000100n² — দশ গুণ কম ধাপ
10010,00010,000সমান
1,000100,0001,000,000100n — দশ গুণ কম
10⁶10⁸ (প্রায় ১ সেকেন্ড)10¹² (প্রায় পৌনে ৩ ঘণ্টা)100n — দশ হাজার গুণ কম

n = ১০০-এর পর থেকে প্রথম পদ্ধতি চিরকালের জন্য এগিয়ে যায়, আর ব্যবধান শুধু বাড়তেই থাকে। দ্বিতীয় জন যদি ১০০ গুণ দ্রুত কম্পিউটার কেনেন? তাহলে n = 10⁶-এ তাঁর লাগবে প্রায় 10¹⁰ সরল ধাপের সময় — তবুও প্রথম জনের চেয়ে ১০০ গুণ ধীর। দ্রুত মেশিন ধ্রুবককে ছোট করে, কিন্তু বৃদ্ধির হার বদলায় না।

ঢাকা থেকে চট্টগ্রাম যাওয়ার দুটি পথ ভাবুন। একটি পথে শহরের ভেতরে প্রথম ২০ মিনিট জ্যাম, তারপর ফাঁকা হাইওয়ে। আরেকটি পথ শুরুতে ফাঁকা, কিন্তু প্রতি কিলোমিটারে নতুন নতুন সিগন্যাল, আর যত এগোবেন সিগন্যাল তত ঘন। মিরপুর থেকে ফার্মগেট যেতে দ্বিতীয় পথই ভালো লাগবে। কিন্তু চট্টগ্রাম পর্যন্ত গেলে প্রথম পথ বহু ঘণ্টা আগে পৌঁছাবে। অ্যালগরিদমের বিশ্লেষণে আমরা সবসময় জিজ্ঞেস করি — “দূরত্ব অনেক বড় হলে কী হবে?”

এই চিন্তাকে বলে asymptotic analysis — ইনপুটের আকার n অসীমের দিকে গেলে চলার সময় কীভাবে বাড়ে, সেটিই একমাত্র প্রশ্ন। পর্ব ০২-এ আমরা RAM model দেখেছি: প্রতিটি সরল কাজ (যোগ, তুলনা, একটি ঘর পড়া) এক একক সময় নেয়। তাই একটি অ্যালগরিদমের চলার সময়কে n-এর একটি ফাংশন T(n) হিসেবে লেখা যায়। এই পর্বে আমরা শিখব T(n)-এর growth rate (বৃদ্ধির হার) নিখুঁতভাবে বলার ভাষা, আর সেই ভাষার নিয়ম।

মূল ধারণা

আমরা দুটি জিনিস ইচ্ছে করে বাদ দিই: ধ্রুবক গুণক (মেশিন, ভাষা, compiler ভেদে বদলায়) আর ছোট n-এর আচরণ (ছোট ইনপুটে সব অ্যালগরিদমই দ্রুত)। যা থাকে, সেটিই অ্যালগরিদমের আসল চরিত্র — তার time complexity.

O চিহ্নটি গণিতে আসে কম্পিউটারের অনেক আগে। জার্মান গণিতবিদ Paul Bachmann ১৮৯৪ সালে সংখ্যাতত্ত্বের একটি বইয়ে এটি ব্যবহার করেন। Edmund Landau ১৯০৯ সালে এটিকে জনপ্রিয় করেন এবং ছোট হাতের o যোগ করেন — তাই এদের “Landau symbols”ও বলা হয়। কম্পিউটার বিজ্ঞানে Ω আর Θ-এর আজকের সংজ্ঞা প্রতিষ্ঠা পায় Donald Knuth-এর ১৯৭৬ সালের একটি ছোট প্রবন্ধ “Big Omicron and Big Omega and Big Theta”-এর মাধ্যমে।

২ · ফাংশনের বৃদ্ধি — 1 থেকে n! পর্যন্ত

বাস্তবের প্রায় সব অ্যালগরিদমের চলার সময় হাতে গোনা কয়েকটি ফাংশনের কোনো একটির মতো বাড়ে। এদের নাম আর চেহারা মুখস্থ নয়, অনুভূতিতে গেঁথে নেওয়া দরকার। নিচের সারণিতে log মানে log₂ (ভিত্তি ২)।

f(n)নামn = 10n = 100n = 10⁶উদাহরণ
1constant111array-র i-তম ঘর পড়া
log nlogarithmic3.326.6419.93binary search (পর্ব ১৮)
√nsquare root3.16101,000মৌলিক কি না, ভাগ করে যাচাই
nlinear1010010⁶সবচেয়ে বড় সংখ্যা খোঁজা
n log nlinearithmic33.2664≈ 2.0 × 10⁷merge sort (পর্ব ২১)
n²quadratic10010⁴10¹²সব জোড়া তুলনা, bubble sort
n³cubic1,00010⁶10¹⁸তিন লুপে matrix গুণ
2ⁿexponential1,024≈ 1.27 × 10³⁰৩ লাখের বেশি অঙ্কের সংখ্যাসব subset যাচাই
n!factorial3,628,800≈ 9.33 × 10¹⁵⁷৫৫ লাখের বেশি অঙ্কের সংখ্যাসব বিন্যাস (permutation) যাচাই

শেষ দুটি সারি খেয়াল করুন। n = ১০০ হলেই 2ⁿ প্রায় 1.27 × 10³⁰ — মহাবিশ্বের বয়স সেকেন্ডে মাপলেও মাত্র প্রায় 4 × 10¹⁷। দাবার ছকের সেই পুরোনো গল্পটি মনে আছে? প্রথম ঘরে একটি ধানের দানা, প্রতি ঘরে দ্বিগুণ — ৬৪তম ঘরে লাগে 2⁶³টি দানা, যা পৃথিবীর সব গোলার ধানেও হয় না। Exponential বৃদ্ধি এমনই।

কনটেস্টের হাতের নিয়ম — এক সেকেন্ডে প্রায় 10⁸

একটি আধুনিক কম্পিউটার C++-এ এক সেকেন্ডে মোটামুটি 10⁸ (দশ কোটি) সরল ধাপ চালাতে পারে — যোগ, তুলনা, array পড়া। এটি নিখুঁত মাপ নয়: ধাপের ধরন, cache, ভাষা ভেদে সংখ্যাটি কয়েক গুণ কম-বেশি হয়। Python সাধারণত এর চেয়ে কয়েক ডজন গুণ ধীর। তবু প্রোগ্রাম লেখার আগেই সিদ্ধান্ত নেওয়ার জন্য এটি দারুণ কাজের। Codeforces বা ICPC-ধাঁচের সমস্যায় প্রশ্নে n-এর সীমা দেখেই বোঝা যায় কোন complexity চাওয়া হচ্ছে:

n-এর সীমাযে complexity চলবে (~১ সেকেন্ড)মোট ধাপের আন্দাজ
n ≤ 10–11O(n!)11! ≈ 4 × 10⁷
n ≤ 20–25O(2ⁿ) বা O(2ⁿ · n)2²⁰ · 20 ≈ 2 × 10⁷
n ≤ 400–500O(n³)500³ ≈ 1.25 × 10⁸
n ≤ 5,000–10⁴O(n²)10⁸
n ≤ 10⁵–10⁶O(n log n) বা O(n √n) (ছোট n-এ)10⁶ · 20 = 2 × 10⁷
n ≤ 10⁷–10⁸O(n)10⁸
n ≤ 10¹⁸O(log n), O(√n) (n ≤ 10¹⁴ পর্যন্ত), বা O(1)সামান্য

একটি উদাহরণে ব্যবহারটি দেখুন। ধরুন প্রশ্নে বলা আছে: একটি সরলরেখায় n-টি বাসস্টপের অবস্থান দেওয়া, n ≤ 2 × 10⁵; বের করতে হবে কতগুলো জোড়া স্টপের দূরত্ব k-এর চেয়ে কম। প্রথমেই মাথায় আসে সব জোড়া পরীক্ষা — n(n−1)/2 ≈ 2 × 10¹⁰ জোড়া, অর্থাৎ কয়েক মিনিট। কোড লেখার আগেই তাই বুঝে যান যে এই পথ বন্ধ। দরকার O(n log n) বা O(n) — যেমন আগে sort করে two pointer বা binary search (পর্ব ১৬ ও ১৮)। উল্টো দিকে কোনো প্রশ্নে n ≤ 18 দেখলে সন্দেহ করুন যে সব subset পরীক্ষা বা bitmask DP (পর্ব ৫৫) চাওয়া হচ্ছে — নইলে সীমা এত ছোট রাখার কারণ নেই। সীমা নিজেই সমাধানের ইঙ্গিত।

দুটি ছোট সতর্কতা। প্রথমত, অনেক প্রশ্নে একাধিক test case থাকে; তখন সাধারণত বলা থাকে “সব test case মিলিয়ে n-এর যোগফল ≤ 2 × 10⁵” — হিসাব করতে হবে সেই যোগফল দিয়ে। দ্বিতীয়ত, শুধু সময় নয়, মেমরিরও সীমা থাকে: n = 10⁵ হলে n × n আকারের int টেবিল প্রায় ৪০ GB — সময়ের আগেই মেমরি ফুরিয়ে যাবে। মেমরির হিসাব পর্ব ০৫-এ বিস্তারিত।

এবার নিজে দেখুন। নিচের ছোট টুলটিতে “চালিয়ে দেখুন” চাপুন, তারপর স্লাইডার টেনে n বদলান। প্রতিটি বৃদ্ধির হারে মোট ধাপ, 10⁸ ধাপ/সেকেন্ড ধরে আনুমানিক সময় আর একটি log-স্কেলের দণ্ড দেখাবে। সবুজ মানে এক সেকেন্ডের মধ্যে, কমলা মানে ১০০ সেকেন্ডের মধ্যে, লাল মানে আশা নেই।

const NS = [10, 20, 50, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9];
const FNS = [["1", n => 1], ["log n", n => Math.log2(n)], ["sqrt n", n => Math.sqrt(n)],
  ["n", n => n], ["n log n", n => n * Math.log2(n)], ["n^2", n => n * n], ["n^3", n => n ** 3],
  ["2^n", n => 2 ** n], ["n!", n => { let p = 1; for (let k = 2; k <= n && p < 1e300; k++) p *= k; return p; }]];
const fmt = x => !isFinite(x) || x >= 1e300 ? "> 1e300" : x < 1e6 ? (+x.toFixed(1)).toLocaleString("en") : x.toExponential(1).replace("e+", "e");
const when = s => s < 1e-3 ? "< 1 ms" : s < 1 ? (s * 1e3).toFixed(0) + " ms" : s < 60 ? s.toFixed(1) + " s"
  : s < 3600 ? (s / 60).toFixed(1) + " min" : s < 86400 ? (s / 3600).toFixed(1) + " h"
  : s < 3.156e7 ? (s / 86400).toFixed(0) + " days" : fmt(s / 3.156e7) + " years";
function draw(n) {
  document.getElementById("nv").textContent = n.toLocaleString("en");
  document.getElementById("rows").innerHTML = FNS.map(([lab, f]) => {
    const ops = f(n), s = ops / 1e8, w = Math.min(100, Math.log10(ops + 1) / 18 * 100);
    const col = s <= 1 ? "#16a34a" : s <= 100 ? "#d97706" : "#dc2626";
    return `<tr><td>${lab}</td><td>${fmt(ops)}</td><td>${when(s)}</td>` +
      `<td><div style="height:10px;width:${w}%;background:${col}"></div></td></tr>`;
  }).join("");
}
const slider = document.getElementById("ns");
slider.oninput = () => draw(NS[slider.value]);
draw(NS[slider.value]);
for (const n of [1e5, 1e6]) console.log(`n = ${fmt(n)}: n log n -> ${when(n * Math.log2(n) / 1e8)}, n^2 -> ${when(n * n / 1e8)}`);

শুরুতে n = 1,000,000 দেখায়। নিচের আউটপুটে দুটি লাইন আসে: n = 100,000: n log n -> 17 ms, n^2 -> 1.7 min আর n = 1.0e6: n log n -> 199 ms, n^2 -> 2.8 h (1.0e6 মানে 1.0 × 10⁶). অর্থাৎ n = 10⁵-এ O(n²) প্রায় দেড় মিনিট নেয় — সময়সীমা এক সেকেন্ড হলে নিশ্চিত “Time Limit Exceeded”. স্লাইডার n = 20-এ আনুন: 2ⁿ তখনো সবুজ (প্রায় ১০ লাখ ধাপ)। n = 50-এ 2ⁿ লাল হয়ে কয়েক মাসে পৌঁছায়, আর n! তো অনেক আগেই অসম্ভব। draw ফাংশন প্রতিটি f(n) হিসাব করে টেবিল আঁকে; দণ্ডের দৈর্ঘ্য log₁₀(ধাপ)-এর সমানুপাতিক, কারণ সাধারণ স্কেলে n² আর 2ⁿ একই ছবিতে আঁটানো অসম্ভব।

৩ · Big-O, Ω, Θ — c আর n₀ দিয়ে আনুষ্ঠানিক সংজ্ঞা

“প্রায় n²-এর মতো বাড়ে” কথাটি বুঝতে সহজ, কিন্তু প্রমাণে চলে না। তাই গণিতের ভাষায় নিখুঁত সংজ্ঞা দরকার। সব সংজ্ঞায় f(n) আর g(n) এমন ফাংশন, যারা বড় n-এ ঋণাত্মক নয় (চলার সময় কখনো ঋণাত্মক হয় না)।

O — ওপরের সীমা

f(n) = O(g(n)), যদি এমন ধ্রুবক c > 0 আর n₀ থাকে যে সব n ≥ n₀-এর জন্য 0 ≤ f(n) ≤ c · g(n).

“f, g-এর চেয়ে দ্রুত বাড়ে না”

Ω — নিচের সীমা

f(n) = Ω(g(n)), যদি এমন ধ্রুবক c > 0 আর n₀ থাকে যে সব n ≥ n₀-এর জন্য f(n) ≥ c · g(n) ≥ 0.

“f, g-এর চেয়ে ধীরে বাড়ে না”

Θ — আঁটসাঁট সীমা

f(n) = Θ(g(n)), যদি ধ্রুবক c₁, c₂ > 0 আর n₀ থাকে যে সব n ≥ n₀-এর জন্য c₁ · g(n) ≤ f(n) ≤ c₂ · g(n).

Θ = O আর Ω দুটোই

সংজ্ঞার দুটি অংশের ভূমিকা আলাদা। c ধ্রুবক গুণককে “মাফ” করে দেয় — 3n² আর n² একই শ্রেণির। n₀ ছোট n-এর গোলমালকে মাফ করে — শুরুতে f একটু বেশি হলেও চলে, শুধু একটি বিন্দুর পর থেকে চিরকাল সীমার নিচে থাকতে হবে। নিচের ছবিটি দেখুন:

f(n) = O(g(n)) — n₀-এর পর থেকে f সবসময় c·g-এর নিচে

Beyond n₀, c·g(n) is an upper bound for f(n)

n সময় n₀ c·g(n) f(n) এখানে f বড় হলেও সমস্যা নেই
ছবি ৪.১ — n₀-এর বাঁ দিকে f(n) কখনো কখনো c·g(n)-এর ওপরে উঠেছে; তাতে কিছু যায় আসে না। n₀-এর ডান দিকে চিরকাল নিচে থাকলেই f(n) = O(g(n)).

“=” চিহ্নটি আসলে “∈”। O(g(n)) হলো ফাংশনের একটি সেট — যত ফাংশন g-এর চেয়ে দ্রুত বাড়ে না, সবাই। তাই “3n + 2 = O(n)” মানে “3n + 2 ∈ O(n)”. এজন্যই এই “সমান” এক দিকে চলে: n = O(n²) সত্য, কিন্তু উল্টে “O(n²) = n” লেখা অর্থহীন।

এবার সবচেয়ে কাজের দক্ষতা: সংজ্ঞা থেকে সরাসরি প্রমাণ। দাবি: f(n) = 3n² + 5n + 2 = Θ(n²). আমাদের তিনটি সংখ্যা খুঁজে বের করতে হবে — c₁, c₂ আর n₀. ধাপে ধাপে দেখুন:

লক্ষ্যতিনটি ধ্রুবক খুঁজতে হবে
খুঁজতে হবে c₁ > 0, c₂ > 0, n₀ — যাতে সব n ≥ n₀-এর জন্য

    c₁ · n²  ≤  3n² + 5n + 2  ≤  c₂ · n²

বাঁ অংশ = Ω(n²),  ডান অংশ = O(n²)
ওপরের সীমাপ্রতিটি ছোট পদকে n² দিয়ে ঢেকে দিই
n ≥ 1 হলে:   5n ≤ 5n²    আর    2 ≤ 2n²

3n² + 5n + 2  ≤  3n² + 5n² + 2n²  =  10n²

=> c₂ = 10,  n₀ = 1
সংখ্যা বসিয়ে যাচাইf(n) ≤ 10n² ?
n = 1  :  f = 3 + 5 + 2       = 10    ≤  10     ✓ (ঠিক সমান)
n = 2  :  f = 12 + 10 + 2     = 24    ≤  40     ✓
n = 10 :  f = 300 + 50 + 2    = 352   ≤  1000   ✓
(যাচাই প্রমাণ নয় — শুধু ভুল ধরার সহায়ক)
নিচের সীমাধনাত্মক পদ ফেলে দিলে মান কমে
n ≥ 1 হলে 5n + 2 > 0, তাই

3n² + 5n + 2  ≥  3n²

=> c₁ = 3
জোড়া দিইΘ প্রমাণ সম্পূর্ণ
সব n ≥ 1-এর জন্য:   3n²  ≤  3n² + 5n + 2  ≤  10n²

c₁ = 3,  c₂ = 10,  n₀ = 1   =>   f(n) = Θ(n²)   ∎

অন্য বাছাইও চলে:  n ≥ 6 হলে 5n + 2 ≤ n²,
তাই c₂ = 4, n₀ = 6 দিয়েও প্রমাণ হয়।
উল্টো দিকf(n) = O(n) কেন নয়?
ধরি কোনো c, n₀ আছে যাতে সব n ≥ n₀-এ  3n² + 5n + 2 ≤ c · n

তাহলে  3n² ≤ c · n   =>   n ≤ c / 3

কিন্তু n = max(n₀, c) নিলে n > c / 3 — বিরোধ!
=> এমন c নেই, তাই f(n) ≠ O(n)
  1. লক্ষ্য ঠিক করা। Θ প্রমাণ মানে দুটি অসমতা একসাথে প্রমাণ — বাঁ দিকেরটি Ω(n²), ডান দিকেরটি O(n²). আমাদের কাজ শুধু তিনটি ধ্রুবক খুঁজে দেখানো যে অসমতা দুটি n₀-এর পর সবসময় সত্য।
  2. ওপরের সীমা — উদার কৌশল। n ≥ 1 হলে n ≤ n² আর 1 ≤ n², তাই 5n ≤ 5n² আর 2 ≤ 2n². প্রতিটি ছোট পদকে n²-এর গুণিতক দিয়ে ঢেকে দিলে পাই f(n) ≤ 10n². অর্থাৎ c₂ = 10 আর n₀ = 1. সব সহগ যোগ করে c বানানো — এই কৌশল যেকোনো বহুপদীতে চলে।
  3. সংখ্যা বসিয়ে মিলিয়ে নেওয়া। n = 1-এ f(1) = 10, আর 10 · 1² = 10 — ঠিক সমান, তাই অসমতা টেকে। n = 2-এ 24 ≤ 40, n = 10-এ 352 ≤ 1000. এমন যাচাই প্রমাণের বদলি নয়, কিন্তু বীজগণিতে ভুল থাকলে সাথে সাথে ধরা পড়ে।
  4. নিচের সীমা — আরও সহজ। 5n + 2 সবসময় ধনাত্মক, তাই সেটি ফেলে দিলে মান কমে: f(n) ≥ 3n². অর্থাৎ c₁ = 3 নিলেই চলে, n ≥ 1-এর জন্য।
  5. দুটি জোড়া দিয়ে প্রমাণ শেষ। c₁ = 3, c₂ = 10, n₀ = 1 দিয়ে সংজ্ঞা পূরণ হলো, তাই 3n² + 5n + 2 = Θ(n²). খেয়াল করুন, ধ্রুবক একটিমাত্র নয়: n ≥ 6 হলে 5n + 2 ≤ n² (n = 6-এ 32 ≤ 36), তাই c₂ = 4, n₀ = 6-ও কাজ করে। ছোট c চাইলে বড় n₀ লাগে — এটিই সাধারণ লেনদেন।
  6. উল্টো দিক — f(n) = O(n) কেন মিথ্যা। প্রমাণ বিরোধের মাধ্যমে: কোনো c থাকলে 3n² ≤ c · n হতো, অর্থাৎ n ≤ c/3. কিন্তু n যত বড় খুশি নেওয়া যায় — যেমন n = max(n₀, c) — তখন n > c/3. কোনো ধ্রুবক চিরকাল বাড়তে থাকা n-কে আটকাতে পারে না। “O নয়” প্রমাণের ধাঁচ সবসময় এমনই।
বহুপদীর সাধারণ নিয়ম

যদি f(n) = adnd + … + a1n + a0 হয় আর সবচেয়ে বড় ঘাতের সহগ ad > 0 হয়, তাহলে f(n) = Θ(nd). ওপরের প্রমাণটি এই নিয়মেরই একটি উদাহরণ।

৪ · little-o, little-ω আর limit test

O(g) বলে “f, g-এর চেয়ে দ্রুত বাড়ে না” — সমানতালে বাড়লেও চলে। কিন্তু কখনো আমরা বলতে চাই “f কঠোরভাবে ধীরে বাড়ে”, যেমন n বনাম n². তার জন্য ছোট হাতের চিহ্ন:

  • little-o: f(n) = o(g(n)), যদি প্রতিটি ধ্রুবক c > 0-এর জন্য এমন n₀ থাকে যে সব n ≥ n₀-এ 0 ≤ f(n) < c · g(n). পার্থক্যটি সূক্ষ্ম: O-তে “কোনো একটি c” পেলেই হয়; o-তে c যত ছোটই নিন, একসময় f তার নিচে নামবেই।
  • little-ω: f(n) = ω(g(n)), যদি প্রতিটি c > 0-এর জন্য একসময় f(n) > c · g(n). অর্থাৎ f কঠোরভাবে দ্রুত বাড়ে। g = o(f) আর f = ω(g) একই কথা।

পাঁচটি চিহ্নকে সংখ্যার তুলনার সাথে মিলিয়ে মনে রাখা যায় — শুধু মনে রাখবেন, এটি উপমা, হুবহু এক নয়:

চিহ্নসংখ্যার উপমাউদাহরণ (সত্য)
f = O(g)f ≤ g5n = O(n), n = O(n²)
f = Ω(g)f ≥ gn² = Ω(n), n² = Ω(n²)
f = Θ(g)f = gn²/2 − 3n = Θ(n²)
f = o(g)f < gn = o(n²), log n = o(√n)
f = ω(g)f > g2ⁿ = ω(n¹⁰⁰)

Limit test — ভাগ করে দেখুন

দুটি ফাংশন তুলনার সবচেয়ে দ্রুত উপায় হলো তাদের ভাগফলের limit নেওয়া। ধরুন L = limn→∞ f(n) / g(n) আছে। তাহলে:

L = 0  ⇒  f = o(g) (তাই O-ও, কিন্তু Θ নয়)
0 < L < ∞  ⇒  f = Θ(g)
L = ∞  ⇒  f = ω(g) (তাই Ω-ও, কিন্তু Θ নয়)

কয়েকটি উদাহরণ, প্রয়োজনে L’Hôpital-এর নিয়ম (লব আর হর দুটোই অসীমে গেলে দুটির derivative-এর ভাগফল নেওয়া) ব্যবহার করে:

  • (3n² + 5n + 2) / n² = 3 + 5/n + 2/n² → 3. যেহেতু 0 < 3 < ∞, তাই Θ(n²) — আগের প্রমাণের সাথে মিলে গেল, অনেক কম পরিশ্রমে।
  • ln n / √n: দুটোই অসীমে যায়। derivative নিলে (1/n) / (1/(2√n)) = 2/√n → 0. তাই log n = o(√n) — যেকোনো log যেকোনো ধনাত্মক ঘাতের চেয়ে ধীরে বাড়ে।
  • n² / 2ⁿ: দুবার L’Hôpital নিলে 2 / (2ⁿ (ln 2)²) → 0. তাই n² = o(2ⁿ); একইভাবে যেকোনো nk = o(2ⁿ).
  • 2ⁿ / 3ⁿ = (2/3)ⁿ → 0. তাই 2ⁿ = o(3ⁿ) — exponential-এর ভিত্তি বদলালে শ্রেণিই বদলে যায়!
  • 2ⁿ / n! = (2/1)(2/2)(2/3)…(2/n) ≤ 2 · (2/n) = 4/n → 0. তাই 2ⁿ = o(n!).

এভাবে প্রচলিত ফাংশনগুলোকে একটি শিকলে সাজানো যায়, যেখানে প্রতিটি পরেরটির little-o:

1 ≺ log n ≺ √n ≺ n ≺ n log n ≺ n² ≺ n³ ≺ 2ⁿ ≺ 3ⁿ ≺ n! ≺ nⁿ

Limit test একমুখী সুবিধা। Limit থাকলে সিদ্ধান্ত পরিষ্কার, কিন্তু limit না থাকলেও f = O(g) হতে পারে। যেমন f(n) = n হলে n জোড় আর 2n হলে n বিজোড় — f/n-এর মান 1 আর 2-এর মধ্যে দোলে, limit নেই, অথচ f = Θ(n) (c₁ = 1, c₂ = 2). তখন সরাসরি সংজ্ঞা দিয়ে প্রমাণ করতে হয়।

৫ · হিসাবের নিয়ম — যোগ, গুণ, ছোট পদ আর log-এর ভিত্তি

প্রতিবার c আর n₀ খুঁজে প্রমাণ করতে হলে বিশ্লেষণ খুব ধীর হতো। সৌভাগ্যক্রমে কয়েকটি নিয়ম সংজ্ঞা থেকেই প্রমাণ হয়, আর এগুলো দিয়েই বাস্তবের প্রায় সব হিসাব হয়ে যায়।

নিয়মসূত্রকোথায় লাগে
ধ্রুবক বাদk · f = Θ(f), যেকোনো ধ্রুবক k > 05n আর n একই শ্রেণি
যোগের নিয়মO(f) + O(g) = O(max(f, g))পরপর দুটি অংশ: বড়টি টেকে
গুণের নিয়মO(f) · O(g) = O(f · g)লুপের ভেতরে লুপ
ছোট পদ বাদn³ + 100n² + 7 = Θ(n³)বহুপদীতে সবচেয়ে বড় ঘাত
log-এর ভিত্তিloga n = logb n / logb a = Θ(logb n)“O(log n)” লিখতে ভিত্তি লাগে না
সংক্রমণ (transitivity)f = O(g), g = O(h) ⇒ f = O(h)শিকল ধরে তুলনা

যোগের নিয়ম কেন সত্য: f ≤ c₁·F আর g ≤ c₂·G হলে f + g ≤ c₁F + c₂G ≤ (c₁ + c₂) · max(F, G). তাই n log n সময়ের sort-এর পর n সময়ের একটি লুপ চালালে মোট O(n log n + n) = O(n log n). গুণের নিয়মও একইভাবে: f · g ≤ (c₁c₂) · F·G.

log-এর ভিত্তি কেন গুরুত্বহীন: log₂ n = log₁₀ n / log₁₀ 2 ≈ 3.32 · log₁₀ n. দুটির পার্থক্য শুধু একটি ধ্রুবক গুণক, আর ধ্রুবক তো আমরা বাদই দিই। কিন্তু সাবধান — ঘাতে ভিত্তি গুরুত্বপূর্ণ। 2ⁿ আর 4ⁿ = 2²ⁿ আলাদা শ্রেণি (ভাগফল 2ⁿ, ধ্রুবক নয়)। একইভাবে 2log₂ n = n, কিন্তু 2log₄ n = √n.

দুটি আলাদা ইনপুট থাকলে দুটিই রাখুন। একটি গ্রাফে V-টি শীর্ষ আর E-টি প্রান্ত থাকলে BFS-এর সময় O(V + E) — এটিকে “O(n)” বলে গুলিয়ে ফেলবেন না, কারণ E প্রায় V² পর্যন্ত হতে পারে। দুটি string-এর দৈর্ঘ্য n আর m হলে LCS-এর DP হয় O(n · m). কোন চলক কোন পরিমাণ মাপে, সেটি সবসময় লিখে দিন।

একটি পূর্ণ উদাহরণ — নিয়মগুলো একসাথে

ধরুন একটি ফাংশন bKash-এর মতো কোনো লেনদেন-তালিকার n-টি অঙ্ক নিয়ে দেখে, কোন দুটি লেনদেনের যোগফল ঠিক x টাকা। কোডের কাঠামো এমন (Pseudocode, 1-based):

sort(a[1..n])                          // Θ(n log n)
for i = 1 to n:                        // n বার
    if binary_search(a, x - a[i]):     // প্রতিবার Θ(log n)
        count = count + 1
for i = 1 to 100:                      // প্রথম ১০০টির রিপোর্ট
    for j = 1 to 100: compare(a[i], a[j])   // 10⁴ বার — n-এর ওপর নির্ভর করে না

এবার নিয়মগুলো একে একে লাগাই। প্রথম লাইন Θ(n log n). দ্বিতীয় অংশে বাইরে n বার, প্রতিবার ভেতরে Θ(log n) — গুণের নিয়মে Θ(n log n). তৃতীয় অংশ 10⁴টি তুলনা, n যত বড়ই হোক সংখ্যাটি একই — অর্থাৎ Θ(1). তিনটি অংশ পরপর চলে, তাই যোগের নিয়মে মোট Θ(n log n + n log n + 1) = Θ(n log n). খেয়াল করুন, তৃতীয় অংশে দুটি nested লুপ থাকলেও সেটি n² নয় — লুপের সংখ্যা নয়, লুপ কতবার চলে আর সেটি n-এর ওপর নির্ভর করে কি না, সেটিই আসল।

৬ · লুপ গুনে complexity — পাঁচটি কাঠামো

বাস্তব কোডের বিশ্লেষণ মানে মূলত লুপ গোনা: সবচেয়ে ভেতরের কাজটি মোট কতবার হয়? নিয়মগুলো এবার কাজে লাগাই।

(ক) পরপর লুপ — যোগ

for i = 1 to n:      do A        // n বার
for j = 1 to n*n:    do B        // n² বার
মোট = n + n² = Θ(n²)            // বড়টি টেকে

(খ) স্বাধীন nested লুপ — গুণ

বাইরের লুপ n বার, প্রতিবার ভেতরের লুপ m বার: মোট n · m. দুটোই n হলে Θ(n²)। যেমন একটি ক্লাসের সব জোড়া ছাত্রের রোল মেলানো।

(গ) নির্ভরশীল ভেতরের লুপ — j চলে i থেকে n

এখানে ভেতরের লুপের দৈর্ঘ্য প্রতিবার বদলায়, তাই সরাসরি গুণ করা যায় না। ছোট n নিয়ে হাতে গুনি — n = 4:

1long long cnt = 0;2for (int i = 0; i < n; i++)3    for (int j = i; j < n; j++)4        cnt++;5printf("cnt = %lld\n", cnt);
ভ্যারিয়েবলের মান (n = 4)
i
j-এর মান
ভেতরে কতবার
cnt
  1. শুরু। cnt = 0. সবচেয়ে ভেতরের cnt++ কতবার চলে, সেটিই মোট কাজের মাপ। n = 4 নিয়ে হাতে গুনব, তারপর সাধারণ সূত্র বানাব।
  2. i = 0: j চলে 0, 1, 2, 3 — ভেতরের লুপ পুরো 4 বার। cnt = 0 + 4 = 4.
  3. i = 1: j শুরু হয় 1 থেকে — 1, 2, 3, মাত্র 3 বার। cnt = 4 + 3 = 7. প্রতিবার ভেতরের লুপ এক ঘর ছোট হচ্ছে।
  4. i = 2: j = 2, 3 — 2 বার। cnt = 7 + 2 = 9.
  5. i = 3: j = 3 — শুধু 1 বার। cnt = 9 + 1 = 10. এরপর i = 4 হলে বাইরের শর্ত 4 < 4 মিথ্যা।
  6. ফল: cnt = 10 = 4 + 3 + 2 + 1. সাধারণভাবে n + (n−1) + … + 1 = n(n+1)/2 = n²/2 + n/2. ধ্রুবক ½ আর ছোট পদ n/2 বাদ দিলে Θ(n²). পূর্ণ nested লুপের প্রায় অর্ধেক কাজ, কিন্তু একই শ্রেণি। (n = 4-এ 4·5/2 = 10 — মিলে গেল।)

(ঘ) প্রতিবার অর্ধেক — log n

যে লুপে চলকটি প্রতিবার অর্ধেক (বা দ্বিগুণ) হয়, সেটি কতবার চলে? n = 20 নিয়ে দেখুন — পাশে i-এর binary রূপও আছে:

1int cnt = 0;2for (int i = n; i >= 1; i /= 2)3    cnt++;4printf("cnt = %d\n", cnt);
ভ্যারিয়েবলের মান (n = 20)
i
i (binary)
cnt
  1. শুরু। cnt = 0, এখনো লুপে ঢোকেনি।
  2. i = 20 (binary 10100). 20 ≥ 1, তাই ভেতরে: cnt = 1. তারপর i = 20 / 2.
  3. i = 10 (1010). cnt = 2. খেয়াল করুন — ২ দিয়ে ভাগ মানে binary-র ডানের একটি অঙ্ক ফেলে দেওয়া।
  4. i = 5 (101). cnt = 3. এবার 5 / 2 = 2 — integer ভাগে দশমিক বাদ যায়।
  5. i = 2 (10). cnt = 4.
  6. i = 1 (1). 1 ≥ 1, তাই শেষবার ভেতরে: cnt = 5. এরপর i = 1 / 2 = 0.
  7. i = 0, শর্ত 0 ≥ 1 মিথ্যা — লুপ শেষ, ছাপা হলো cnt = 5. 20-এর binary-তে ৫টি অঙ্ক, আর ⌊log₂ 20⌋ + 1 = 4 + 1 = 5. সাধারণভাবে লুপটি ⌊log₂ n⌋ + 1 বার চলে, অর্থাৎ Θ(log n). n = 10⁶ হলেও মাত্র ২০ বার!

এই কাঠামোই binary search-এর (পর্ব ১৮) প্রাণ: প্রতি ধাপে খোঁজার জায়গা অর্ধেক। ফোনবুকে নাম খোঁজার সময় আপনিও মাঝখান থেকে খোলেন, তারপর বাকি অর্ধেকে যান — ১০০০ পাতার বইয়ে ১০ বারের মতো খুললেই পাতা পেয়ে যান।

(ঙ) i · i ≤ n পর্যন্ত — √n

কোনো সংখ্যা মৌলিক কি না যাচাই করতে √n পর্যন্ত ভাগ করলেই চলে, কারণ n = a · b হলে a আর b-এর অন্তত একটি ≤ √n. লুপটি দেখতে এমন — n = 30 নিয়ে গুনি:

1int cnt = 0;2for (int i = 1; i * i <= n; i++)3    cnt++;4printf("cnt = %d\n", cnt);
ভ্যারিয়েবলের মান (n = 30)
i
i * i
cnt
  1. শুরু। cnt = 0. শর্তে i নয়, i-এর বর্গ n-এর সাথে তুলনা হবে।
  2. i = 1, i·i = 1 ≤ 30 — ভেতরে। cnt = 1.
  3. i = 2, i·i = 4 ≤ 30. cnt = 2.
  4. i = 3, i·i = 9 ≤ 30. cnt = 3.
  5. i = 4, i·i = 16 ≤ 30. cnt = 4.
  6. i = 5, i·i = 25 ≤ 30. cnt = 5. i বাড়ছে একে একে, কিন্তু i·i বাড়ছে অনেক দ্রুত।
  7. i = 6, i·i = 36 — 30-এর বেশি, লুপ শেষ। cnt = 5 = ⌊√30⌋. সাধারণভাবে লুপটি ⌊√n⌋ বার চলে: Θ(√n). n = 10¹²-এও মাত্র 10⁶ বার — এক সেকেন্ডের অনেক কম।

এক নজরে — লুপের কাঠামো আর তার complexity

কাঠামোমোট পুনরাবৃত্তিশ্রেণি
for i in 0..n-1nΘ(n)
দুটি স্বাধীন লুপ, প্রতিটি nn²Θ(n²)
for i, ভেতরে for j = i..n-1n(n+1)/2Θ(n²)
i /= 2 বা i *= 2⌊log₂ n⌋ + 1Θ(log n)
i * i <= n⌊√n⌋Θ(√n)
বাইরে n বার, ভেতরে অর্ধেকের লুপn(⌊log₂ n⌋ + 1)Θ(n log n)
for i = 1..n, ভেতরে j += i (j ≤ n)n/1 + n/2 + … + n/n ≈ n ln nΘ(n log n) — প্রমাণ পর্ব ০৬-এ
i *= 2 বাইরে, ভেতরে j < i1 + 2 + 4 + … ≤ 2nΘ(n) — অনুশীলন ১ দেখুন

শুধু “কয়টি লুপ” গুনবেন না, প্রতিটি লুপ কতবার চলে সেটি গুনুন। শেষ সারিটি দেখতে দুই লুপের মতো, কিন্তু মোট কাজ Θ(n). আবার উল্টোটাও হয় — একটি লুপের ভেতরে s = s + ch (Python string জোড়া) বা v.erase(v.begin()) প্রতিবার পুরো ডেটা কপি করতে পারে, তখন “একটিমাত্র লুপ” আসলে Θ(n²).

৭ · নিজে গুনে দেখুন — দ্বিগুণ n, কত গুণ কাজ?

তত্ত্ব হলো। এবার পরীক্ষা। নিচের প্রোগ্রামটি ছয় ধরনের লুপের ভেতরের কাজ সত্যিই গুনে দেখে — n = 250, 500, 1000, 2000-এর জন্য। শেষ কলামে আছে ratio = গণনা(2000) / গণনা(1000). এটিই “doubling test”: n দ্বিগুণ করলে কাজ কত গুণ হয়, সেটি দেখেই বৃদ্ধির হার চেনা যায়। আপনার ভাষার ট্যাবটি বেছে নিন:

#include <cstdio>
#include <vector>
using ll = long long;
ll lin(ll n)  { ll c = 0; for (ll i = 0; i < n; i++) c++; return c; }
ll quad(ll n) { ll c = 0; for (ll i = 0; i < n; i++) for (ll j = 0; j < n; j++) c++; return c; }
ll tri(ll n)  { ll c = 0; for (ll i = 0; i < n; i++) for (ll j = i; j < n; j++) c++; return c; }
ll halv(ll n) { ll c = 0; for (ll i = n; i >= 1; i /= 2) c++; return c; }
ll root(ll n) { ll c = 0; for (ll i = 1; i * i <= n; i++) c++; return c; }
ll nlog(ll n) { ll c = 0; for (ll i = 0; i < n; i++) for (ll j = n; j >= 1; j /= 2) c++; return c; }
int main() {
    std::vector<ll> ns = {250, 500, 1000, 2000};
    std::vector<std::pair<const char*, ll (*)(ll)>> loops = {
        {"n", lin}, {"n^2", quad}, {"n(n+1)/2", tri}, {"log n", halv}, {"sqrt n", root}, {"n log n", nlog}};
    std::printf("%-10s", "loop");
    for (ll n : ns) std::printf("%10lld", n);
    std::printf("%8s\n", "ratio");
    for (auto [label, f] : loops) {
        std::vector<ll> cnt; std::printf("%-10s", label);
        for (ll n : ns) { cnt.push_back(f(n)); std::printf("%10lld", cnt.back()); }
        std::printf("%8.2f\n", (double)cnt[3] / cnt[2]);
    }
}

আউটপুট:

loop             250       500      1000      2000   ratio
n                250       500      1000      2000    2.00
n^2            62500    250000   1000000   4000000    4.00
n(n+1)/2       31375    125250    500500   2001000    4.00
log n              8         9        10        11    1.10
sqrt n            15        22        31        44    1.42
n log n         2000      4500     10000     22000    2.20

প্রতিটি ফাংশন একটি লুপ-কাঠামোর ভেতরের কাজ গোনে আর গণনাটি ফেরত দেয়; loops তালিকায় নাম আর function pointer জোড়া করে রাখা, তাই একই লুপে ছয়টিই চালানো যায়। গণনা long long-এ রাখা হয়েছে, কারণ n বড় করলে n² দ্রুত int-এর সীমা (প্রায় 2.1 × 10⁹) পেরিয়ে যায়। শেষ কলাম cnt[3] / cnt[2] = গণনা(2000) / গণনা(1000).

def lin(n):                         # একটি লুপ
    return sum(1 for i in range(n))
def quad(n):                        # দুটি পূর্ণ nested লুপ
    return sum(1 for i in range(n) for j in range(n))
def tri(n):                         # ভেতরের লুপ i থেকে n পর্যন্ত
    return sum(1 for i in range(n) for j in range(i, n))
def halv(n):                        # প্রতিবার অর্ধেক
    c, i = 0, n
    while i >= 1: c, i = c + 1, i // 2
    return c
def root(n):                        # i*i <= n পর্যন্ত
    c, i = 0, 1
    while i * i <= n: c, i = c + 1, i + 1
    return c
def nlog(n): return n * halv(n)     # বাইরে n বার, ভেতরে অর্ধেক-লুপ

ns = [250, 500, 1000, 2000]
print(f"{'loop':<10}" + "".join(f"{n:>10}" for n in ns) + f"{'ratio':>8}")
for label, f in [("n", lin), ("n^2", quad), ("n(n+1)/2", tri),
                 ("log n", halv), ("sqrt n", root), ("n log n", nlog)]:
    cnt = [f(n) for n in ns]
    print(f"{label:<10}" + "".join(f"{c:>10}" for c in cnt) + f"{cnt[-1] / cnt[-2]:>8.2f}")

আউটপুট C++ সংস্করণের হুবহু এক:

loop             250       500      1000      2000   ratio
n                250       500      1000      2000    2.00
n^2            62500    250000   1000000   4000000    4.00
n(n+1)/2       31375    125250    500500   2001000    4.00
log n              8         9        10        11    1.10
sqrt n            15        22        31        44    1.42
n log n         2000      4500     10000     22000    2.20

এখানে লুপগুলো sum(1 for …) আকারে লেখা — প্রতিটি পুনরাবৃত্তিতে একটি 1 যোগ হয়, তাই যোগফলই গণনা। nlog সরাসরি n · halv(n) ফেরত দেয়, কারণ ভেতরের অর্ধেক-লুপ প্রতিবার একই সংখ্যক বার চলে। Python-এ একই কাজ C++-এর চেয়ে অনেক ধীরে চলে, কিন্তু গণনা আর ratio এক — ভাষা বদলায় ধ্রুবক, শ্রেণি নয়।

ratio কলামটি পড়ুন। Θ(n) দ্বিগুণ হয় (2.00), Θ(n²) চার গুণ (4.00) — নির্ভরশীল লুপ n(n+1)/2-ও চার গুণ, কারণ ½ ধ্রুবক ratio-তে কেটে যায়। Θ(log n) গুণ হয় না, শুধু এক যোগ হয় (10 → 11). Θ(√n) বাড়ে √2 ≈ 1.41 গুণ (31 → 44, ⌊√⌋-এর কারণে 1.42). আর Θ(n log n)-এর ratio 2-এর একটু বেশি (2.20) — n দ্বিগুণ, log n-এ সামান্য বৃদ্ধি। সাধারণ নিয়ম: n দ্বিগুণে কাজ প্রায় 2k গুণ হলে বৃদ্ধি Θ(nk).

৮ · প্রচলিত ভুল ধারণা — আর পরের পর্বের দরজা

ভুল ১: “O মানেই worst case”

না। O, Ω, Θ হলো ফাংশনের সীমা বলার ভাষা — কোন ফাংশনের কথা বলছেন, সেটি আলাদা প্রশ্ন। Insertion sort-এর (পর্ব ২০) কথা ধরুন। আগে থেকে সাজানো array-তে সে নেয় Θ(n) সময় — এটি তার best case. উল্টো করে সাজানো array-তে নেয় Θ(n²) — এটি তার worst case. দুটি আলাদা ফাংশন, আর প্রতিটির জন্যই O, Ω, Θ তিনটি বলা যায়। “Insertion sort-এর worst case Θ(n²)” আর “best case Θ(n)” — দুটোই নিখুঁত বাক্য। “সব ইনপুটে insertion sort O(n²) আর Ω(n)” — এটিও সত্য। Best, worst আর average case কীভাবে আলাদা করে বিশ্লেষণ করতে হয়, আর মেমরির হিসাব — space complexity — পুরোটাই পর্ব ০৫-এর বিষয়।

ভুল ২: দৈনন্দিন কথায় O বলে Θ বোঝানো

প্রোগ্রামাররা প্রায়ই বলেন “merge sort O(n log n)” — বোঝাতে চান Θ(n log n). এটি সাধারণত ক্ষতিকর নয়, তবে মনে রাখুন: O শুধু upper bound. “Binary search O(n)” কথাটিও গাণিতিকভাবে সত্য (log n ≤ n), কিন্তু প্রায় অর্থহীন — যেমন “ঢাকা থেকে সিলেট ১০,০০০ কিলোমিটারের কম” সত্য, কিন্তু কাজে লাগে না। পরীক্ষায় বা ইন্টারভিউতে সবচেয়ে আঁটসাঁট সীমাটি বলুন, আর প্রয়োজনে স্পষ্ট করে Θ লিখুন। একইভাবে lower bound Ω কোনো সমস্যার জন্যও বলা যায় — যেমন তুলনা-ভিত্তিক যেকোনো sort-এর worst case Ω(n log n), যার প্রমাণ পর্ব ২৪-এ।

ভুল ৩: “ধ্রুবক কখনো গুরুত্বপূর্ণ নয়”

বিশ্লেষণে ধ্রুবক বাদ দিই, কিন্তু বাস্তবে ধ্রুবক সময় খায়। 100n বনাম n²-এর সারণিতে n < 100 হলে n²-ই জিতেছিল। এজন্যই বাস্তবের sort লাইব্রেরিগুলো ছোট অংশে (কয়েক ডজন উপাদান পর্যন্ত) insertion sort চালায় — Θ(n²) হলেও তার ধ্রুবক খুব ছোট। আবার array আর linked list দুটোতেই সব উপাদান ঘুরে দেখা Θ(n), কিন্তু array সাধারণত কয়েক গুণ দ্রুত, কারণ পাশাপাশি ঘর CPU-র cache-এ একসাথে আসে (পর্ব ১০ ও ১২)। সিদ্ধান্তের ক্রম: আগে সঠিক শ্রেণি বাছুন, তারপর একই শ্রেণির মধ্যে ধ্রুবক মাপুন।

ভুল ৪: “O(1) মানে তাৎক্ষণিক”

O(1) মানে শুধু “n-এর ওপর নির্ভর করে না”। একটি লুপ যদি সবসময় ঠিক 10⁹ বার চলে, সেটিও O(1) — কিন্তু কয়েক সেকেন্ড লাগবে। আবার hash table-এর (পর্ব ৩২) “O(1)” আসলে গড় হিসাব; খারাপ ইনপুটে একটি খোঁজ অনেক ধীর হতে পারে।

ভুল ৫: “n মানে সবসময় সংখ্যাটি নিজে”

n হলো ইনপুটের আকার। একটি সংখ্যা N মৌলিক কি না, √N পর্যন্ত ভাগ করে দেখা মানে O(√N) ধাপ। কিন্তু N লিখতে লাগে মাত্র প্রায় log₂ N বিট। বিটের সংখ্যা b ধরলে √N = 2b/2 — ইনপুটের আকারের তুলনায় exponential! এই সূক্ষ্মতা পরে knapsack-এর DP (পর্ব ৫৩) আর P বনাম NP (পর্ব ৬৪) বুঝতে লাগবে।

আরও গভীরে: log-এর যেকোনো ঘাতও যেকোনো ধনাত্মক ঘাতের চেয়ে ধীরে বাড়ে: সব k আর সব ε > 0-এর জন্য (log n)k = o(nε) — limit test-এ বারবার L’Hôpital নিলেই প্রমাণ হয়। তাই গবেষণাপত্রে log-এর গুণক লুকিয়ে রাখতে “soft-O” চিহ্ন Õ ব্যবহার হয়: Õ(n) মানে O(n · (log n)k), কোনো ধ্রুবক k-এর জন্য। আরেকটি কাজের তথ্য: log(n!) = Θ(n log n), কারণ log(n!) ≤ n log n, আবার শেষ n/2টি গুণকের প্রতিটি ≥ n/2 বলে log(n!) ≥ (n/2) log(n/2). পর্ব ২৪-এ sort-এর নিম্নসীমার প্রমাণে ঠিক এটিই লাগবে।

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

  • প্রমাণ করুন 3n² + 5n + 2 = Θ(n²). — c₁ = 3, c₂ = 10, n₀ = 1 নিয়ে দুটি অসমতা দেখান (অংশ ৩)।
  • n = O(n²) সত্য, তাহলে n = Θ(n²) কেন নয়? — n ≥ c·n² হলে n ≤ 1/c, বড় n-এ মিথ্যা; তাই Ω(n²) নয়।
  • log₂ n আর log₁₀ n কি আলাদা complexity? — না, পার্থক্য শুধু ধ্রুবক গুণক log₂ 10 ≈ 3.32.
  • 2ⁿ⁺¹ = O(2ⁿ)? 2²ⁿ = O(2ⁿ)? — প্রথমটি হ্যাঁ (= 2 · 2ⁿ), দ্বিতীয়টি না (ভাগফল 2ⁿ, ধ্রুবক নয়)।
  • n ≤ 2 × 10⁵, সময়সীমা ১ সেকেন্ড — কোন complexity লক্ষ্য করবেন? — O(n log n) বা O(n); O(n²) প্রায় 4 × 10¹⁰ ধাপ, অসম্ভব।

৯ · AI-এর যুগে complexity যাচাই

AI-কে কোড দিয়ে জিজ্ঞেস করলে এক সেকেন্ডে উত্তর আসে: “এর time complexity O(n log n)”. আত্মবিশ্বাসী ভাষা, পরিষ্কার ব্যাখ্যা — কিন্তু প্রায়ই ভুল। কারণ AI অনেক সময় কোডের চেহারা দেখে অনুমান করে (“দুটি লুপ, তাই n²”), ধাপ গোনে না। কনটেস্টে এর দাম একটি “Time Limit Exceeded”, ইন্টারভিউতে একটি ভুল উত্তর।

AI-এর সাধারণ ভুলউদাহরণযেভাবে ধরবেন
লুপ গুনে complexity বলাi *= 2-এর ভেতরে j < i লুপকে “O(n log n)” বলা (আসলে Θ(n))ছোট n-এ হাতে গুনুন
লুকানো খরচ উপেক্ষালুপে list.pop(0), x in list, substr, string জোড়াপ্রতিটি লাইব্রেরি কলের খরচ জেনে নিন
O আর Θ গুলিয়ে ফেলাশুধু worst case বলে best/average চেপে যাওয়া“কোন case?” জিজ্ঞেস করুন
সীমার সাথে না মেলানোn = 10⁵-এ O(n²) সমাধান দিয়ে “চলবে” বলা10⁸/সেকেন্ড নিয়মে হিসাব করুন
off-by-one“লুপ log n বার চলে” — আসলে ⌊log₂ n⌋ + 1n = 1, 2, 20-এ গুনে দেখুন

AI-কে শুধু “complexity কত?” জিজ্ঞেস না করে তাকে দিয়ে গোনার কাজ করান — তাহলে তার যুক্তি যাচাই করা যায়:

নিচের কোডের time complexity বের করো, কিন্তু শুধু উত্তর দিও না। ১. প্রতিটি লুপ বাইরের চলকের প্রতিটি মানে কতবার চলে, সূত্র আকারে লেখো। ২. n = 8 হলে সবচেয়ে ভেতরের লাইন মোট কতবার চলে, একটি ছোট টেবিলে হাতে গুনে দেখাও। ৩. তারপর মোট যোগফল সরল করে Θ আকারে লেখো, আর বলো এটি best, worst না সব ইনপুটের জন্য। ৪. কোনো লাইব্রেরি ফাংশনের (যেমন sort, erase, in) লুকানো খরচ থাকলে আলাদা করে উল্লেখ করো। ৫. n ≤ 200000 আর সময়সীমা ১ সেকেন্ড হলে এটি চলবে কি না, ধাপ গুনে বলো। [এখানে কোড পেস্ট করুন]

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

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

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

প্র ০১গণিতবিদদের একটি পুরোনো চিহ্ন কীভাবে কম্পিউটার বিজ্ঞানের দৈনন্দিন ভাষা হলো — আর Knuth কেন Ω আর Θ-এর সংজ্ঞা নতুন করে প্রস্তাব করেছিলেন?

O চিহ্নের জন্ম সংখ্যাতত্ত্বে। Paul Bachmann ১৮৯৪ সালে মৌলিক সংখ্যা আর গাণিতিক ফাংশনের আসন্ন মান নিয়ে লিখতে গিয়ে “এই পদের মান মোটামুটি এত বড়” বোঝাতে O ব্যবহার করেন। Edmund Landau ১৯০৯ সালে মৌলিক সংখ্যার বণ্টন নিয়ে তাঁর বিখ্যাত বইয়ে এটি ব্যাপকভাবে ব্যবহার করেন এবং ছোট হাতের o যোগ করেন। গণিতবিদদের কাছে এটি ছিল ভুলের মাপ লেখার শর্টহ্যান্ড — যেমন “মোট যোগফল = মূল পদ + O(√n)”.

কম্পিউটার বিজ্ঞান ষাটের দশকে অ্যালগরিদমের চলার সময় নিয়ে ভাবতে শুরু করলে এই চিহ্নটি হাতের কাছে ছিল। সমস্যা হলো, সবাই O দিয়েই সব কথা বলছিলেন — নিচের সীমা বোঝাতেও। আবার Ω চিহ্নটি গণিতে আগে থেকেই ছিল (Hardy আর Littlewood ১৯১৪ সালে ব্যবহার করেন), কিন্তু অন্য অর্থে: “অসীমভাবে বহুবার অন্তত এত বড়”। এই গোলমাল দূর করতে Donald Knuth ১৯৭৬ সালে ACM SIGACT News-এ “Big Omicron and Big Omega and Big Theta” প্রবন্ধে প্রস্তাব করেন: Ω মানে “সব বড় n-এ অন্তত c·g(n)”, আর Θ মানে দুটোই। তাঁর যুক্তি ছিল ব্যবহারিক — অ্যালগরিদম বিশ্লেষণে আমরা প্রায়ই নিচের সীমা প্রমাণ করি (“এই সমস্যার যেকোনো সমাধান অন্তত এত সময় নেবে”), তাই আলাদা, পরিষ্কার চিহ্ন দরকার।

প্রস্তাবটি প্রায় সাথে সাথে গৃহীত হয়, আর পরবর্তীকালে Cormen, Leiserson, Rivest ও Stein-এর পাঠ্যবই (CLRS) এই সংজ্ঞাগুলোকে বিশ্বজুড়ে মানক করে তোলে। তবে কিছু সূক্ষ্মতা রয়ে গেছে: সংখ্যাতত্ত্বের কিছু লেখায় এখনো Hardy–Littlewood অর্থে Ω দেখা যায়। আর “=” চিহ্নের একমুখী ব্যবহার নিয়ে Knuth নিজেও স্বীকার করেছেন যে এটি একরকম “অপব্যবহার”, তবে এতটাই সুবিধাজনক যে বদলানোর চেষ্টা বৃথা।

মূল উপলব্ধি: নিখুঁত চিহ্ন নিখুঁত চিন্তার হাতিয়ার। “এটি O(n²)” আর “এটি Θ(n²)” আলাদা দাবি — প্রথমটি শুধু প্রতিশ্রুতি দেয় যে এর চেয়ে খারাপ হবে না, দ্বিতীয়টি বলে ঠিক এমনই হবে। কোনটি বলছেন, জেনে বলুন।

প্র ০২কম্পিউটার যদি ১০০০ গুণ দ্রুত হয়, তাহলে Θ(n), Θ(n²) আর Θ(2ⁿ) অ্যালগরিদম একই সময়ে কত বড় সমস্যা সমাধান করতে পারবে?

ধরুন এখন এক ঘণ্টায় কোনো অ্যালগরিদম সর্বোচ্চ n₁ আকারের সমস্যা সমাধান করতে পারে। নতুন কম্পিউটার ১০০০ গুণ দ্রুত, অর্থাৎ একই সময়ে ১০০০ গুণ বেশি ধাপ। নতুন সর্বোচ্চ আকার n₂ কত?

  • Θ(n): n₂ = 1000 · n₁. সমস্যা ১০০০ গুণ বড় — পুরো লাভটাই পাওয়া গেল।
  • Θ(n²): n₂² = 1000 · n₁², তাই n₂ = √1000 · n₁ ≈ 31.6 · n₁. লাভ অনেক কম।
  • Θ(n³): n₂ = ∛1000 · n₁ = 10 · n₁.
  • Θ(2ⁿ): 2n₂ = 1000 · 2n₁, তাই n₂ = n₁ + log₂ 1000 ≈ n₁ + 10. ১০০০ গুণ দ্রুত মেশিনে মাত্র ১০টি উপাদান বেশি!

এই হিসাবটিই ব্যাখ্যা করে কেন হার্ডওয়্যারের উন্নতি exponential অ্যালগরিদমকে বাঁচাতে পারে না। কয়েক দশক ধরে কম্পিউটারের গতি নিয়মিত বেড়েছে, কিন্তু সব subset যাচাই করার মতো অ্যালগরিদম আজও n = ৪০–৫০-এর পর অচল। উল্টো দিকে, একটি ভালো অ্যালগরিদম আবিষ্কার — যেমন n² থেকে n log n-এ নামা — n = 10⁶-এ প্রায় ৫০,০০০ গুণ লাভ দেয়, যা হার্ডওয়্যারের বহু প্রজন্মের সমান।

এই উপলব্ধি থেকেই ১৯৬৫ সালে Alan Cobham আর Jack Edmonds আলাদাভাবে প্রস্তাব করেন যে “দক্ষ অ্যালগরিদম” মানে polynomial time — কোনো ধ্রুবক k-এর জন্য O(nk). এটি “Cobham–Edmonds thesis” নামে পরিচিত, আর P বনাম NP প্রশ্নের (পর্ব ৬৪) ভিত্তি এখানেই। অবশ্য n¹⁰⁰ বাস্তবে মোটেও দক্ষ নয় — তবু তত্ত্বে polynomial আর exponential-এর সীমারেখা অত্যন্ত কাজের, কারণ polynomial শ্রেণি যোগ, গুণ আর একটির ভেতরে আরেকটি বসানোয় বন্ধ থাকে।

মূল উপলব্ধি: দ্রুত মেশিন কেনা ধ্রুবকের সাথে লড়াই; ভালো অ্যালগরিদম বাছাই বৃদ্ধির হারের সাথে লড়াই। বড় ইনপুটে দ্বিতীয়টিই সবসময় জেতে।

প্র ০৩Asymptotically “সেরা” অ্যালগরিদম কি সবসময় বাস্তবে সেরা? “Galactic algorithm” কী?

না। Asymptotic বিশ্লেষণ বলে “n যথেষ্ট বড় হলে” কে জিতবে — কিন্তু সেই “যথেষ্ট বড়” কত বড়, সেটি লুকিয়ে থাকে ধ্রুবক আর n₀-এর ভেতরে। কখনো কখনো সেই বিন্দু এত দূরে যে বাস্তবের কোনো ইনপুট সেখানে পৌঁছায় না।

সবচেয়ে বিখ্যাত উদাহরণ matrix গুণ। সাধারণ তিন-লুপ পদ্ধতি Θ(n³)। ১৯৬৯ সালে Volker Strassen দেখান এটি প্রায় O(n2.81)-এ করা যায় — পর্ব ৪৯-এ দেখবেন। এরপর Coppersmith–Winograd (১৯৯০) আর পরবর্তী গবেষণায় ঘাত নেমে এসেছে ২.৩৭-এর সামান্য নিচে। কিন্তু এই পদ্ধতিগুলোর ধ্রুবক এত বিশাল যে কোনো বাস্তব আকারের matrix-এ এগুলো সাধারণ পদ্ধতিকে হারাতে পারে না। এমন অ্যালগরিদমকে মজা করে বলা হয় galactic algorithm — শুধু ছায়াপথের মাপের ইনপুটে লাভজনক। ২০১৯ সালে David Harvey আর Joris van der Hoeven পূর্ণসংখ্যা গুণের O(n log n) অ্যালগরিদম প্রকাশ করেন — তাত্ত্বিকভাবে যুগান্তকারী, কিন্তু বিশাল সংখ্যা ছাড়া ব্যবহারিক নয়।

উল্টো দিকের উদাহরণও আছে। বাস্তবের sort লাইব্রেরিগুলো hybrid: বড় অংশে Θ(n log n) পদ্ধতি, কিন্তু ছোট টুকরোয় Θ(n²) insertion sort, কারণ ছোট n-এ তার ধ্রুবক ছোট। Python-এর Timsort (২০০২ সালে Tim Peters লিখেছেন) ছোট “run”-এ insertion sort ব্যবহার করে। আবার Quick sort-এর worst case Θ(n²), merge sort-এর Θ(n log n) — তবু অনেক লাইব্রেরি quick sort-ভিত্তিক পদ্ধতি বেছে নেয়, কারণ গড়ে তার ধ্রুবক ছোট আর মেমরি-ব্যবহার cache-বান্ধব (পর্ব ২২ ও ২৫)।

মূল উপলব্ধি: Big-O প্রথম ছাঁকনি — ভুল শ্রেণির অ্যালগরিদম বাদ দেয়। শেষ সিদ্ধান্ত নিতে হয় বাস্তব ইনপুটের আকার, ধ্রুবক আর হার্ডওয়্যার মেপে। তত্ত্ব আর পরিমাপ একে অপরের পরিপূরক।

প্র ০৪log-এর ভিত্তি বাদ দেওয়া যায়, কিন্তু exponential-এর ভিত্তি বাদ দেওয়া যায় না — কেন এই অসমতা?

কারণটি লুকিয়ে আছে ভিত্তি বদলের সূত্রে। loga n = logb n / logb a. এখানে logb a একটি সাধারণ ধ্রুবক — n-এর ওপর নির্ভর করে না। তাই log₂ n আর log₁₀ n-এর অনুপাত সবসময় log₂ 10 ≈ 3.32, n যত বড়ই হোক। Θ-এর সংজ্ঞায় ঠিক এমন ধ্রুবক অনুপাতই অনুমোদিত, তাই দুটি একই শ্রেণি, আর আমরা নিশ্চিন্তে শুধু “log n” লিখি।

Exponential-এ পরিস্থিতি আলাদা। 3ⁿ / 2ⁿ = (1.5)ⁿ — এই অনুপাত ধ্রুবক নয়, n-এর সাথে নিজেই exponential হারে বাড়ে। তাই 2ⁿ = o(3ⁿ), দুটি ভিন্ন শ্রেণি। একই কারণে 2n+1 = 2 · 2ⁿ = Θ(2ⁿ) (যোগ করা ঘাত শুধু ধ্রুবক গুণক দেয়), কিন্তু 22n = 4ⁿ ≠ O(2ⁿ) (গুণ করা ঘাত ভিত্তিই বদলে দেয়)।

আরও সূক্ষ্ম একটি ফাঁদ: nlog₂ 3 আর 3log₂ n আসলে একই ফাংশন (দুই দিকে log₂ নিলে দুটোই log₂ 3 · log₂ n). এই ধরনের রূপ recurrence আর Master theorem-এ (পর্ব ০৮–০৯) বারবার আসে — যেমন Karatsuba গুণের সময় Θ(nlog₂ 3) ≈ Θ(n1.585). আবার 2log₂ n = n কিন্তু 2log₄ n = √n: log যখন ঘাতের ভেতরে ঢোকে, তখন তার ভিত্তিও গুরুত্বপূর্ণ হয়ে যায়, কারণ ধ্রুবক গুণকটি তখন ঘাতে বসে, আর ঘাতের ধ্রুবক বদলালে বৃদ্ধির শ্রেণি বদলায়।

একটি বাস্তব ব্যাখ্যাও আছে। log হলো “কতবার ভাগ করলে ১-এ নামে” — ২ দিয়ে ভাগ করুন বা ১০ দিয়ে, ধাপের সংখ্যা শুধু একটি স্থির অনুপাতে আলাদা। কিন্তু exponential হলো “প্রতি ধাপে কত গুণ বাড়ে” — দ্বিগুণ আর তিনগুণের পার্থক্য প্রতি ধাপে জমতে জমতে বিশাল হয়।

মূল উপলব্ধি: কোনো ধ্রুবক যদি গুণক হিসেবে বসে, Θ তাকে উপেক্ষা করে; যদি ঘাতে বা ভিত্তিতে বসে, তাহলে উপেক্ষা করা যায় না। প্রতিটি সরলীকরণের আগে জিজ্ঞেস করুন — ধ্রুবকটি কোথায় বসেছে?

প্র ০৫যেকোনো দুটি ফাংশনের মধ্যে কি সবসময় O বা Ω সম্পর্ক থাকে? সংখ্যার মতো কি সব ফাংশন তুলনা করা যায়?

সংখ্যার ক্ষেত্রে যেকোনো দুটি a, b-এর জন্য a ≤ b বা a ≥ b সত্য। স্বাভাবিকভাবে মনে হয় ফাংশনেও তাই — f = O(g) নয়তো f = Ω(g). কিন্তু এটি মিথ্যা, আর এখানেই O-কে “≤”-এর সাথে তুলনার উপমা ভেঙে পড়ে।

একটি পরিচিত উদাহরণ: f(n) = n আর g(n) = n1 + sin n. sin n-এর মান −1 আর 1-এর মধ্যে দোলে, তাই g কখনো প্রায় n⁰ = 1-এর কাছে নামে, কখনো প্রায় n²-এর কাছে ওঠে — আর এটি বারবার, অসীমবার ঘটে। কোনো ধ্রুবক c দিয়ে n ≤ c · g(n) চিরকাল টেকে না (g যখন প্রায় 1), আবার n ≥ c · g(n)-ও টেকে না (g যখন প্রায় n²)। তাই n ≠ O(g) এবং n ≠ Ω(g) — দুটি ফাংশন তুলনার অযোগ্য। আরও সরল একটি উদাহরণ: g(n) = 1 যখন n বিজোড়, আর n² যখন n জোড় — f(n) = n-এর সাথে এটিও তুলনা করা যায় না।

এখানে limit test-ও কাজ করে না: f/g-এর কোনো limit নেই, ভাগফল অসীমবার শূন্যের কাছে আর অসীমবার অসীমের কাছে যায়। এজন্যই limit test শুধু একটি পর্যাপ্ত শর্ত — limit থাকলে দারুণ, না থাকলে সংজ্ঞায় ফিরে যেতে হয়।

সুখবর হলো, বাস্তব অ্যালগরিদমের চলার সময় সাধারণত na, (log n)b, cn-এর মতো “ভদ্র” ফাংশনের গুণফল-যোগফল, আর এদের মধ্যে তুলনা সবসময় সম্ভব। তবে worst case আর best case-এর ফাংশন যখন খুব আলাদা হয় — যেমন কোনো অ্যালগরিদম জোড় n-এ দ্রুত আর বিজোড় n-এ ধীর — তখন “সব ইনপুটের জন্য Θ” বলা যায় না, আলাদা আলাদা case বিশ্লেষণ লাগে। এটিই পর্ব ০৫-এর বিষয়।

মূল উপলব্ধি: O, Ω, Θ হলো একটি আংশিক ক্রম, পূর্ণ ক্রম নয়। উপমা চিন্তায় সাহায্য করে, কিন্তু প্রমাণ সবসময় সংজ্ঞা থেকেই আসে।

অনুশীলন

খাতা-কলম নিয়ে বসুন। প্রথম তিনটিতে কোড লাগবে না; শেষটিতে নিজে প্রোগ্রাম লিখবেন।

  1. হাতে গুনুন: n = 16 হলে নিচের কোডে cnt-এর শেষ মান কত? সাধারণ n-এ complexity কত?
    for (int i = 1; i <= n; i *= 2)
        for (int j = 0; j < i; j++)
            cnt++;

    i-এর মান 1, 2, 4, 8, 16 (32 হলে 32 ≤ 16 মিথ্যা)। প্রতিটি i-তে ভেতরের লুপ i বার চলে: 1 + 2 + 4 + 8 + 16 = 31. সাধারণভাবে, যদি 2k ≤ n < 2k+1 হয়, মোট = 1 + 2 + … + 2k = 2k+1 − 1 < 2n. আবার মোট ≥ 2k > n/2. তাই Θ(n) — “দুই লুপ, তাই n log n বা n²” ভাবলে ভুল হতো। জ্যামিতিক ধারার যোগফল শেষ পদের দ্বিগুণের কম — এই কৌশল পর্ব ০৬-এ আরও দেখবেন।

  2. প্রমাণ লিখুন: সংজ্ঞা থেকে দেখান 5n³ − 2n² + 7 = Θ(n³). তারপর দেখান n³ ≠ O(n²).

    ওপরের সীমা: n ≥ 1 হলে −2n² ≤ 0 আর 7 ≤ 7n³, তাই f(n) ≤ 5n³ + 7n³ = 12n³. নিচের সীমা: n ≥ 1 হলে n² ≤ n³, তাই 2n² ≤ 2n³, ফলে f(n) ≥ 5n³ − 2n³ + 7 ≥ 3n³. সুতরাং c₁ = 3, c₂ = 12, n₀ = 1 দিয়ে 3n³ ≤ f(n) ≤ 12n³ — অর্থাৎ Θ(n³). (ঋণাত্মক পদ থাকলে নিচের সীমায় সাবধান হতে হয় — এখানে সেটিই আসল কাজ।)

    n³ ≠ O(n²): ধরি c, n₀ আছে যাতে সব n ≥ n₀-এ n³ ≤ c · n². তাহলে n ≤ c. কিন্তু n = max(n₀, c + 1) নিলে n > c — বিরোধ। তাই এমন c নেই।

  3. বৃদ্ধির ক্রমে সাজান (ধীর থেকে দ্রুত; একই শ্রেণির হলে বলুন): n², 2ⁿ, n log n, √n, log n, n!, 100, n1.5, log(n!), 2log₂ n.

    100 ≺ log n ≺ √n ≺ n = 2log₂ n ≺ n log n ≍ log(n!) ≺ n1.5 ≺ n² ≺ 2ⁿ ≺ n!

    কারণ: 100 ধ্রুবক, Θ(1). 2log₂ n হুবহু n. log(n!) = Θ(n log n) (ওপরের “আরও গভীরে” দেখুন)। n log n বনাম n1.5: দুটিকে n দিয়ে ভাগ করলে log n বনাম √n, আর log n = o(√n) — তাই n log n = o(n1.5). বাকিগুলো অংশ ৪-এর শিকল থেকেই আসে।

  4. কোড লিখুন: নিচের “harmonic” লুপের ভেতরের কাজ গুনে cnt / (n · ln n) ছাপান — n = 10³, 10⁴, 10⁵, 10⁶ নিয়ে। ভাগফল কোন সংখ্যার দিকে যায়? এ থেকে complexity কী অনুমান করবেন?
    for (int i = 1; i <= n; i++)
        for (int j = i; j <= n; j += i)
            cnt++;

    একটি সম্ভাব্য সমাধান:

    #include <cmath>
    #include <cstdio>
    int main() {
        for (long long n : {1000LL, 10000LL, 100000LL, 1000000LL}) {
            long long cnt = 0;
            for (long long i = 1; i <= n; i++)
                for (long long j = i; j <= n; j += i) cnt++;
            std::printf("n=%lld cnt=%lld ratio=%.4f\n", n, cnt, cnt / (n * std::log((double)n)));
        }
    }

    গণনাগুলো আসে 7069, 93668, 1166750, 13970034, আর ratio যথাক্রমে প্রায় 1.0233, 1.0170, 1.0134, 1.0112 — ধীরে ধীরে 1-এর দিকে নামছে। কারণ i-তম ধাপে ভেতরের লুপ ⌊n/i⌋ বার চলে, মোট n(1 + 1/2 + … + 1/n) ≈ n ln n. তাই complexity Θ(n log n). দেখতে দুই লুপ, কিন্তু Θ(n²) নয় — sieve of Eratosthenes-এর (পর্ব ৬২) বিশ্লেষণও এই ধাঁচের।

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

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

ক্রমফাংশনগুলোকে ধীর থেকে দ্রুত বৃদ্ধির ক্রমে সাজান (▲▼ চেপে সরান)

  1. log n
  2. √n
  3. n log n
  4. n²
  5. 2ⁿ
  6. n!

f(n) = 3n² + 5n + 2 সম্পর্কে কোন উক্তিটি মিথ্যা?

  • f(n) = O(n³)
  • f(n) = Θ(n²)
  • f(n) = Ω(n)
  • f(n) = O(n)

n ≤ 10⁵, সময়সীমা ১ সেকেন্ড, C++-এ এক সেকেন্ডে প্রায় 10⁸ সরল ধাপ ধরুন। কোন complexity-র সমাধান নিরাপদে চলবে?

  • O(n²)
  • O(n log n)
  • O(n³)
  • O(2ⁿ)

বাইরের লুপ for (i = n; i >= 1; i /= 2), আর তার ভেতরে for (j = 0; j < n; j++) একটি সরল কাজ। মোট time complexity কত?

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

log₂ n আর log₁₀ n-এর সম্পর্ক কোনটি?

  • দুটি একে অপরের Θ — পার্থক্য শুধু একটি ধ্রুবক গুণক
  • log₁₀ n = o(log₂ n)
  • log₂ n দ্রুততর শ্রেণি, তাই binary search-এর বিশ্লেষণে ভিত্তি লিখতেই হবে
  • এদের মধ্যে কোনো O সম্পর্ক নেই

দুটি ধনাত্মক ফাংশনের জন্য limn→∞ f(n)/g(n) = 0 হলে কোনটি সত্য?

  • f = Θ(g)
  • f = O(g), কিন্তু f ≠ Θ(g)
  • f = Ω(g)
  • কিছুই বলা যায় না

নিচের কোন উক্তিটি সত্য?

  • 2ⁿ⁺¹ = O(2ⁿ)
  • 2²ⁿ = O(2ⁿ)
  • n! = O(2ⁿ)
  • 3ⁿ = Θ(2ⁿ)

শব্দকোষ

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

মনে রাখুন

  1. বড় n-এ বৃদ্ধির হারই সিদ্ধান্ত নেয়: 1 ≺ log n ≺ √n ≺ n ≺ n log n ≺ n² ≺ n³ ≺ 2ⁿ ≺ n!.
  2. O ≤, Ω ≥, Θ = — সংজ্ঞায় ধ্রুবক c আর শুরুর বিন্দু n₀; প্রমাণ মানে এদের খুঁজে দেখানো।
  3. o আর ω কঠোর তুলনা; limit test: 0 ⇒ o, ধ্রুবক ⇒ Θ, ∞ ⇒ ω.
  4. নিয়ম: ধ্রুবক ও ছোট পদ বাদ, পরপর অংশে বড়টি, nested লুপে গুণ, অর্ধেক হলে log n, i·i ≤ n হলে √n.
  5. কনটেস্টে ~10⁸ ধাপ/সেকেন্ড; O মানেই worst case নয়; AI-এর complexity হাতে গুনে আর doubling test দিয়ে যাচাই করুন।

পরের পর্বে: একই অ্যালগরিদম কোনো ইনপুটে উড়ে চলে, কোনোটিতে হামাগুড়ি দেয়। Best, worst আর average case আলাদা করে বিশ্লেষণ করা, আর সময়ের পাশাপাশি মেমরির হিসাব — space complexity.

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

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