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

Recurrence সমাধান — substitution আর recursion tree

Solving Recurrences — Unrolling, Recursion Trees & the Substitution Method

Taught in Bangla, this lesson shows how to write a recurrence from recursive code and solve it by unrolling, recursion trees and the substitution method with induction, including the classic constant trap. It explains why floors and base cases can usually be ignored, covers change of variables for √n recurrences and characteristic equations for linear recurrences, and verifies answers numerically in C++ or Python.

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

  • যেকোনো রিকার্সিভ কোড দেখে তার সময়ের recurrence লেখা — base case সহ
  • Unrolling আর recursion tree দিয়ে recurrence-এর উত্তর বের করা
  • Substitution পদ্ধতিতে induction দিয়ে উত্তর প্রমাণ করা, আর এর বিখ্যাত ফাঁদ এড়ানো
  • √n-ওয়ালা recurrence-এ চলক বদল, আর Fibonacci-র মতো linear recurrence-এ characteristic equation
  • C++ বা Python-এ সংখ্যা দিয়ে নিজের উত্তর যাচাই করা — AI-এর দেওয়া উত্তরও

১ · Recurrence কী — নিজের ছোট রূপ দিয়ে লেখা হিসাব

পর্ব ০৭-এ দেখেছেন, recursion-এ একটি ফাংশন নিজেকেই আবার ডাকে — ছোট ইনপুটে। এবার প্রশ্ন হলো, এমন ফাংশন চলতে কত সময় নেয়? সাধারণ loop-এর বেলায় আমরা গুনতাম: loop কতবার ঘোরে, প্রতিবারে কত কাজ। কিন্তু রিকার্সিভ ফাংশনের ভেতরে তো সে নিজেই আছে! তার সময় জানতে হলে আগে নিজের সময়টাই জানতে হয়।

একটি বোর্ড পরীক্ষার কথা ভাবুন। প্রধান পরীক্ষকের কাছে এলো nটি খাতা। তিনি নিজে সব খাতা দেখেন না — খাতাগুলো দুই ভাগ করে দুজন সহকারীকে দেন। সহকারীরাও একই নিয়মে নিজেদের ভাগ আবার দুই ভাগ করে পরের জনদের দেন। শেষে প্রত্যেকে ফেরত পাওয়া নম্বরগুলো মিলিয়ে একটি তালিকা বানান, যাতে লাগে খাতার সংখ্যার সমান সময়। তাহলে প্রধান পরীক্ষকের মোট সময় = দুজন সহকারীর সময় (প্রত্যেকে n/2 খাতা) + তালিকা মেলানোর সময় (n). অর্থাৎ T(n) = 2T(n/2) + n. এখানে T নিজেকে দিয়েই সংজ্ঞায়িত — এটাই recurrence.

মূল ধারণা

Recurrence (বা recurrence relation) হলো এমন একটি সমীকরণ, যেখানে T(n)-কে প্রকাশ করা হয় তারই ছোট ইনপুটের মান — যেমন T(n/2) বা T(n−1) — দিয়ে। সঙ্গে থাকে একটি base case: সবচেয়ে ছোট ইনপুটে খরচ কত, যেমন T(1) = 1. Recurrence সমাধান করা মানে এমন একটি সরাসরি সূত্র বা অন্তত একটি Θ-সীমা বের করা, যার ডান দিকে আর T নেই — যেমন T(n) = Θ(n log n).

কেন এটা এত গুরুত্বপূর্ণ? কারণ কোর্সের সামনের বেশিরভাগ ভালো অ্যালগরিদম — binary search, merge sort, quick sort, heap, segment tree, DFS, Karatsuba, Strassen, dynamic programming — সবই রিকার্সিভ। তাদের সময় বোঝার প্রধান পথ recurrence. এই পর্বে আমরা তিনটি হাতিয়ার শিখব:

Unrolling

সমীকরণকে বারবার নিজের ভেতরে বসিয়ে খুলে লেখা, যতক্ষণ না প্যাটার্ন দেখা যায়।

সরল, এক শাখার recurrence

Recursion tree

প্রতিটি ডাক একটি নোড; স্তর ধরে খরচ যোগ করে উত্তর অনুমান করা।

ভাগ করে জয় করা

Substitution

অনুমান করা উত্তরকে induction দিয়ে কঠোরভাবে প্রমাণ করা।

পরীক্ষার খাতার প্রমাণ

সংখ্যা দিয়ে যাচাই

প্রোগ্রামে T(n) হিসাব করে অনুমানের সাথে অনুপাত দেখা।

AI-এর উত্তর মিলিয়ে নিন

চতুর্থ একটি হাতিয়ার — master theorem — আসবে পর্ব ০৯-এ। সেটি অনেক recurrence এক লাইনে সমাধান করে, কিন্তু কেন কাজ করে তা বুঝতে হলে এই পর্বের recursion tree লাগবেই।

২ · কোড দেখে recurrence লেখা

নিয়মটি সহজ। একটি রিকার্সিভ ফাংশনের সময় T(n) = (প্রতিটি রিকার্সিভ ডাকের সময় যোগ) + (ডাকের বাইরে ফাংশনটি নিজে যা কাজ করে)। তিনটি প্রশ্ন করুন:

  1. কতবার নিজেকে ডাকে? — এটি সামনের গুণক (যেমন 2T-এর 2).
  2. প্রতিটি ডাকে ইনপুট কত বড়? — n/2, n/3, n−1, n−2…
  3. ডাকের বাইরে কত কাজ? — ভাগ করা, জোড়া দেওয়া, loop — এটি শেষের পদ।

নিচের ডায়াগ্রামে merge sort-এর pseudocode লাইন ধরে ধরে পড়া হচ্ছে, আর ডান দিকে recurrence তৈরি হচ্ছে। “পরে” চাপুন:

1mergeSort(A, lo, hi):   // n = hi - lo2  if n <= 1: return3  mid = (lo + hi) / 24  mergeSort(A, lo, mid)5  mergeSort(A, mid, hi)6  merge(A, lo, mid, hi)
হিসাবের খাতা
এই লাইনের খরচ
recurrence
  1. নাম দিন। n = hi − lo টি উপাদান সাজাতে mergeSort যত ধাপ নেয়, তার নাম দিলাম T(n). এখনো জানি না এটি কত — সেটাই খুঁজছি।
  2. Base case. n ≤ 1 হলে কিছুই সাজাতে হয় না, সরাসরি ফেরত — খরচ ধ্রুবক। তাই T(1) = Θ(1). এটিই recurrence-এর থামার জায়গা।
  3. মাঝখান বের করা। একটি যোগ আর একটি ভাগ — ধ্রুবক সময়, Θ(1).
  4. প্রথম রিকার্সিভ ডাক। বাঁ অর্ধেকে আছে ⌊n/2⌋টি উপাদান। এর সময় কত, আমরা জানি না — কিন্তু নামটা জানি: T(⌊n/2⌋). এখানেই T নিজের ভেতরে ঢুকল।
  5. দ্বিতীয় রিকার্সিভ ডাক। ডান অর্ধেকে ⌈n/2⌉টি উপাদান, সময় T(⌈n/2⌉). n বিজোড় হলে দুই ভাগ এক উপাদান ছোট-বড় হয় — তাই floor (⌊⌋) আর ceiling (⌈⌉)।
  6. জোড়া দেওয়া। দুটি সাজানো অর্ধেক merge করতে প্রতিটি উপাদান একবার করে দেখা হয় — Θ(n). সব যোগ করে, floor-ceiling আর ছোট ধ্রুবক বাদ দিয়ে পাই পরিচিত রূপ: T(n) = 2T(n/2) + Θ(n).

এবার একই নিয়মে আরও চারটি বিখ্যাত রিকার্সিভ ফাংশন দেখুন (নিচের সারণিতে merge sort-সহ পাঁচটি)। কোডটি চালানোর জন্য নয় — শুধু গঠন দেখার জন্য (C++-ধাঁচের pseudocode, ০-ভিত্তিক index):

// Pseudocode
search(A, lo, hi, x):            // binary search, n = hi - lo
    if n == 0: return -1
    mid = (lo + hi) / 2
    if A[mid] == x: return mid
    if A[mid] < x: return search(A, mid + 1, hi, x)   // ONE call, half size
    return search(A, lo, mid, x)

fib(n):                          // naive Fibonacci
    if n < 2: return n
    return fib(n - 1) + fib(n - 2)   // TWO calls, sizes n-1 and n-2

hanoi(n, from, to, via):         // Tower of Hanoi
    if n == 0: return
    hanoi(n - 1, from, via, to)      // move n-1 disks out of the way
    move disk n from -> to           // 1 move
    hanoi(n - 1, via, to, from)      // put them back on top

sortRec(A, n):                   // recursive selection sort
    if n <= 1: return
    swap the largest of A[0..n-1] into A[n-1]   // n steps
    sortRec(A, n - 1)
ফাংশনডাক × আকারবাইরের কাজRecurrenceউত্তর
Binary search১টি × n/2Θ(1)T(n) = T(n/2) + 1Θ(log n)
Merge sort২টি × n/2Θ(n) (merge)T(n) = 2T(n/2) + nΘ(n log n)
সরল Fibonaccin−1 আর n−2Θ(1)T(n) = T(n−1) + T(n−2) + 1Θ(φⁿ) ≈ Θ(1.618ⁿ)
Tower of Hanoi২টি × n−1১টি চালT(n) = 2T(n−1) + 12ⁿ − 1
রিকার্সিভ selection sort১টি × n−1Θ(n) (সবচেয়ে বড়টি খোঁজা)T(n) = T(n−1) + nn(n+1)/2 = Θ(n²)

খেয়াল করুন, দেখতে প্রায় একই রকম দুটি recurrence-এর উত্তর কত আলাদা হতে পারে। T(n/2) + 1 হলো Θ(log n) — দশ লাখ ইনপুটে মাত্র ২০-২১ ধাপ। কিন্তু 2T(n−1) + 1 হলো Θ(2ⁿ) — n = 64 হলেই সংখ্যাটা কল্পনার বাইরে। পার্থক্য শুধু “অর্ধেক” বনাম “এক কম”, আর “একটি ডাক” বনাম “দুটি ডাক”। এজন্যই recurrence অনুমানে নয়, হিসাব করে সমাধান করতে হয়।

দুটি ডাক মানেই 2T(…) লিখবেন না — আকারও দেখুন। Fibonacci-র দুটি ডাকের আকার n−1 আর n−2, সমান নয়; তাই 2T(n−1) লিখলে শুধু একটি ঊর্ধ্বসীমা (2ⁿ) পাবেন, সঠিক উত্তর (φⁿ) নয়। আবার binary search-এ কোডে দুটি return search(…) লেখা থাকলেও প্রতিবার চলে একটিই — তাই গুণক ১।

৩ · Base case আর floor-ceiling — কেন সাধারণত বাদ দেওয়া যায়

বইয়ে প্রায়ই লেখা থাকে শুধু T(n) = 2T(n/2) + n — base case নেই, floor-ceiling নেই। এটা কি ফাঁকি? না — দুটি সুনির্দিষ্ট কারণে এগুলো সাধারণত Θ-উত্তর বদলায় না।

Base case শুধু ধ্রুবক বদলায়

রিকার্সিভ অ্যালগরিদমে ছোট ইনপুটে (ধরুন n ≤ ১০) কাজ সবসময় কোনো ধ্রুবক সময়ে শেষ হয়। তাই আমরা ধরে নিই “যথেষ্ট ছোট n-এ T(n) = Θ(1)”. ধ্রুবকটি ১ হোক বা ১০০, উত্তরে শুধু একটি গুণক বা নিম্নমানের পদ বদলায়। যেমন T(n) = 2T(n/2) + n, T(1) = c হলে (n দুইয়ের ঘাত ধরে) ঠিক উত্তর n·lg n + c·n — c যা-ই হোক, এটি Θ(n log n). এখানে lg মানে log2.

Floor-ceiling: দুইয়ের ঘাতের মাঝে চাপা দেওয়া

প্রথমে উদাহরণ। T(n) = T(⌊n/2⌋) + 1, T(1) = 1. হাতে হিসাব করুন: T(1) = 1, T(2) = 2, T(3) = T(1) + 1 = 2, T(4) = 3, T(7) = T(3) + 1 = 3, T(8) = 4. নিয়মটি দাঁড়ায় T(n) = ⌊lg n⌋ + 1 — floor থাকা সত্ত্বেও উত্তর Θ(log n), ঠিক যেমন n দুইয়ের ঘাত ধরে পেতাম।

সাধারণ যুক্তিটি “চাপা দেওয়া” (sandwich). ধরুন T একটি বাড়তে থাকা ফাংশন (বড় ইনপুটে সময় কম লাগে না — প্রায় সব অ্যালগরিদমেই সত্য)। যেকোনো n-এর জন্য এমন k আছে যে 2k ≤ n < 2k+1. তাহলে T(2^k) ≤ T(n) ≤ T(2^(k+1)). Merge sort-এ T(2k) = Θ(2k·k), আর 2k+1 বড়জোর n-এর দ্বিগুণ। ইনপুট দ্বিগুণ করলে n log n বড়জোর দ্বিগুণের সামান্য বেশি হয় — ধ্রুবক গুণক মাত্র। তাই দুই পাশই Θ(n log n), মাঝখানের T(n)-ও তাই।

এই যুক্তি তখনই খাটে, যখন উত্তরটি “ভদ্র” (polynomial-এর মতো) — ইনপুট দ্বিগুণ হলে উত্তর একটি ধ্রুবক গুণ বাড়ে। 2ⁿ-এর মতো exponential উত্তরে n দ্বিগুণ হলে উত্তর বর্গ হয়ে যায় — সেখানে এভাবে floor বাদ দেওয়া চলে না। তবে exponential উত্তর সাধারণত আসে T(n−1) ধরনের recurrence থেকে, যেখানে floor থাকেই না। তাই এই পর্বের বাকি অংশে আমরা নিশ্চিন্তে n-কে দুই (বা চার, তিন) এর ঘাত ধরব।

৪ · Unrolling — খুলে খুলে লেখার পদ্ধতি

সবচেয়ে সরাসরি পদ্ধতির নাম iteration বা unrolling method. পেঁয়াজের খোসা ছাড়ানোর মতো: T(n)-এর ভেতরে T(n−1)-এর সংজ্ঞা বসান, তার ভেতরে T(n−2)-এর… কয়েক ধাপ পর একটি প্যাটার্ন চোখে পড়বে। তখন “i ধাপ পরে” সাধারণ রূপ লিখে i-কে এমন বসান যাতে base case-এ পৌঁছান।

Tower of Hanoi দিয়ে দেখি: T(n) = 2T(n−1) + 1, T(1) = 1 (একটি চাকতি এক চালেই সরে)।

1T(n) = 2T(n-1) + 12     = 4T(n-2) + 2 + 13     = 8T(n-3) + 4 + 2 + 14     = 2^i·T(n-i) + (2^i - 1)5i=n-1: 2^(n-1)·T(1) + 2^(n-1) - 16     = 2^n - 1  =  Θ(2^n)
কতবার খুললাম
i
সামনের গুণক
জমা যোগফল
  1. শুরুর সমীকরণ। n চাকতি সরাতে: ওপরের n−1টি সরিয়ে রাখা (T(n−1)), সবচেয়ে বড়টি এক চালে (১), তারপর n−1টি আবার তার ওপরে (T(n−1)). এটিই একবার খোলা অবস্থা: i = 1, গুণক 2, জমা ১।
  2. একবার বসান। T(n−1) = 2T(n−2) + 1. এটি বসালে 2[2T(n−2) + 1] + 1 = 4T(n−2) + 2 + 1. গুণক হলো 4, জমা যোগফল ৩।
  3. আবার বসান। T(n−2) = 2T(n−3) + 1 বসিয়ে পাই 8T(n−3) + 4 + 2 + 1. প্যাটার্নটা দেখছেন? গুণক দুইয়ের ঘাত, জমা অংশ 1 + 2 + 4 + … — একটি geometric series.
  4. সাধারণ রূপ। i বার খোলার পর: 2^i·T(n−i) + (1 + 2 + … + 2^(i−1)). ধারাটির যোগফল 2^i − 1 (পর্ব ০৬-এর সূত্র)। এই দাবিটি চাইলে i-এর ওপর induction দিয়ে প্রমাণ করা যায়।
  5. Base case-এ পৌঁছান। T(n−i) যখন T(1), অর্থাৎ i = n − 1. বসালে 2^(n−1)·T(1) + 2^(n−1) − 1, আর T(1) = 1.
  6. উত্তর। 2^(n−1) + 2^(n−1) − 1 = 2ⁿ − 1. যাচাই: n = 3 হলে 7টি চাল — হাতে চেষ্টা করলে সত্যিই ৭টি লাগে। সময় Θ(2ⁿ): প্রতিটি নতুন চাকতি কাজ দ্বিগুণ করে।

একই পদ্ধতিতে বাকি দুটি এক-শাখার recurrence মিনিটেই শেষ হয়:

  • Binary search: T(n) = T(n/2) + 1 = T(n/4) + 2 = T(n/8) + 3 = … = T(n/2i) + i. n/2i = 1 হলে i = lg n, তাই T(n) = T(1) + lg n = lg n + 1 = Θ(log n).
  • রিকার্সিভ selection sort: T(n) = T(n−1) + n = T(n−2) + (n−1) + n = … = T(1) + 2 + 3 + … + n. T(1) = 1 ধরলে যোগফল n(n+1)/2 = Θ(n²). এটা সেই পরিচিত “১ থেকে n-এর যোগফল” — insertion sort-এর worst case-ও ঠিক এই আকারের।

Unrolling-এর দুর্বলতা হলো, দুটি ভিন্ন আকারের শাখা থাকলে (যেমন T(n/3) + T(2n/3)) খোলা রূপ দ্রুত জট পাকিয়ে যায় — প্রতিবার পদ দ্বিগুণ হয়। তখন সেই জটকে ছবিতে সাজাতে হয়। সেই ছবির নামই recursion tree.

৫ · Recursion tree — স্তর ধরে খরচ যোগ

Recursion tree-এ প্রতিটি রিকার্সিভ ডাক একটি নোড। নোডের ভেতরে লেখা হয় সেই ডাকের নিজস্ব কাজ — অর্থাৎ recurrence-এর শেষ পদ, যেটি রিকার্সিভ ডাকের বাইরে। তারপর তিনটি প্রশ্ন:

  1. প্রতি স্তরের মোট খরচ কত? (নোডের সংখ্যা × প্রতিটির কাজ)
  2. স্তর কয়টি? (আকার কতবার ভাগ হলে ১-এ নামে)
  3. সব যোগ করলে কত? — শেষ স্তরের leaf-গুলোসহ।

একটি বড় garments কোম্পানির বেতন হিসাবের কথা ভাবুন। MD একটি হিসাব চাইলেন; দুজন GM-কে অর্ধেক অর্ধেক দায়িত্ব দিলেন; প্রত্যেক GM আবার দুজন ম্যানেজারকে… প্রতিটি পদে কিছু কাজ হয় (কাগজ মেলানো)। পুরো কোম্পানির মোট শ্রমঘণ্টা জানতে স্তর ধরে যোগ করাই সবচেয়ে সহজ: “সব GM মিলে কত ঘণ্টা”, “সব ম্যানেজার মিলে কত” — তারপর সব স্তর যোগ। Recursion tree ঠিক এই সাংগঠনিক চার্ট।

উদাহরণ ১: T(n) = 2T(n/2) + n — প্রতিটি স্তর সমান

0          n1     n/2     n/22  n/4  n/4  n/4  n/4⋮  . . . . . . . . .k  1 1 1 1 ... 1 1 1
স্তরের হিসাব
স্তর
নোড
প্রতিটির আকার
স্তরের খরচ
মোট (এ পর্যন্ত)
  1. মূল (root). প্রথম ডাক নিজে merge করে n কাজ। স্তর ০-এর খরচ n.
  2. স্তর ১। দুটি ডাক, প্রতিটি n/2 আকারের, প্রতিটির নিজস্ব কাজ n/2. স্তরের মোট: 2·(n/2) = n. মোট হলো 2n.
  3. স্তর ২। চারটি ডাক, প্রতিটি n/4 — মোট আবার n! নোড দ্বিগুণ হচ্ছে, প্রতিটির কাজ অর্ধেক হচ্ছে — কাটাকাটি।
  4. যেকোনো স্তর i. 2^i টি নোড, প্রতিটির আকার n/2^i, স্তরের খরচ ঠিক n. এই “প্রতি স্তর সমান” বৈশিষ্ট্যটাই merge sort-এর চাবিকাঠি।
  5. শেষ স্তর (leaf). আকার ১ হয় যখন n/2^k = 1, অর্থাৎ k = lg n. সেখানে n টি leaf, প্রতিটিতে T(1) = Θ(1), মোট Θ(n). স্তর ০ থেকে lg n — মোট lg n + 1টি স্তর।
  6. যোগফল। (lg n + 1)টি স্তর × প্রতিটিতে n = n·lg n + n = Θ(n log n). উদাহরণ: n = 16 হলে ৫টি স্তর × 16 = 80, যা T(1) = 1 ধরে হিসাব করা T(16)-এর সাথে হুবহু মেলে।

উদাহরণ ২: T(n) = T(n/3) + T(2n/3) + n — অসমান ভাগ

Quick sort যদি প্রতিবার ইনপুটকে ১ : ২ অনুপাতে ভাগ করে, তার সময় দাঁড়ায় এই recurrence-এ (পর্ব ২২-এ বিস্তারিত)। গাছটি এবার বাঁকা — বাঁ দিকের শাখা দ্রুত শেষ হয়, ডান দিকেরটা দেরিতে। Master theorem এটি সরাসরি সামলাতে পারে না, কিন্তু recursion tree পারে:

0             n1      n/3          2n/32  n/9  2n/9    2n/9  4n/9L  বাঁ পথ: log₃ n স্তরে শেষR  ডান পথ: log₃/₂ n স্তরে শেষ
স্তরের হিসাব
স্তরের খরচ
যা জানলাম
  1. মূল। নিজস্ব কাজ n. এখনো কিছু বলা যায় না।
  2. স্তর ১। দুটি টুকরো, আকার n/3 আর 2n/3. তাদের নিজস্ব কাজের যোগফল n/3 + 2n/3 = n. ভাগ অসমান, কিন্তু টুকরোগুলোর আকারের যোগফল সেই n-ই।
  3. স্তর ২। চারটি টুকরো: n/9, 2n/9, 2n/9, 4n/9 — যোগফল আবার n. যতক্ষণ কোনো শাখা শেষ হয়নি, প্রতিটি স্তর পূর্ণ এবং খরচ ঠিক n.
  4. নিচের সীমা। সবচেয়ে ছোট পথ সবসময় বাঁয়ে: n → n/3 → n/9 → …, যা ১-এ নামে log₃ n স্তরে। ততক্ষণ পর্যন্ত সব স্তর পূর্ণ, প্রতিটিতে n. তাই T(n) ≥ n·log₃ n = Ω(n log n).
  5. ওপরের সীমা। সবচেয়ে লম্বা পথ ডানে: n → 2n/3 → 4n/9 → …, যা শেষ হয় log₃/₂ n স্তরে। নিচের দিকে কিছু শাখা আগেই শেষ হয়ে যায়, তাই সেসব স্তরের খরচ n-এর কম। ফলে T(n) ≤ n·(log₃/₂ n + 1) = O(n log n). দুই সীমা মিলে Θ(n log n) — ১ : ২ ভাগেও quick sort “ভালো” থাকে।

একটি সূক্ষ্ম বিষয়: ওপরের সীমায় leaf-গুলোর খরচ কি আলাদা করে যোগ করতে হলো না? না, কারণ প্রতিটি স্তরে টুকরোগুলোর আকারের যোগফল বড়জোর n, আর প্রতিটি টুকরো অন্তত ১ আকারের — তাই এক স্তরে বড়জোর n টি নোড, leaf-সহ। এই খুঁটিনাটি পরীক্ষার খাতায় এক লাইনে লিখে দিলে প্রমাণ সম্পূর্ণ হয়।

উদাহরণ ৩: T(n) = 3T(n/4) + n² — ওপরের স্তর ভারী

0            n²1   (n/4)²  (n/4)²  (n/4)²2   9 টি নোড, প্রতিটি (n/16)²i   3^i টি নোড, প্রতিটি (n/4^i)²k   3^k = n^0.79 টি leaf
স্তরের হিসাব
স্তর
স্তরের খরচ
মোট
  1. মূল। নিজস্ব কাজ n². একা এই নোডটিই বলে দেয় T(n) ≥ n², অর্থাৎ Ω(n²).
  2. স্তর ১। তিনটি টুকরো, প্রতিটি n/4 আকারের; প্রতিটির কাজ (n/4)² = n²/16. স্তরের মোট 3n²/16 — মূলের চেয়ে অনেক কম।
  3. স্তর ২। ৯টি নোড, প্রতিটি (n/16)² = n²/256; মোট 9n²/256 = (3/16)²·n². প্রতি স্তরে খরচ 3/16 গুণে কমছে।
  4. যেকোনো স্তর i. খরচ (3/16)^i·n² — একটি কমতে থাকা geometric series. এমন ধারায় প্রথম পদটিই সবচেয়ে বড় এবং মোট যোগফলের বেশিরভাগ সেখান থেকেই আসে।
  5. Leaf. আকার ১ হয় k = log₄ n স্তরে। leaf-এর সংখ্যা 3^k = 3^(log₄ n) = n^(log₄ 3) ≈ n^0.79 (কারণ a^(log_b n) = n^(log_b a)). প্রতিটি leaf-এ Θ(1), মোট Θ(n^0.79) — n²-এর তুলনায় নগণ্য।
  6. যোগফল। অসীম ধারা ধরে ওপরের সীমা: n²·(1 + 3/16 + (3/16)² + …) = n²·1/(1 − 3/16) = (16/13)·n². সঙ্গে leaf-এর Θ(n^0.79). নিচের সীমা মূল থেকেই n². তাই T(n) = Θ(n²) — আর ১৬/১৩ ≈ ১.২৩০৮ ধ্রুবকটি পরের অংশের প্রোগ্রামেও দেখা যাবে।
তিন ধরনের গাছ

স্তরের খরচ সমান থাকলে (2T(n/2) + n): উত্তর = এক স্তরের খরচ × স্তরের সংখ্যা → Θ(n log n). খরচ ওপর থেকে নিচে কমতে থাকলে (3T(n/4) + n²): মূলই জেতে → Θ(n²). খরচ বাড়তে থাকলে (যেমন 4T(n/2) + n): leaf-গুলো জেতে → Θ(leaf-এর সংখ্যা) = Θ(n^(log₂ 4)) = Θ(n²). পর্ব ০৯-এর master theorem হুবহু এই তিনটি ঘটনার নাম দেয়।

নিচের টুলে নিজেই a, b, c বদলে T(n) = aT(n/b) + n^c-এর প্রতিটি স্তরের খরচ দেখুন। কোন স্তর ভারী, তা সংখ্যাগুলোই বলে দেবে:

// a = how many subproblems, b = size shrinks by b, c = work per node is (size)^c
function levelTable(a, b, c, n) {
  console.log(`T(n) = ${a}T(n/${b}) + n^${c},  n = ${n}`);
  let sum = 0, size = n, nodes = 1, d = 0;
  while (size >= 1) {
    const cost = nodes * size ** c;            // total work on this level
    sum += cost;
    console.log(`  level ${d}: ${nodes} nodes x size ${size} -> cost ${cost}`);
    nodes *= a; size /= b; d++;
  }
  console.log(`  total = ${sum}  (total / n^${c} = ${(sum / n ** c).toFixed(3)})`);
}
levelTable(2, 2, 1, 16);    // merge sort: every level equal
levelTable(3, 4, 2, 256);   // root-heavy
levelTable(4, 2, 1, 16);    // leaf-heavy

প্রথম গাছে পাঁচটি স্তরের প্রতিটিতে cost 16, মোট total = 80, অনুপাত 5.000 — অর্থাৎ lg 16 + 1 = 5টি সমান স্তর। দ্বিতীয়টিতে খরচ 65536, 12288, 2304, 432, 81 — প্রতিবার প্রায় ৩/১৬ গুণ, মোট 80641, অনুপাত 1.230 (প্রায় ১৬/১৩)। তৃতীয়টিতে 16, 32, 64, 128, 256 — দ্বিগুণ হচ্ছে, শেষ স্তরই সবচেয়ে ভারী, মোট 496, অনুপাত 31.000 = 2n − 1, অর্থাৎ n-এর তুলনায় বাড়তেই থাকে (উত্তর আসলে Θ(n²))। levelTable(3, 2, 1, 64) বা levelTable(8, 2, 2, 16) চালিয়ে দেখুন — কোনটি কোন ধরনের গাছ?

৬ · Substitution — অনুমান করুন, induction দিয়ে প্রমাণ করুন

Recursion tree চমৎকার অনুমান দেয়, কিন্তু “…” আর “ধারা চলছে” লেখা ছবি সবসময় কঠোর প্রমাণ নয়। কঠোর প্রমাণের মানদণ্ড হলো substitution method: দুই ধাপ —

  1. উত্তরের রূপ অনুমান করুন (যেমন T(n) ≤ c·n lg n), যেখানে c একটি এখনো-অজানা ধ্রুবক।
  2. Mathematical induction দিয়ে প্রমাণ করুন: ধরে নিন n-এর চেয়ে ছোট সব k-এর জন্য অনুমানটি সত্য (এটি inductive hypothesis), তারপর recurrence-এ বসিয়ে দেখান n-এর জন্যও সত্য — একই c দিয়ে। শেষে base case মিলিয়ে নিন।

পূর্ণ উদাহরণ: merge sort-এর O(n log n)

Recurrence: T(n) = 2T(⌊n/2⌋) + n, T(1) = 1. দাবি: n ≥ 2 হলে T(n) ≤ c·n lg n, কোনো ধ্রুবক c-এর জন্য।

  • Inductive step: ধরে নিই ⌊n/2⌋-এর জন্য দাবিটি সত্য। তাহলে T(n) ≤ 2·c⌊n/2⌋·lg⌊n/2⌋ + n ≤ 2·c(n/2)·lg(n/2) + n = cn(lg n − 1) + n = cn lg n − cn + n. এটি ≤ cn lg n হবে, যদি −cn + n ≤ 0, অর্থাৎ c ≥ 1.
  • Base case: n = 1-এ সমস্যা — c·1·lg 1 = 0, অথচ T(1) = 1. সমাধান: base হিসেবে n = 2 আর n = 3 নিন (n ≥ 4 হলে ⌊n/2⌋ ≥ 2, তাই induction কখনো n = 1-এ নামে না)। T(2) = 2·1 + 2 = 4 ≤ c·2·1, আর T(3) = 2·1 + 3 = 5 ≤ c·3·lg 3 ≈ 4.75c. দুটোই c ≥ 2 হলে খাটে।
  • উপসংহার: c = 2 নিলে সব n ≥ 2-এ T(n) ≤ 2n lg n, অর্থাৎ T(n) = O(n log n). নিচের সীমাও (Ω) একইভাবে প্রমাণ করা যায় — দুটো মিলে Θ.

বিখ্যাত ভুল প্রমাণ: “merge sort আসলে O(n)! ধরি T(k) ≤ ck. তাহলে T(n) ≤ 2c(n/2) + n = cn + n = O(n). প্রমাণ শেষ।” — ভুলটা কোথায়? Induction-এ আমরা দাবি করেছিলাম ঠিক T(n) ≤ cn, একই c দিয়ে। কিন্তু পেলাম cn + n, যা cn-এর চেয়ে বড়। “cn + n তো O(n)” বলা মানে প্রতি ধাপে ধ্রুবকটা একটু করে বাড়ানো — (c+1), (c+2), … — lg n ধাপ পরে সেটা আর ধ্রুবক থাকে না। O-চিহ্ন induction-এর ভেতরে কখনো ব্যবহার করবেন না — সবসময় স্পষ্ট ধ্রুবক লিখুন।

যখন অনুমান ঠিক, কিন্তু প্রমাণ আটকে যায়

এবার উল্টো সমস্যা। T(n) = 2T(n/2) + 1, T(1) = 1 — একটি গাছ, যার প্রতিটি নোডে ধ্রুবক কাজ (যেমন রিকার্শন দিয়ে array-র সর্বোচ্চ মান খোঁজা)। নোড আছে প্রায় 2n টি, তাই উত্তর O(n) হওয়ারই কথা। কিন্তু সরাসরি প্রমাণ করতে গেলে কী হয়, দেখুন:

1ধরি: T(k) ≤ c·k  (সব k < n)2T(n) ≤ 2·c(n/2) + 1 = cn + 13cn + 1 ≤ cn ?   কখনোই না  ✗4নতুন: T(k) ≤ c·k − d5T(n) ≤ 2(c·n/2 − d) + 1
          = cn − 2d + 1 ≤ cn − d  ✓6base: T(1) = 1 ≤ c − d  → c=2, d=1 ✓
প্রমাণের অবস্থা
অনুমান
অবস্থা
  1. স্বাভাবিক অনুমান। উত্তর O(n) মনে হচ্ছে, তাই inductive hypothesis: n-এর চেয়ে ছোট সব k-এর জন্য T(k) ≤ ck.
  2. বসাই। T(n) = 2T(n/2) + 1 ≤ 2·c(n/2) + 1 = cn + 1.
  3. আটকে গেলাম। আমাদের দরকার ঠিক T(n) ≤ cn, কিন্তু পেলাম cn + 1 — মাত্র ১ বেশি, তবু induction ভেঙে গেল। c যত বড়ই নিই, “+১” থেকেই যায়। মনে রাখুন: এখানে অনুমানটা ভুল নয়, প্রমাণের রূপটা দুর্বল।
  4. অনুমান শক্ত করুন। উল্টো শোনালেও কৌশলটি হলো আরও কঠিন দাবি প্রমাণ করা: T(k) ≤ ck − d, যেখানে d ≥ 0 একটি নতুন ধ্রুবক — অর্থাৎ একটি নিম্নমানের পদ বিয়োগ করা। শক্ত দাবি মানে hypothesis-ও শক্ত, আর সেটাই সাহায্য করবে।
  5. আবার বসাই। T(n) ≤ 2(c·n/2 − d) + 1 = cn − 2d + 1. এটি ≤ cn − d হবে যদি −2d + 1 ≤ −d, অর্থাৎ d ≥ 1. দুটি ডাক থেকে দুটি “−d” পেলাম, যার একটি “+১”-কে খেয়ে ফেলল!
  6. Base case. T(1) = 1 ≤ c·1 − d. d = 1 নিলে c ≥ 2. তাই T(n) ≤ 2n − 1 = O(n). মজার ব্যাপার: n দুইয়ের ঘাত হলে T(n) ঠিক 2n − 1 — শক্ত অনুমানটি হুবহু সঠিক উত্তর!

অনুমান কোথা থেকে আসে? তিনটি উৎস: (১) recursion tree, (২) চেনা recurrence-এর সাথে মিল (যেমন T(n) = 2T(n/2 + 17) + n দেখতে merge sort-এর মতো, উত্তরও Θ(n log n)), (৩) ছোট প্রোগ্রামে সংখ্যা হিসাব করে প্রবণতা দেখা — পরের অংশে ঠিক এটাই করব। আর অনুমান খুব আলগা হলে (যেমন O(n²) অনুমান করলেন যেখানে আসল উত্তর n log n) প্রমাণ সফল হবে ঠিকই, কিন্তু সেটি দুর্বল সীমা। তাই সাধারণত ওপর আর নিচ দুই দিক থেকে চেপে আসল Θ খুঁজে বের করতে হয়।

৭ · সংখ্যা দিয়ে যাচাই — অনুপাত কি স্থির হয়?

হাতে কাগজে সমাধান করার পর একটি সহজ পরীক্ষা: recurrence-টি সরাসরি প্রোগ্রামে হিসাব করুন, তারপর আপনার অনুমান করা উত্তর দিয়ে ভাগ করুন। n বাড়ার সাথে অনুপাত যদি একটি ধ্রুবকের দিকে স্থির হয়, অনুমান Θ হিসেবে ঠিক পথে। যদি অনুপাত বাড়তেই থাকে বা শূন্যের দিকে নামে, অনুমান ভুল।

নিচের প্রোগ্রাম চারটি recurrence হিসাব করে n = 2k-এ (floor সহ, base case T(n) = 1 যখন n ≤ 1). একই উপ-সমস্যা বারবার হিসাব না করতে মান মনে রাখা হয় — এই কৌশলের নাম memoization (পর্ব ৫২-এ বিস্তারিত)। আপনার পছন্দের ভাষার ট্যাব বেছে নিন:

#include <cstdio>
#include <map>
using ll = long long;
std::map<ll, ll> memo[4];

ll T(int id, ll n) {                                   // base case: T = 1 for n <= 1
    if (n <= 1) return 1;
    if (auto it = memo[id].find(n); it != memo[id].end()) return it->second;
    ll r;
    if (id == 0)      r = T(0, n / 2) + 1;                    // A: binary search
    else if (id == 1) r = 2 * T(1, n / 2) + n;                // B: merge sort
    else if (id == 2) r = T(2, n / 3) + T(2, 2 * n / 3) + n;  // C: uneven split
    else              r = 3 * T(3, n / 4) + n * n;            // D: 3T(n/4) + n^2
    return memo[id][n] = r;
}

int main() {
    std::printf("%3s %8s %7s %7s %7s %7s\n", "k", "n", "A/lgn", "B/nlgn", "C/nlgn", "D/n^2");
    for (int k = 4; k <= 20; k += 4) {
        ll n = 1LL << k;
        double lg = k;
        std::printf("%3d %8lld %7.4f %7.4f %7.4f %7.4f\n", k, n, T(0, n) / lg,
                    T(1, n) / (n * lg), T(2, n) / (n * lg), T(3, n) / double(n * n));
    }
}

আউটপুট:

  k        n   A/lgn  B/nlgn  C/nlgn   D/n^2
  4       16  1.2500  1.2500  1.0469  1.2227
  8      256  1.1250  1.1250  1.0439  1.2305
 12     4096  1.0833  1.0833  1.0475  1.2308
 16    65536  1.0625  1.0625  1.0634  1.2308
 20  1048576  1.0500  1.0500  1.0729  1.2308

memo[id] হলো প্রতিটি recurrence-এর জন্য একটি std::map, যেখানে আগে হিসাব করা T(n) জমা থাকে; n / 2 পূর্ণসংখ্যার ভাগ, তাই এটিই floor. ভাগ করার সময় double ব্যবহার করা হয়েছে যাতে দশমিক অনুপাত আসে। সারণির ব্যাখ্যা নিচে, দুই ভাষার জন্য একই।

from functools import cache

@cache
def T(i, n):                                       # base case: T = 1 for n <= 1
    if n <= 1:
        return 1
    if i == 0: return T(0, n // 2) + 1                      # A: binary search
    if i == 1: return 2 * T(1, n // 2) + n                  # B: merge sort
    if i == 2: return T(2, n // 3) + T(2, 2 * n // 3) + n   # C: uneven split
    return 3 * T(3, n // 4) + n * n                         # D: 3T(n/4) + n^2

print("%3s %8s %7s %7s %7s %7s" % ("k", "n", "A/lgn", "B/nlgn", "C/nlgn", "D/n^2"))
for k in range(4, 21, 4):
    n = 1 << k
    print("%3d %8d %7.4f %7.4f %7.4f %7.4f" % (k, n, T(0, n) / k,
          T(1, n) / (n * k), T(2, n) / (n * k), T(3, n) / (n * n)))

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

  k        n   A/lgn  B/nlgn  C/nlgn   D/n^2
  4       16  1.2500  1.2500  1.0469  1.2227
  8      256  1.1250  1.1250  1.0439  1.2305
 12     4096  1.0833  1.0833  1.0475  1.2308
 16    65536  1.0625  1.0625  1.0634  1.2308
 20  1048576  1.0500  1.0500  1.0729  1.2308

@cache (functools) নিজে থেকেই memoization করে; // হলো পূর্ণসংখ্যার ভাগ (floor). Python-এর int যত বড়ই হোক উপচে পড়ে না, তাই n² নিয়ে চিন্তা নেই — C++-এ long long দরকার হয়েছিল।

এবার সারণিটি পড়ুন:

  • A আর B (binary search, merge sort): অনুপাত 1.25 → 1.05, ধীরে ধীরে ১-এর দিকে। কারণ আসল উত্তর lg n + 1 আর n(lg n + 1), তাই অনুপাত ঠিক 1 + 1/lg n. স্থির হচ্ছে — Θ(log n) আর Θ(n log n) নিশ্চিত।
  • D (3T(n/4) + n²): অনুপাত 1.2308-এ এসে থেমে গেছে — recursion tree-র (16/13)·n² হুবহু! মূল-ভারী গাছে নিচের স্তরগুলো দ্রুত নগণ্য হয়, তাই এত দ্রুত স্থির।
  • C (T(n/3) + T(2n/3) + n): 1.04 থেকে 1.07-এর মধ্যে ঘুরছে, খুব ধীরে বাড়ছে। এটি বাড়তে বাড়তে প্রায় 1.089-এর কাছে থামে (আরও গভীরে বক্সে কারণ দেখুন)। floor-এর কারণে একটু ওঠানামাও আছে। অনুপাত অসীমের দিকে ছুটছে না, শূন্যের দিকেও নামছে না — তাই Θ(n log n) ঠিক।

সংখ্যার পরীক্ষা প্রমাণ নয়। ধীরে বাড়া পদ (যেমন log log n) ছোট n-এ প্রায় ধ্রুবক দেখায় — n = 220-এ log log n মাত্র ৪.৩. এই পরীক্ষা ভুল অনুমান ধরতে দারুণ, কিন্তু ঠিক অনুমানকে চূড়ান্ত করতে লাগে substitution বা master theorem.

৮ · চলক বদল আর linear recurrence — √n আর Fibonacci

চলক বদল: T(n) = 2T(√n) + lg n

কিছু recurrence অচেনা দেখায়, কিন্তু একটু রূপ বদলালেই চেনা রূপে আসে। এই কৌশলের নাম change of variables. এখানে ইনপুট প্রতিবার বর্গমূলে নামছে — n, √n, n1/4, … এমন আচরণ দেখা যায় van Emde Boas tree-র মতো কিছু উন্নত স্ট্রাকচারে।

  1. m = lg n ধরুন, অর্থাৎ n = 2m. তাহলে √n = 2m/2, আর recurrence হয়: T(2m) = 2T(2m/2) + m.
  2. নতুন ফাংশন S(m) = T(2m) সংজ্ঞায়িত করুন। তাহলে S(m) = 2S(m/2) + m — এ তো merge sort!
  3. তাই S(m) = Θ(m log m). ফেরত বসালে T(n) = Θ(lg n · lg lg n).

একই কৌশলে T(n) = T(√n) + 1 হয় S(m) = S(m/2) + 1 = Θ(log m), অর্থাৎ T(n) = Θ(log log n) — অবিশ্বাস্য রকম ধীরে বাড়ে।

Linear homogeneous recurrence আর golden ratio

Fibonacci-র F(n) = F(n−1) + F(n−2) ভাগ-করে-জয় নয়; এখানে আকার ১ বা ২ করে কমে। এমন recurrence-কে বলে linear homogeneous (প্রতিটি পদ আগের পদগুলোর ধ্রুবক-গুণিত যোগফল, বাড়তি কোনো পদ নেই)। কৌশলটি হলো উত্তরের রূপ অনুমান করা: F(n) = rⁿ.

  • বসালে rⁿ = rⁿ⁻¹ + rⁿ⁻², দুই দিকে rⁿ⁻² দিয়ে ভাগ করে পাই r² = r + 1 — এটিই characteristic equation.
  • মূল দুটি: φ = (1 + √5)/2 ≈ 1.618 (golden ratio) আর ψ = (1 − √5)/2 ≈ −0.618.
  • সাধারণ সমাধান: F(n) = α·φⁿ + β·ψⁿ. F(0) = 0, F(1) = 1 বসিয়ে α = 1/√5, β = −1/√5. ফল: F(n) = (φⁿ − ψⁿ)/√5 — বিখ্যাত Binet-এর সূত্র। |ψ| < 1, তাই ψⁿ দ্রুত শূন্যের দিকে যায়, আর F(n) হলো φⁿ/√5-এর নিকটতম পূর্ণসংখ্যা।

এবার সরল fib(n)-এর সময়: C(n) = C(n−1) + C(n−2) + 1 (ডাকের সংখ্যা)। শেষের “+১” সরাতে U(n) = C(n) + 1 ধরুন: তাহলে U(n) = U(n−1) + U(n−2) — আবার Fibonacci-র রূপ! U(0) = U(1) = 2 থেকে পাওয়া যায় U(n) = 2F(n+1), অর্থাৎ C(n) = 2F(n+1) − 1 = Θ(φⁿ). নিচের প্রোগ্রাম আসল কোড চালিয়ে ডাক আর চাল গোনে:

#include <cmath>
#include <cstdio>

long long calls = 0, moves = 0;

long long fib(int n) {                           // calls: C(n) = C(n-1) + C(n-2) + 1
    ++calls;
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
}

void hanoi(int n, int from, int to, int via) {   // moves: T(n) = 2T(n-1) + 1
    if (n == 0) return;
    hanoi(n - 1, from, via, to);
    ++moves;                                     // move the largest disk
    hanoi(n - 1, via, to, from);
}

int main() {
    const double phi = (1 + std::sqrt(5.0)) / 2;
    std::printf("%3s %10s %10s %11s\n", "n", "fib_calls", "hanoi", "calls/phi^n");
    for (int n = 5; n <= 25; n += 5) {
        calls = moves = 0; fib(n); hanoi(n, 1, 3, 2);
        std::printf("%3d %10lld %10lld %11.4f\n", n, calls, moves, calls / std::pow(phi, n));
    }
}

আউটপুট:

  n  fib_calls      hanoi calls/phi^n
  5         15         31      1.3525
 10        177       1023      1.4391
 15       1973      32767      1.4465
 20      21891    1048575      1.4471
 25     242785   33554431      1.4472

Hanoi-র কলামে হুবহু 2ⁿ − 1: 31, 1023, 32767, … — unrolling-এর উত্তর মিলে গেল। Fibonacci-র ডাক 2F(n+1) − 1: n = 10-এ 2·89 − 1 = 177, n = 25-এ 2·121393 − 1 = 242785. শেষ কলামে ডাক ÷ φⁿ স্থির হচ্ছে প্রায় 1.4472-এ, যা 2φ/√5-এর মান — Θ(φⁿ) সংখ্যাতেও প্রমাণিত। মাত্র n = 25-এই প্রায় আড়াই লাখ ডাক; n = 50 হলে ডাক হবে কয়েক হাজার কোটি। একই মান বারবার হিসাব না করে মনে রাখলে (memoization, পর্ব ৫২) সময় নামে Θ(n)-এ।

আরও গভীরে: অসমান ভাগের সাধারণ সমাধান দেয় Akra–Bazzi method (১৯৯৮)। T(n) = T(n/3) + T(2n/3) + n-এর জন্য এটি বলে Θ(n log n); আরও সূক্ষ্ম হিসাবে প্রধান পদটি n·lg n / H, যেখানে H = lg 3 − 2/3 ≈ 0.918 — তাই ওপরের সারণির C-কলাম ধীরে ধীরে 1/H ≈ 1.089-এর দিকে যায়। আর merge sort-এর worst-case তুলনার সংখ্যা floor-ceiling সহ হুবহু হিসাব করা যায়: n⌈lg n⌉ − 2⌈lg n⌉ + 1 — n দুইয়ের ঘাত না হলেও উত্তর Θ(n log n)-ই থাকে।

পর্ব ০৯-এর ঝলক: T(n) = aT(n/b) + f(n) রূপের recurrence-এ তিনটি গাছের কোনটি ঘটছে, তা বোঝা যায় f(n)-কে leaf-এর সংখ্যা nlogb a-এর সাথে তুলনা করে। ১৯৮০ সালে Jon Bentley, Dorothea Haken আর James Saxe এই ধরনের recurrence-এর একটি সাধারণ সমাধান-পদ্ধতি প্রকাশ করেন; পরে Cormen, Leiserson ও Rivest-এর পাঠ্যবই এর একটি রূপকে “master theorem” নামে জনপ্রিয় করে। কিন্তু Hanoi-র 2T(n−1) + 1, Fibonacci, বা T(n/3) + T(2n/3) — এগুলো master theorem-এর আওতার বাইরে। সেখানে এই পর্বের হাতিয়ারই ভরসা।

৯ · AI-এর যুগে recurrence

AI-কে “এই কোডের time complexity কত?” জিজ্ঞেস করলে উত্তর আসে সেকেন্ডে, আত্মবিশ্বাসী ভাষায়। সমস্যা হলো, recurrence-এর প্রশ্নে AI প্রায়ই চেনা প্যাটার্নের সাথে মিলিয়ে উত্তর দেয় — আর দেখতে একই রকম দুটি recurrence-এর উত্তর যে একেবারে আলাদা হতে পারে, সেটা এই পর্বেই দেখেছেন। নিচের ভুলগুলো বারবার দেখা যায়:

AI-এর ভুলসঠিক কথাকীভাবে ধরবেন
T(n) = 2T(n−1) + 1-এ master theorem লাগিয়ে “Θ(n)”Master theorem শুধু T(n/b) রূপে খাটে; উত্তর 2ⁿ − 1n = 1…5 হাতে হিসাব: 1, 3, 7, 15, 31
T(n) = T(n−1) + n-কে “Θ(n log n)”যোগফল 1 + 2 + … + n = Θ(n²)Unrolling করে লিখুন
“cn + n = O(n), তাই প্রমাণ শেষ”Induction-এ ধ্রুবক হুবহু একই থাকতে হবেশেষ লাইনে ঠিক ≤ cn এসেছে কি না দেখুন
Recursion tree-তে leaf-এর খরচ বাদLeaf-এর সংখ্যা nlogb a, তা ভারী হতে পারেJS টুলে শেষ স্তরের খরচ দেখুন
সরল fib-কে “O(2ⁿ)” বলেই থামাসেটা ঠিক, কিন্তু আঁটসাঁট (tight) উত্তর Θ(φⁿ)ডাক ÷ φⁿ অনুপাত স্থির কি না দেখুন

AI-কে শিক্ষকের মতো নয়, সহপাঠীর মতো ব্যবহার করুন — তাকে দিয়ে ধাপ লেখান, আর প্রতিটি ধাপ নিজে যাচাই করুন। নিচের prompt-এ শেষ দুটি নির্দেশ খেয়াল করুন:

নিচের রিকার্সিভ ফাংশনের সময় বিশ্লেষণ করো: [এখানে কোড পেস্ট করুন] ১. প্রতিটি রিকার্সিভ ডাক কয়টি, কত আকারের, আর ডাকের বাইরে কত কাজ — আলাদা করে লেখো, তারপর base case সহ recurrence দাও। ২. Recursion tree দিয়ে প্রথম তিনটি স্তর আর শেষ স্তরের খরচ দেখাও, স্তরের সংখ্যা বলো। ৩. Master theorem ব্যবহার করলে আগে বলো recurrence-টি T(n) = aT(n/b) + f(n) রূপে আছে কি না। ৪. Substitution দিয়ে প্রমাণ করো — প্রতিটি অসমতায় স্পষ্ট ধ্রুবক c লিখবে, O-চিহ্ন ব্যবহার করবে না। ৫. শেষে n = 1 থেকে 8 পর্যন্ত recurrence-এর মান একটি সারণিতে দাও, যাতে আমি হাতে মিলিয়ে নিতে পারি।

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

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

  • T(n) = 2T(n/2) + n-কে recursion tree দিয়ে সমাধান করুন। — প্রতি স্তরে n, স্তর lg n + 1টি, তাই Θ(n log n)
  • Tower of Hanoi-তে n চাকতির জন্য কয়টি চাল লাগে? প্রমাণসহ। — T(n) = 2T(n−1) + 1, T(1) = 1 থেকে unrolling বা induction-এ 2ⁿ − 1
  • T(n) ≤ cn প্রমাণ করতে গিয়ে cn + 1 পেলে কী করবেন? — অনুমান শক্ত করে T(n) ≤ cn − d প্রমাণ করুন
  • T(n) = 2T(√n) + log n-এর সমাধান? — m = lg n বসিয়ে S(m) = 2S(m/2) + m, উত্তর Θ(log n · log log n)
  • সরল রিকার্সিভ Fibonacci-র time complexity? — Θ(φⁿ), φ ≈ 1.618; O(2ⁿ) সঠিক কিন্তু আঁটসাঁট নয়

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

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

প্র ০১Tower of Hanoi-র কিংবদন্তিতে ৬৪টি সোনার চাকতি — সত্যিই কি পৃথিবী শেষ হওয়ার আগে কাজটা শেষ হবে?

ধাঁধাটি ১৮৮৩ সালে বাজারে আনেন ফরাসি গণিতবিদ Édouard Lucas (ছদ্মনামে)। সঙ্গে জুড়ে দেওয়া হয় একটি গল্প: এক মন্দিরে পুরোহিতরা ৬৪টি সোনার চাকতি নিয়মমতো সরাচ্ছেন, কাজ শেষ হলে নাকি পৃথিবী ধ্বংস হবে। গল্পটি বানানো, কিন্তু গণিতটা খাঁটি।

এই পর্বে প্রমাণ করেছেন, n চাকতিতে অন্তত 2ⁿ − 1টি চাল লাগে। কেন “অন্তত”? সবচেয়ে বড় চাকতিটি সরানোর মুহূর্তে বাকি n−1টি অবশ্যই তৃতীয় দণ্ডে থাকতে হবে — নইলে বড়টি ছোটর ওপরে পড়বে। তাই যেকোনো সমাধানে n−1টি চাকতি অন্তত দুবার পুরো সরাতে হয়, আর বড়টি অন্তত একবার। এটিই recurrence-কে নিচের সীমা হিসেবেও সত্য করে: যেকোনো পদ্ধতিতে চাল ≥ 2T(n−1) + 1. অর্থাৎ আমাদের রিকার্সিভ সমাধানটি শুধু একটি সমাধান নয়, সর্বোত্তম সমাধান।

এবার সংখ্যাটা: 2⁶⁴ − 1 ≈ 1.8 × 10¹⁹. প্রতি সেকেন্ডে একটি চাল দিলে লাগবে প্রায় ৫৮,৫০০ কোটি বছর — মহাবিশ্বের বর্তমান বয়সের (প্রায় ১,৩৮০ কোটি বছর) চল্লিশ গুণেরও বেশি। আধুনিক CPU সেকেন্ডে শত কোটি চাল গুনতে পারলেও লাগবে কয়েকশো বছর।

শিক্ষাটা অ্যালগরিদমের জন্য গভীর: exponential recurrence-এ হার্ডওয়্যার দ্রুত করে লাভ খুব কম। কম্পিউটার হাজার গুণ দ্রুত হলে 2ⁿ সময়ের অ্যালগরিদম একই সময়ে মাত্র প্রায় ১০টি বেশি চাকতি সামলাতে পারে (কারণ 2¹⁰ ≈ 1000). অথচ n log n অ্যালগরিদম একই উন্নতিতে প্রায় হাজার গুণ বড় ইনপুট সামলায়।

মূল উপলব্ধি: Recurrence শুধু সময় হিসাবের যন্ত্র নয় — সঠিকভাবে যুক্তি দিলে এটি দেখায় কোনো কাজের সর্বনিম্ন খরচও। আর T(n−1)-এর আগে “2” বসলেই সমস্যা ব্যবহারিক অর্থে অসাধ্য হয়ে যেতে পারে।

প্র ০২খরগোশের হিসাব থেকে golden ratio — Fibonacci-র recurrence অ্যালগরিদমের জগতে এত বারবার কেন ফিরে আসে?

১২০২ সালে ইতালির পিসা শহরের Leonardo (পরে যাঁকে Fibonacci নামে ডাকা হয়) তাঁর বই Liber Abaci-তে একটি খরগোশের ধাঁধা দেন: প্রতিটি প্রাপ্তবয়স্ক জোড়া প্রতি মাসে একটি নতুন জোড়ার জন্ম দেয়, নতুন জোড়া এক মাস পরে প্রাপ্তবয়স্ক হয়। এক বছরে কত জোড়া? প্রতি মাসের সংখ্যা আগের দুই মাসের যোগফল — F(n) = F(n−1) + F(n−2). মজার তথ্য: এই ধারা তারও বহু আগে ভারতীয় ছন্দশাস্ত্রে (দীর্ঘ-হ্রস্ব মাত্রার বিন্যাস গোনায়) পরিচিত ছিল।

সরাসরি সূত্র F(n) = (φⁿ − ψⁿ)/√5 আঠারো শতকেই de Moivre-সহ কয়েকজন গণিতবিদ জানতেন; ১৮৪৩ সালে Jacques Binet এটি প্রকাশ করেন, তাই নাম Binet-এর সূত্র। এই পর্বের characteristic equation পদ্ধতি দিয়ে যে কেউ এটি কয়েক লাইনে বের করতে পারেন।

অ্যালগরিদমে Fibonacci বারবার আসে কারণ “এক ধাপ বা দুই ধাপ কমে” এমন রিকার্শন খুব স্বাভাবিক। কয়েকটি উদাহরণ: সিঁড়িতে এক বা দুই ধাপ করে ওঠার উপায় গোনা (জনপ্রিয় ইন্টারভিউ প্রশ্ন); AVL tree-র সবচেয়ে পাতলা গাছের নোড-সংখ্যা Fibonacci-র মতো recurrence মানে, আর সেখান থেকেই প্রমাণ হয় AVL-এর উচ্চতা O(log n) (পর্ব ২৮); Euclid-এর GCD অ্যালগরিদমের worst case আসে পরপর দুটি Fibonacci সংখ্যায় — Gabriel Lamé ১৮৪৪ সালে এটি দেখান, আর একে প্রায়ই computational complexity-র প্রথম দিকের বিশ্লেষণগুলোর একটি বলা হয় (পর্ব ৬২)। Fibonacci heap নামের ডেটা স্ট্রাকচারের নামও এসেছে এর বিশ্লেষণে Fibonacci সংখ্যা আসে বলে।

আর সময়ের দিক থেকে: সরল fib-এর ডাক 2F(n+1) − 1 = Θ(φⁿ), memoization-এ Θ(n), আর 2×2 matrix-এর ঘাত দ্রুত বের করে Θ(log n) গুণে।

মূল উপলব্ধি: একটি recurrence যখন অনেক জায়গায় দেখা দেয়, তার সমাধান একবার ভালোভাবে জানলে অনেক অ্যালগরিদম একসাথে বোঝা হয়ে যায়। φ ≈ 1.618 সংখ্যাটি তাই শুধু শিল্পকলার “সোনালি অনুপাত” নয়, অ্যালগরিদমেরও একটি ধ্রুবক।

প্র ০৩Substitution-এ আগে উত্তর “অনুমান” করতে হয় — এটা কি ফাঁকি নয়? গণিতে অনুমানের জায়গা কোথায়?

প্রশ্নটি স্বাভাবিক — পরীক্ষার খাতায় “ধরি উত্তর n log n” লিখতে প্রথমে অস্বস্তি লাগে। কিন্তু এখানে দুটি আলাদা কাজ আছে: আবিষ্কার আর প্রমাণ। Induction শুধু দ্বিতীয় কাজের যন্ত্র; সে বলে না উত্তর কী, শুধু যাচাই করে প্রস্তাবিত উত্তর সঠিক কি না। অনুমান ভুল হলে প্রমাণ কোথাও আটকে যাবে — তাই ফাঁকি দেওয়ার সুযোগ নেই।

গণিতের ইতিহাসও এভাবেই চলে। বহু বিখ্যাত উপপাদ্য প্রথমে সংখ্যা দেখে অনুমান করা হয়েছে, প্রমাণ এসেছে পরে। Euler, Gauss — দুজনেই বিপুল পরিমাণ হাতে-হিসাব করে প্যাটার্ন খুঁজতেন। আজকের গবেষকেরা একই কাজ করেন কম্পিউটারে — এই পর্বের অনুপাত-সারণির মতো।

Recurrence-এর ক্ষেত্রে ভালো অনুমানের উৎস কয়েকটি: recursion tree (প্রায় সবসময় সঠিক রূপ দেয়), চেনা recurrence-এর সাথে মিল, আর সংখ্যার পরীক্ষা। তবে অনুমানের রূপ নিয়েও ভাবতে হয়: সঠিক Θ অনুমান করেও প্রমাণ আটকে যেতে পারে, যেমন 2T(n/2) + 1-এ T(n) ≤ cn. তখন নিম্নমানের পদ বিয়োগ করে (cn − d) অনুমান শক্ত করতে হয় — এই কৌশলটি Cormen-Leiserson-Rivest-Stein-এর বিখ্যাত পাঠ্যবইয়ে আলাদা করে শেখানো হয়, কারণ নতুনরা প্রায়ই উল্টোটা করেন: অনুমান দুর্বল করেন।

আরও একটি দিক: ১৯৯৮ সালে Mohamad Akra আর Louay Bazzi এমন একটি পদ্ধতি প্রকাশ করেন যা T(n/3) + T(2n/3) + n-এর মতো অসমান ভাগের recurrence-ও সরাসরি সমাধান করে। অর্থাৎ যত বেশি recurrence আমরা বুঝি, তত বেশি “অনুমান” পরিণত হয় সাধারণ সূত্রে। কিন্তু নতুন, অদ্ভুত recurrence-এ ফিরে যেতে হয় সেই পুরোনো পথে: অনুমান, তারপর প্রমাণ।

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

প্র ০৪Master theorem থাকতে recursion tree আঁকা শিখব কেন? এক লাইনের সূত্রই কি যথেষ্ট নয়?

Master theorem সত্যিই শক্তিশালী — পর্ব ০৯-এ দেখবেন, T(n) = aT(n/b) + f(n) রূপের বেশিরভাগ recurrence সে এক লাইনে সমাধান করে। এর ইতিহাস: ১৯৮০ সালে Jon Bentley, Dorothea Haken আর James Saxe divide and conquer recurrence-এর একটি সাধারণ সমাধান-পদ্ধতি প্রকাশ করেন; ১৯৯০ সালে প্রকাশিত Cormen, Leiserson ও Rivest-এর Introduction to Algorithms বইয়ের মাধ্যমে “master theorem” নামটি সারা বিশ্বের শিক্ষার্থীদের কাছে পৌঁছায়।

কিন্তু সূত্রের সীমা আছে। (১) T(n−1) রূপ: Hanoi, রিকার্সিভ selection sort, Fibonacci — কোনোটিই n/b রূপে নেই। (২) অসমান ভাগ: T(n/3) + T(2n/3) + n, বা quick sort-এর ৯ : ১ ভাগ। (৩) ফাঁক: T(n) = 2T(n/2) + n/lg n-এর মতো কিছু f(n) সূত্রের তিনটি ঘটনার কোনোটিতেই পড়ে না (সাধারণ রূপে)। (৪) বর্গমূল: 2T(√n) + lg n — আগে চলক বদলাতে হয়।

আরও গুরুত্বপূর্ণ: সূত্র মুখস্থ থাকলে আপনি উত্তর জানবেন, কিন্তু কেন তা জানবেন না। Recursion tree দেখায় খরচ আসলে কোথায় — মূলে, প্রতি স্তরে সমানভাবে, নাকি leaf-এ। এই অন্তর্দৃষ্টিই অ্যালগরিদম উন্নত করার পথ দেখায়। যেমন Karatsuba-র গুণন বা Strassen-এর matrix গুণনে উপ-সমস্যার সংখ্যা a কমানোই ছিল মূল আবিষ্কার, কারণ leaf-ভারী গাছে সময় নির্ভর করে nlogb a-এর ওপর — a একটি কমলেই ঘাত কমে।

প্রোগ্রামিং কনটেস্টেও প্রায়ই এমন recurrence আসে যা কোনো বইয়ের ছকে পড়ে না — যেমন একটি ডাক n/2-এ আর একটি n/4-এ, সঙ্গে n log n কাজ। তখন গাছ এঁকে প্রতিটি স্তরের খরচ দেখা ছাড়া উপায় নেই।

মূল উপলব্ধি: সূত্র হলো ক্যালকুলেটর, recursion tree হলো বোঝাপড়া। ক্যালকুলেটর দ্রুত, কিন্তু কোন সমস্যায় খাটে না, সেটা বোঝার জন্যও বোঝাপড়া লাগে।

প্র ০৫একটি recurrence-এর ছোট্ট একটি সংখ্যা বদলে কি ইতিহাস বদলেছে? Karatsuba আর Strassen-এর গল্প কী বলে?

হ্যাঁ — অ্যালগরিদমের ইতিহাসের সবচেয়ে নাটকীয় দুটি মুহূর্ত আসলে recurrence-এর একটি গুণক কমানোর গল্প।

Karatsuba (১৯৬০): দুটি n অঙ্কের সংখ্যা স্কুলের নিয়মে গুণ করতে লাগে n² ধাপ। ১৯৬০ সালে মস্কোয় বিখ্যাত গণিতবিদ Andrey Kolmogorov একটি সেমিনারে ধারণা দেন যে এর চেয়ে দ্রুত সম্ভব নয়। মাত্র কয়েক দিনের মধ্যে তেইশ বছর বয়সী ছাত্র Anatoly Karatsuba দেখান যে সংখ্যাকে অর্ধেক করে চারটির বদলে তিনটি গুণ দিয়ে কাজ চালানো যায়। Recurrence বদলে গেল: 4T(n/2) + n থেকে 3T(n/2) + n. Leaf-ভারী গাছে উত্তর nlog₂ 3 ≈ n1.585. আজকের বড় সংখ্যার লাইব্রেরি (যেমন cryptography-তে) এটি আর এর উত্তরসূরি পদ্ধতিগুলো ব্যবহার করে।

Strassen (১৯৬৯): দুটি n×n matrix গুণে সাধারণ পদ্ধতিতে n³ ধাপ। ভাগ করে জয় করলে আটটি অর্ধেক-আকারের গুণ: 8T(n/2) + n², উত্তর আবার n³. জার্মান গণিতবিদ Volker Strassen দেখান যে চতুর কিছু যোগ-বিয়োগ দিয়ে সাতটি গুণেই চলে: 7T(n/2) + n², উত্তর nlog₂ 7 ≈ n2.807. এখান থেকে বিশাল একটি গবেষণা-ধারা শুরু হয়, যা আজও চলছে।

দুই গল্পেই কৌশল একই: বাইরের কাজ (n বা n²) একটু বাড়ল, কিন্তু উপ-সমস্যার সংখ্যা a একটি কমল। Leaf-ভারী গাছে সময় নির্ভর করে nlogb a-এর ওপর, তাই a-এর সামান্য পরিবর্তন ঘাতকে বদলে দেয় — আর বড় n-এ ঘাতের পার্থক্যই সব। এই অ্যালগরিদমগুলো বিস্তারিত দেখবেন পর্ব ৪৯-এ।

মূল উপলব্ধি: Recurrence শুধু বিশ্লেষণের হাতিয়ার নয়, আবিষ্কারের দিকনির্দেশক। গাছ এঁকে দেখুন খরচ কোথায় জমছে — তারপর ঠিক সেই জায়গাটিতে আঘাত করুন।

অনুশীলন

কাগজ-কলম নিয়ে বসুন — recurrence হাতে চালানোই আসল অনুশীলন।

  1. হাতে চালান: T(1) = 1 ধরে (ক) T(n) = 2T(n/2) + n-এর T(8) আর (খ) T(n) = 3T(n/4) + n²-এর T(16) হিসাব করুন। তারপর T(8)-কে n(lg n + 1) সূত্রের সাথে, আর T(16)/16²-কে উপরের C++ সারণির D-কলামের প্রথম মানের সাথে মেলান।

    (ক) T(2) = 2·1 + 2 = 4, T(4) = 2·4 + 4 = 12, T(8) = 2·12 + 8 = 32. সূত্র: 8·(3 + 1) = 32 — মিলে গেছে।

    (খ) T(4) = 3·T(1) + 16 = 19, T(16) = 3·19 + 256 = 313. 313/256 ≈ 1.2227 — সারণির k = 4 সারির D-কলাম হুবহু এটাই। n বাড়লে এটি 16/13 ≈ 1.2308-এর দিকে যায়।

  2. কোড থেকে recurrence: নিচের ফাংশনের recurrence লিখুন এবং recursion tree দিয়ে সমাধান করুন: solve(n) — যদি n ≤ 1 হয় ফেরত দেয়; নইলে ০ থেকে n−1 পর্যন্ত একটি loop চালায় (প্রতিবার ধ্রুবক কাজ), তারপর solve(n/2) তিনবার ডাকে।

    তিনটি ডাক, প্রতিটি n/2, বাইরে n কাজ: T(n) = 3T(n/2) + n. স্তর i-তে 3iটি নোড, প্রতিটিতে n/2i কাজ, স্তরের খরচ (3/2)i·n — বাড়তে থাকা ধারা, তাই leaf-এরা জেতে। স্তর lg n-এ leaf-এর সংখ্যা 3lg n = nlg 3 ≈ n1.585. উত্তর Θ(nlog₂ 3) — Karatsuba-র সময়ও ঠিক এটাই। JS টুলে levelTable(3, 2, 1, 64) চালালে স্তরের খরচ 64, 96, 144, 216, 324, 486, 729 — প্রতিবার ১.৫ গুণ।

  3. কোড লিখুন: অংশ ৭-এর প্রোগ্রামে (C++ বা Python) পঞ্চম একটি recurrence যোগ করুন: E: T(n) = 4T(n/2) + n, এবং T(n)/n² অনুপাত ছাপান। অনুপাত কোন মানে স্থির হয়? হাতে সূত্র বের করে মিলিয়ে দেখুন।

    Python-এ একটি শাখা যোগ করুন — if i == 4: return 4 * T(4, n // 2) + n (শেষ return-এর আগে), আর print-এ T(4, n) / (n * n). C++-এ memo[5] করে একই শাখা যোগ করুন।

    অনুপাত: k = 4-এ 1.9375, তারপর 1.9961, 1.9998, … → 2. কারণ: T(n) = an² + bn ধরে বসালে an² + bn = 4(an²/4 + bn/2) + n = an² + 2bn + n, তাই b = −1; T(1) = a − 1 = 1 থেকে a = 2. অর্থাৎ T(n) = 2n² − n, অনুপাত 2 − 1/n → 2. উত্তর Θ(n²) — leaf-ভারী গাছ।

  4. চলক বদল: T(n) = T(√n) + 1 (T(2) = 1) সমাধান করুন। n = 216 = 65536 হলে T(n) কত?

    m = lg n, S(m) = T(2m) ধরলে S(m) = S(m/2) + 1, S(1) = 1 — এটি binary search-এর recurrence, তাই S(m) = lg m + 1. ফলে T(n) = lg lg n + 1 = Θ(log log n).

    n = 65536 হলে: √65536 = 256, √256 = 16, √16 = 4, √4 = 2. গুনে দেখুন — 65536 → 256 → 16 → 4 → 2: চারটি ধাপ, তারপর T(2) = 1. মোট T(65536) = 4 + 1 = 5 = lg lg 65536 + 1 = lg 16 + 1 = 5.

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

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

ক্রমRecursion tree দিয়ে একটি রিকার্সিভ কোডের সময় বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. কোড দেখে base case সহ recurrence লেখা
  2. গাছ আঁকা — প্রতিটি নোডে সেই ডাকের নিজস্ব কাজ
  3. প্রতিটি স্তরের মোট খরচ বের করা
  4. স্তরের সংখ্যা (গভীরতা) আর leaf-এর সংখ্যা বের করা
  5. সব স্তরের খরচ যোগ করে উত্তর অনুমান
  6. Substitution (induction) দিয়ে অনুমান প্রমাণ

T(n) = 2T(n−1) + 1, T(1) = 1 — এই recurrence-এর সঠিক সমাধান কোনটি?

  • T(n) = n
  • T(n) = 2n − 1
  • T(n) = Θ(n log n)
  • T(n) = Θ(n2)

T(n) = 2T(n/2) + n-এর recursion tree-তে (n দুইয়ের ঘাত) প্রতি স্তরের খরচ আর স্তরের সংখ্যা কত?

  • প্রতি স্তরে n/2i, স্তর lg n টি
  • প্রতি স্তরে n, স্তর n টি
  • প্রতি স্তরে n, স্তর lg n + 1টি
  • প্রতি স্তরে n2, স্তর lg n টি

কেউ T(n) = 2T(n/2) + n-এর জন্য লিখলেন: “ধরি T(k) ≤ ck. তাহলে T(n) ≤ cn + n = O(n). তাই T(n) = O(n).” প্রমাণটির ভুল কোথায়?

  • Inductive step-এ ঠিক T(n) ≤ cn আসেনি — cn + n, cn-এর চেয়ে বড়
  • শুধু base case দেখানো হয়নি
  • n/2-এর বদলে ⌊n/2⌋ লেখা হয়নি
  • প্রমাণ ঠিক আছে, merge sort আসলেই O(n)

T(n) = 3T(n/4) + n2-এর recursion tree-তে স্তর i-এর মোট খরচ কত, আর উত্তর কী?

  • (3/4)i·n2; উত্তর Θ(n2 log n)
  • n2 প্রতি স্তরে; উত্তর Θ(n2 log n)
  • (16/3)i·n2; উত্তর Θ(n0.79)
  • (3/16)i·n2; উত্তর Θ(n2)

T(n) = 2T(√n) + lg n — চলক বদলে (m = lg n) সমাধান করলে উত্তর কী?

  • Θ(log log n)
  • Θ(log n · log log n)
  • Θ(n)
  • Θ(log n)

F(n) = F(n−1) + F(n−2)-এর characteristic equation আর সরল রিকার্সিভ Fibonacci ফাংশনের সময় কোনটি?

  • r2 = 2r; Θ(2n)
  • r2 = r + 1; Θ(n log n)
  • r2 = r + 1; Θ(φn), φ ≈ 1.618
  • r2 + r + 1 = 0; Θ(n)

শব্দকোষ

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

মনে রাখুন

  1. Recurrence = (ডাকের সংখ্যা × ছোট আকারের T) + বাইরের কাজ, সঙ্গে base case; base case আর floor-ceiling সাধারণত শুধু ধ্রুবক বদলায়।
  2. Unrolling: খুলে লিখুন, প্যাটার্ন ধরুন, base case-এ পৌঁছান — Hanoi 2ⁿ − 1, binary search lg n + 1, T(n−1) + n হলো Θ(n²)।
  3. Recursion tree: স্তরের খরচ সমান → খরচ × গভীরতা; কমছে → মূল জেতে; বাড়ছে → leaf (nlogb a) জেতে।
  4. Substitution: একই ধ্রুবক দিয়ে induction; আটকে গেলে নিম্নমানের পদ বিয়োগ করে অনুমান শক্ত করুন — কখনো O-চিহ্ন দিয়ে “প্রমাণ” নয়।
  5. বিশেষ কৌশল: √n হলে m = lg n বসান; Fibonacci-ধাঁচে rⁿ বসিয়ে characteristic equation — উত্তর Θ(φⁿ)। আর সবশেষে সংখ্যা দিয়ে অনুপাত যাচাই।

পরের পর্বে: আজ যে তিন ধরনের গাছ দেখলেন — সমান, মূল-ভারী আর leaf-ভারী — সেগুলোকে এক সূত্রে বাঁধব। T(n) = aT(n/b) + f(n) দেখলেই এক লাইনে উত্তর: master theorem.

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

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