পাঠ ৪১ · ৪৪-এর মধ্যে · মডিউল ৮
Home / Courses / Discrete Mathematics / রিকার্সিভ জটিলতা

রিকার্সিভ অ্যালগরিদমের জটিলতা

Complexity of recursive algorithms
৯ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • একটি রিকার্সিভ অ্যালগরিদমের ধাপ সংখ্যাকে রিকারেন্স হিসেবে লেখা
  • নেইভ ফিবোনাচির রিকারেন্স ক্যারেক্টারিস্টিক ইকুয়েশন (L30) দিয়ে সমাধান করে $\Theta(\varphi^n)$ পাওয়া
  • merge sort ও binary search-এর রিকারেন্স Master Theorem (L31) দিয়ে সমাধান করা
  • Python কোডে নেইভ বনাম মেমোয়াইজড ফিবোনাচির কল সংখ্যা লাইভ ইনস্ট্রুমেন্ট করে তুলনা করা

১ · রিকারেন্স থেকে জটিলতায়

M6-এ (L29-L32) আমরা রিকারেন্স রিলেশন সমাধান করার গণিত শিখেছি — কিন্তু সেখানে এই সমাধানগুলো শুধু গাণিতিক অনুশীলন ছিল। এই পাঠে সেই একই গণিত সরাসরি ব্যবহার হবে: যখন একটি অ্যালগরিদম নিজেকে রিকার্সিভভাবে কল করে, তার "ধাপ সংখ্যা" $T(n)$ প্রায় সবসময় একটি রিকারেন্স রিলেশন মেনে চলে। সেই রিকারেন্স সমাধান করাই অ্যালগরিদমের জটিলতা বের করার পদ্ধতি।

২ · ওয়ার্কড উদাহরণ ১ — নেইভ রিকার্সিভ ফিবোনাচি

L29-এ দেখা ফিবোনাচি সংজ্ঞা $F(n)=F(n-1)+F(n-2)$ সরাসরি কোডে বসালে (memoization ছাড়া), প্রতিটি কল দুটি নতুন রিকার্সিভ কল তৈরি করে। এই ফাংশনের কল সংখ্যা $T(n)$-এর রিকারেন্স: $$T(n) = T(n-1) + T(n-2) + O(1)$$ এটি L31-এর ডিভাইড-অ্যান্ড-কনকার ফর্ম ($T(n)=aT(n/b)+f(n)$) নয় — তাই Master Theorem এখানে প্রযোজ্য নয়। কিন্তু এটি ঠিক L30-এর ধরনের একটি ২য়-ক্রম লিনিয়ার হোমোজিনিয়াস রিকারেন্স। ক্যারেক্টারিস্টিক ইকুয়েশন: $$x^2 = x + 1 \implies x^2 - x - 1 = 0 \implies x = \frac{1 \pm \sqrt5}{2}$$ পজিটিভ মূল $\varphi = \frac{1+\sqrt5}{2} \approx 1.618$ — বিখ্যাত গোল্ডেন রেশিওGolden Ratio (φ)≈১.৬১৮, একটি বিশেষ ধ্রুবক যা ফিবোনাচি সিকোয়েন্সের ক্যারেক্টারিস্টিক ইকুয়েশনের মূল হিসেবে স্বাভাবিকভাবে বেরিয়ে আসে।। এর মানে $T(n) = \Theta(\varphi^n)$ — এক্সপোনেনশিয়াল। তাই নেইভ রিকার্সিভ ফিবোনাচি বড় $n$-এ ব্যবহারিকভাবে অসম্ভব ধীর।

৩ · ওয়ার্কড উদাহরণ ২ ও ৩ — Merge Sort ও Binary Search (L31 পুনর্ব্যবহার)

এবার L31-এর দুটি ফলাফল সরাসরি পুনর্ব্যবহার করি:

  • Merge sort: $T(n)=2T(n/2)+\Theta(n)$। এখানে $a=2,b=2$, তাই $\log_b a = \log_2 2 = 1$, এবং $f(n)=n=n^{\log_b a}$ — Master Theorem কেস ২ প্রযোজ্য, তাই $T(n)=\Theta(n\log n)$।
  • Binary search: $T(n)=T(n/2)+\Theta(1)$। এখানে $a=1,b=2$, তাই $\log_b a=\log_2 1=0$, এবং $f(n)=\Theta(1)=n^0=n^{\log_b a}$ — আবার কেস ২, তাই $T(n)=\Theta(\log n)$।
মূল কথা — এই পাঠ কেন "পেব্যাক"

M6-এ আমরা বিমূর্তভাবে রিকারেন্স সমাধান করার কৌশল শিখেছিলাম — ক্যারেক্টারিস্টিক ইকুয়েশন, Master Theorem। এই পাঠে দেখা গেল সেই একই কৌশল সরাসরি বলে দেয় কোন অ্যালগরিদম ব্যবহারযোগ্য (merge sort, binary search — polynomial/logarithmic) আর কোনটি নয় (নেইভ ফিবোনাচি — exponential)। এটাই বিচ্ছিন্ন গণিত ও বাস্তব প্রোগ্রামিং সিদ্ধান্তের মধ্যে সবচেয়ে সরাসরি সংযোগগুলোর একটি।

৪ · কোড: নেইভ বনাম মেমোয়াইজড — কল সংখ্যায় ব্যবধান

তত্ত্ব অনুযায়ী নেইভ ফিবোনাচি এক্সপোনেনশিয়াল আর মেমোয়াইজড সংস্করণ (L29-এ ইঙ্গিত করা, DSA কোর্সে dynamic programming হিসেবে বিস্তারিত) লিনিয়ার। নিচের কোডে আমরা সংখ্যাটি অনুমান করছি না — সরাসরি একটি কাউন্টার দিয়ে ইনস্ট্রুমেন্ট করে গণনা করছি:

Python
# নেইভ রিকার্সিভ ফিবোনাচি — প্রতিটি ফাংশন-কলে কাউন্টার বাড়বে
naive_calls = 0

def fib_naive(n):
    global naive_calls
    naive_calls += 1
    if n < 2:
        return n
    return fib_naive(n - 1) + fib_naive(n - 2)

n = 20
result_naive = fib_naive(n)
print(f"fib_naive({n}) = {result_naive}  |  মোট ফাংশন-কল: {naive_calls}")

# মেমোয়াইজড ফিবোনাচি — একই n দ্বিতীয়বার গণনা করা হয় না (cache miss-এই শুধু কাউন্ট বাড়বে)
memo = {}
memo_computations = 0

def fib_memo(n):
    global memo_computations
    if n in memo:
        return memo[n]
    memo_computations += 1
    if n < 2:
        memo[n] = n
        return n
    result = fib_memo(n - 1) + fib_memo(n - 2)
    memo[n] = result
    return result

result_memo = fib_memo(n)
print(f"fib_memo({n})  = {result_memo}  |  মোট নতুন গণনা: {memo_computations}")

ratio = naive_calls / memo_computations
print(f"\nনেইভ, মেমোয়াইজডের চেয়ে {ratio:.1f} গুণ বেশি কল করেছে!")

    
মেমোয়াইজেশন প্রতিটি $n$ শুধু একবার গণনা করে (মোট $n+1$টি ইউনিক সাব-প্রবলেম, তাই কল সংখ্যা লিনিয়ার, $\Theta(n)$), যেখানে নেইভ সংস্করণ একই সাব-প্রবলেম বারবার recompute করে — এটাই এক্সপোনেনশিয়াল বনাম লিনিয়ারের পার্থক্যের প্রকৃত উৎস। এই কৌশলকেই DSA কোর্সে "dynamic programming"-এর ভিত্তি হিসেবে বিস্তারিত দেখা হবে।

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

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

প্র ০১ নেইভ ফিবোনাচির রিকারেন্স $T(n)=T(n-1)+T(n-2)+O(1)$-এ কেন Master Theorem প্রযোজ্য নয়?

Master Theorem শুধুমাত্র ডিভাইড-অ্যান্ড-কনকার ফর্ম $T(n)=aT(n/b)+f(n)$-এর রিকারেন্সে কাজ করে — যেখানে সমস্যাটি একটি ভাগফলে (যেমন $n/2$) ছোট হয়। কিন্তু ফিবোনাচির রিকারেন্সে সমস্যাটি একটি বিয়োগফলে ($n-1$, $n-2$) ছোট হয় — সম্পূর্ণ ভিন্ন কাঠামো। এই ধরনের রিকারেন্সের জন্য L30-এর ক্যারেক্টারিস্টিক ইকুয়েশন পদ্ধতিই সঠিক টুল, Master Theorem নয়।

প্র ০২ মেমোয়াইজেশন কেন শুধু "কিছুটা দ্রুততর" নয়, বরং সম্পূর্ণ ভিন্ন জটিলতা ক্লাসে ($\Theta(\varphi^n) \to \Theta(n)$) নিয়ে যায়?

নেইভ সংস্করণে একই সাব-প্রবলেম (যেমন $fib(10)$) বহুবার recompute হয় — মোট ইউনিক সাব-প্রবলেম মাত্র $n+1$টি হলেও, রিকার্সিভ কল ট্রি-তে এদের পুনরাবৃত্তি এক্সপোনেনশিয়াল হারে বাড়ে। মেমোয়াইজেশন প্রতিটি সাব-প্রবলেম ঠিক একবার সমাধান করে, ফলাফল সংরক্ষণ করে — তাই মোট কাজ ইউনিক সাব-প্রবলেমের সংখ্যার সমানুপাতিক, যা এখানে লিনিয়ার। এটি একটি "কনস্ট্যান্ট ফ্যাক্টর" উন্নতি নয় — এটি সম্পূর্ণ ভিন্ন জটিলতা ক্লাসে যাওয়া।

প্র ০৩ L40-এর সাথে সংযোগ করে ভাবুন: quicksort-এর রিকারেন্স কেন merge sort-এর মতো একটি নির্দিষ্ট রিকারেন্স নয়?

Merge sort সবসময় ইনপুটকে ঠিক দুটি সমান অর্ধেকে ভাগ করে — তাই এর রিকারেন্স $T(n)=2T(n/2)+\Theta(n)$ ইনপুট-নিরপেক্ষভাবে স্থির। কিন্তু quicksort-এর ভাগ pivot-এর অবস্থানের উপর নির্ভর করে — worst case-এ pivot সবচেয়ে ছোট/বড় হলে ভাগটি হয় $1$ ও $n-1$ (রিকারেন্স $T(n)=T(n-1)+\Theta(n)$, যা সমাধান করলে $\Theta(n^2)$), আর গড় কেসে ভাগ প্রায় সমান হয় (রিকারেন্স $T(n)=2T(n/2)+\Theta(n)$-এর কাছাকাছি, $\Theta(n\log n)$)। তাই quicksort-এর "একটি" রিকারেন্স নেই — এটি ইনপুটের উপর নির্ভরশীল, যা সরাসরি L40-এর আলোচনার সাথে যুক্ত।

অনুশীলন

  1. পরীক্ষা করুন: কোড সেলে n = 25 করে Run চাপুন (সাবধান — এটি একটু সময় নিতে পারে)। নেইভ কল সংখ্যা কতটা বেড়েছে লক্ষ করুন।

    যেহেতু নেইভ ফিবোনাচি $\Theta(\varphi^n)$, $n$ ৫ বাড়লে কল সংখ্যা প্রায় $\varphi^5 \approx 11.1$ গুণ বেড়ে যাবে (কারণ $T(n+5)/T(n) \approx \varphi^5$)। এই দ্রুত বৃদ্ধিই দেখায় কেন naive recursion ছোট $n$-এর বাইরে ব্যবহারযোগ্য নয়।

  2. যাচাই করুন: $T(n)=3T(n/2)+n$ রিকারেন্সের জন্য Master Theorem-এর কোন কেস প্রযোজ্য তা L31-এর পদ্ধতিতে নির্ণয় করুন ($\log_b a$ গণনা করে $f(n)$-এর সাথে তুলনা করুন)।

    এখানে $a=3, b=2$, তাই $\log_2 3 \approx 1.585$। $f(n)=n=n^1$, যা $n^{1.585}$-এর চেয়ে ধীরে বাড়ে — অর্থাৎ $f(n)=O(n^{\log_b a - \epsilon})$ কোনো $\epsilon>0$-এর জন্য (এখানে $\epsilon\approx0.585$)। তাই Master Theorem কেস ১ প্রযোজ্য: $T(n)=\Theta(n^{\log_2 3}) \approx \Theta(n^{1.585})$।

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

পূর্ববর্তী পাঠ
সেরা, গড় ও সবচেয়ে খারাপ কেস বিশ্লেষণ