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

রিকার্শন — নিজেকে ডাকা ফাংশন আর call stack

Recursion — Base Cases, the Call Stack, Recursion Trees, Fast Power & Tower of Hanoi

Taught in Bangla, this lesson teaches recursion through base and recursive cases and the leap of faith. It traces frames on the call stack, draws the recursion tree for fib(4), discusses stack overflow, recursion depth, tail recursion and Python's recursion limit, and works through fast power, Tower of Hanoi, subsets and permutations in C++ and Python, plus converting recursion to iteration.

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

  • যেকোনো রিকার্সিভ সমাধানে base case আর recursive case আলাদা করে চেনা ও লেখা
  • Call stack-এ frame কীভাবে জমে আর খালি হয়, হাতে ধাপে ধাপে trace করা
  • Recursion tree এঁকে মোট কল, সময় আর জায়গার (stack depth) হিসাব করা
  • Tail recursion, stack overflow আর Python-এর recursion limit — কোথায় বিপদ
  • Fast power, Tower of Hanoi, subset/permutation লেখা, আর রিকার্শনকে লুপে রূপান্তর

১ · রিকার্শন কী — নিজের ছোট সংস্করণকে ডাকা

ধরুন কমলাপুর স্টেশনে ট্রেনের টিকিটের লম্বা লাইনে দাঁড়িয়ে আছেন। জানতে চান আপনি লাইনে কত নম্বরে। পেছন ফিরে সবাইকে গোনার উপায় নেই, সামনে অনেক মানুষ। তখন একটি সহজ কৌশল আছে: সামনের জনকে জিজ্ঞেস করুন, “ভাই, আপনি কত নম্বরে?” তিনিও জানেন না, তাই তিনি তাঁর সামনের জনকে একই প্রশ্ন করেন। প্রশ্নটি এভাবে সামনে এগোতে থাকে — যতক্ষণ না একেবারে কাউন্টারের সামনের মানুষটির কাছে পৌঁছায়। তিনি কাউকে জিজ্ঞেস না করেই বলেন, “আমি ১ নম্বর।” এবার উত্তর পেছনে ফিরতে থাকে: “আমি ২”, “আমি ৩”… শেষে আপনার সামনের জন বলেন “আমি ৪১”, আর আপনি বোঝেন আপনি ৪২ নম্বরে।

এটাই recursion (রিকার্শন)। একটি ফাংশন নিজের সমস্যার একটু ছোট সংস্করণ সমাধান করতে নিজেকেই ডাকে, তারপর সেই ছোট উত্তর থেকে নিজের উত্তর বানায়। লক্ষ করুন, এই কৌশল কাজ করে দুটি কারণে:

🛑 Base case — যেখানে থামে

কাউন্টারের সামনের মানুষটি কাউকে জিজ্ঞেস করেন না, সরাসরি উত্তর দেন। একে বলে base case. এটি না থাকলে প্রশ্ন কোনোদিন থামত না।

উত্তর সরাসরি জানা

🔁 Recursive case — সমস্যা ছোট করা

বাকি সবাই সামনের জনকে একই প্রশ্ন করেন (সমস্যা এক মানুষ ছোট), আর উত্তরের সাথে ১ যোগ করেন। একে বলে recursive case.

ছোট উত্তর + নিজের অংশ

গণিতে এর সবচেয়ে পরিচিত উদাহরণ factorial: n! = n × (n − 1)!, আর 0! = 1 (বা 1! = 1). “৫-এর factorial কত?” জানতে হলে আগে জানতে হবে “৪-এর factorial কত?” — একই প্রশ্ন, ছোট আকারে। নিচের pseudocode-এ রিকার্সিভ ফাংশনের সাধারণ কাঠামো আর factorial দেখুন:

Pseudocode (যেকোনো রিকার্সিভ ফাংশনের কাঠামো)

SOLVE(problem):
    if problem is small enough:            // base case
        return the answer directly
    smaller = a smaller version of problem
    part = SOLVE(smaller)                   // recursive call (leap of faith)
    return COMBINE(part, problem)           // build the bigger answer

FACT(n):                                    // n >= 0
    if n <= 1: return 1
    return n * FACT(n - 1)

যেকোনো রিকার্সিভ সমাধান লেখার আগে তিনটি প্রশ্নের উত্তর নিজের কাছে পরিষ্কার রাখুন:

  1. ফাংশনটি ঠিক কী ফেরত দেয়? এক বাক্যে বলুন — যেমন “fact(n) ফেরত দেয় n!”। এই চুক্তিটিই পরে বিশ্বাসের ভিত্তি।
  2. সবচেয়ে ছোট কোন ইনপুটের উত্তর সরাসরি জানি? সেটিই base case. সব সম্ভাব্য ইনপুট যেন শেষমেশ এখানে পৌঁছায়।
  3. সমস্যা প্রতিবার কি সত্যিই ছোট হচ্ছে? n থেকে n − 1, অ্যারের দৈর্ঘ্য অর্ধেক, string-এর একটি অক্ষর কম — কোনো একটি মাপ যেন প্রতিটি কলে কমে। এটিই termination-এর নিশ্চয়তা।

Base case ভুল হলে বিপদ নিঃশব্দে আসে। fact(n)-এ base case যদি শুধু n == 1 হয়, আর কেউ fact(0) ডাকে, তাহলে কল যায় 0 → −1 → −2 … — কোনোদিন 1-এ পৌঁছায় না। প্রোগ্রাম অসীম রিকার্শনে পড়ে শেষে crash করে। তাই base case লিখুন n <= 1, অথবা শুরুতেই ইনপুট যাচাই করুন।

২ · Leap of faith — ছোট কলকে বিশ্বাস করুন

নতুনরা রিকার্শন বুঝতে গিয়ে প্রায়ই মাথার ভেতরে পুরো কল-শিকল চালানোর চেষ্টা করেন: “fact(5) ডাকে fact(4), সেটি ডাকে fact(3), সেটি…” — পাঁচ ধাপ পর্যন্ত চলে, কিন্তু Tower of Hanoi বা permutation-এ কয়েকশো কল মাথায় ধরে রাখা অসম্ভব। অভিজ্ঞ প্রোগ্রামাররা এটি করেন না। তাঁরা একটি মানসিক কৌশল ব্যবহার করেন, যার নাম “leap of faith” — বিশ্বাসের লাফ।

মূল ধারণা

রিকার্সিভ কল লেখার সময় ধরে নিন: ছোট ইনপুটের জন্য ফাংশনটি ইতিমধ্যে ঠিকঠাক কাজ করে। তারপর শুধু দুটি জিনিস যাচাই করুন — (১) base case ঠিক উত্তর দেয়, (২) ছোট উত্তরটি ঠিক ধরে নিলে আপনার বড় উত্তরটি ঠিক হয়। ভেতরে কী ঘটছে, তা নিয়ে আর ভাবতে হবে না।

এটি অন্ধ বিশ্বাস নয় — এর পেছনে আছে mathematical induction, যা আপনি পর্ব ০৩-এ শুদ্ধতার প্রমাণে দেখেছেন। দুটি তুলনা করুন:

Induction-এর প্রমাণরিকার্সিভ ফাংশন
ভিত্তি: সবচেয়ে ছোট n-এর জন্য দাবিটি সত্যBase case সরাসরি ঠিক উত্তর দেয়
অনুমান: n − 1 (বা n-এর চেয়ে ছোট সব মান)-এর জন্য দাবি সত্যLeap of faith: ছোট কল ঠিক উত্তর দেয়
ধাপ: অনুমান থেকে n-এর জন্য দাবি প্রমাণছোট উত্তর থেকে বড় উত্তর তৈরি
উপসংহার: সব n-এর জন্য সত্যসব বৈধ ইনপুটে ফাংশন ঠিক

উদাহরণ — fact-এর শুদ্ধতার প্রমাণ। দাবি: সব n ≥ 0-এর জন্য fact(n) ফেরত দেয় n!।

  • ভিত্তি: n = 0 বা 1 হলে ফাংশন 1 ফেরত দেয়, আর 0! = 1! = 1 — ঠিক।
  • ধাপ: ধরি n ≥ 2, এবং fact(n − 1) ঠিকঠাক (n − 1)! ফেরত দেয় (induction-এর অনুমান)। তাহলে ফাংশন ফেরত দেয় n × (n − 1)! = n! — ঠিক।
  • শেষ হওয়া: প্রতিটি কলে n এক করে কমে, আর n ≤ 1 হলেই থামে — তাই কলের সংখ্যা সসীম।

এই তিন লাইনের প্রমাণ দেখায় কেন leap of faith বৈধ। পরের অংশগুলোতে Tower of Hanoi বা subset তৈরির মতো জটিল রিকার্শনেও আমরা ঠিক এভাবেই ভাবব — “ছোট কাজটি হয়ে গেছে ধরে নিই, এখন আমার অংশটুকু কী?”

অফিসে বড় একটি রিপোর্ট বানাতে হবে। ম্যানেজার নিজে সব না লিখে বলেন, “প্রথম ৯টি অধ্যায় রহিম লিখবে, শেষ অধ্যায়টি আমি লিখে জোড়া দেব।” রহিম কীভাবে ৯টি অধ্যায় লিখবেন, সেটা ম্যানেজার দেখেন না — তিনি বিশ্বাস করেন রহিম ঠিকই লিখবেন। রহিমও একই কৌশলে ৮টি অধ্যায় আরেকজনকে দেন… শেষজন মাত্র একটি অধ্যায় পান, সেটি নিজেই লেখেন (base case). প্রত্যেকে শুধু নিজের অংশটুকু ঠিক করলেই পুরো রিপোর্ট ঠিক হয়।

৩ · Call stack — প্রতিটি কলের নিজের খাতা

leap of faith দিয়ে লেখা সহজ হয়, কিন্তু কম্পিউটার আসলে কীভাবে এটি চালায়? একই ফাংশন একসাথে চারবার “চলছে” — প্রতিটির n আলাদা — তবু গুলিয়ে যায় না কেন?

উত্তর হলো call stack. প্রতিবার কোনো ফাংশন ডাকা হলে মেমরিতে তার জন্য একটি নতুন stack frame তৈরি হয় — যেন প্রতিটি কলের নিজের একটি খাতা। সেই খাতায় লেখা থাকে:

  • এই কলের parameter (যেমন n = 3) আর local ভ্যারিয়েবল,
  • return address — কাজ শেষে কাকে, কোন লাইনে উত্তর ফেরত দিতে হবে,
  • অর্ধেক-করা কাজের অবস্থা — যেমন “ফেরত এলে 3 দিয়ে গুণ করতে হবে।”

খাতাগুলো রাখা হয় একটির ওপর আরেকটি — ঠিক অফিসের টেবিলে ফাইলের স্তূপের মতো। নতুন কল মানে স্তূপের ওপরে নতুন খাতা (push), কল শেষ মানে ওপরের খাতাটি সরানো (pop). যে শেষে এসেছে, সে আগে যায় — এটিই stack ডেটা স্ট্রাকচারের নিয়ম, যা পর্ব ১৩-এ বিস্তারিত দেখবেন।

নিচের ডায়াগ্রামে fact(4) চলার সময় stack কীভাবে বাড়ে আর কমে, দেখুন। “পরে” চাপুন:

1long long fact(int n) {2    if (n <= 1) return 1;      // base case3    return n * fact(n - 1);    // recursive case4}56int main() {7    cout << fact(4) << '\n';8}
ওপরের frame-এর অবস্থা
n (ওপরের frame)
ফেরত মান
stack-এ fact-frame
Call stackওপরের লাইন = এখন যে কল চলছে

                    
  1. প্রথম push. main ডাকল fact(4). নতুন frame তৈরি হলো, তাতে n = 4. শর্ত n <= 1 মিথ্যা, তাই লাইন ৩-এ গিয়ে fact(3) ডাকতে হবে। গুণটা এখনো বাকি — frame-টি “4 * ?” লিখে অপেক্ষায় থাকল।
  2. দ্বিতীয় push. fact(3)-এর নিজের frame, নিজের n = 3. খেয়াল করুন, নিচের frame-এর n = 4 মুছে যায়নি — প্রতিটি কলের n আলাদা খাতায় আলাদা।
  3. তৃতীয় push. fact(2) এলো, n = 2. সে-ও fact(1) ডেকে অপেক্ষায়। এখন stack-এ তিনটি fact-frame.
  4. Base case. fact(1)-এ n = 1, শর্ত n <= 1 সত্য — লাইন ২ সরাসরি 1 ফেরত দেয়। এই মুহূর্তে stack সবচেয়ে গভীর: চারটি fact-frame. এটিই এই কলের recursion depth.
  5. প্রথম pop. fact(1)-এর frame সরে গেল। fact(2) তার অপেক্ষার জায়গা থেকে আবার চলল: 2 × 1 = 2, ফেরত দিল 2.
  6. দ্বিতীয় pop. fact(3) পেল 2, হিসাব করল 3 × 2 = 6, ফেরত দিল 6. উত্তর এখন নিচের দিকে নামছে — ঠিক টিকিটের লাইনে উত্তর পেছনে ফেরার মতো।
  7. তৃতীয় pop. fact(4) পেল 6, হিসাব 4 × 6 = 24, ফেরত 24. stack-এ এখন শুধু একটি fact-frame, সেটিও সরে যাচ্ছে।
  8. main আবার চলছে। সব fact-frame খালি। main তার return address-এ ফিরে 24 ছাপল। মোট কল ৪টি, সর্বোচ্চ গভীরতা ৪ — তাই সময় Θ(n), আর stack-এ বাড়তি জায়গা Θ(n).

এই ছবি থেকে রিকার্শনের দুটি খরচ পরিষ্কার হয়:

  • সময় = মোট কলের সংখ্যা × প্রতিটি কলের ভেতরের কাজ। fact(n)-এ n টি কল, প্রতিটিতে O(1) কাজ, তাই Θ(n).
  • জায়গা = সর্বোচ্চ গভীরতা × প্রতিটি frame-এর আকার। একসাথে সর্বোচ্চ n টি frame জমে, তাই বাড়তি জায়গা Θ(n) — যদিও একটি লুপ দিয়ে লিখলে লাগত মাত্র O(1). এই লুকানো খরচটি পরীক্ষায় প্রায়ই জিজ্ঞেস করা হয়।

৪ · fib(4) — recursion tree আর একই কাজ বারবার

factorial-এ প্রতিটি কল ঠিক একবার নিজেকে ডাকে, তাই কলগুলো একটি সরলরেখা। কিন্তু অনেক রিকার্শনে একটি কল দুই বা ততোধিকবার নিজেকে ডাকে। সবচেয়ে বিখ্যাত উদাহরণ Fibonacci সংখ্যা: F(0) = 0, F(1) = 1, আর F(n) = F(n − 1) + F(n − 2). সংজ্ঞাটি হুবহু কোডে লিখলে:

long long fib(int n) {
    if (n <= 1) return n;                // base cases: fib(0)=0, fib(1)=1
    return fib(n - 1) + fib(n - 2);      // two recursive calls
}

দেখতে নিরীহ। কিন্তু প্রতিটি কল দুটি নতুন কল তৈরি করে, তাই কলগুলো আর রেখা নয় — একটি গাছ। একে বলে recursion tree: প্রতিটি কল একটি node, তার ভেতরের কলগুলো তার child. নিচে fib(4)-এর গাছ আর একই সময়ে call stack পাশাপাশি দেখুন। খেয়াল করুন — গাছ চওড়া হলেও stack কখনো গাছের উচ্চতার বেশি গভীর হয় না।

Recursion tree… = অপেক্ষায়, ◀ = এখন চলছে

                
গণনা
মোট কল এ পর্যন্ত
stack-এর গভীরতা
Call stackওপরে = এখন

                    
  1. শুরু। main ডাকল fib(4). এটি আগে বাঁ দিকের কাজ fib(3) শেষ করবে, তারপর ডানের fib(2). মোট কল ১, গভীরতা ১.
  2. বাঁ দিকে নামা। fib(3)-এর frame push হলো। fib(4) “… + ?” অবস্থায় অপেক্ষায়। কল ২টি।
  3. আরও নিচে। fib(2) push হলো; এটিও প্রথমে বাঁয়ের fib(1) ডাকবে। গভীরতা ৩.
  4. প্রথম base case. fib(1) সরাসরি 1 ফেরত দেয়। stack এখন সবচেয়ে গভীর — ৪টি fib-frame, যা গাছের উচ্চতার সমান। কল ৪টি।
  5. পাশের শাখা। fib(1) pop হলো; fib(2) এবার ডানের fib(0) ডাকল, যা 0 ফেরত দেয়। গভীরতা আবার ৪ — নতুন frame পুরোনোটির জায়গাতেই বসেছে। কল ৫টি।
  6. প্রথম যোগফল। fib(2) = 1 + 0 = 1, pop হলো। fib(3) এবার ডানের fib(1) ডাকল, যা 1 ফেরত দেয়। কল ৬টি, গভীরতা ৩.
  7. fib(3) শেষ। fib(3) = 1 + 1 = 2. দুটি frame-ই সরে গেছে; stack-এ শুধু fib(4), যে এখন ডান দিকের কাজ শুরু করবে।
  8. একই কাজ আবার! fib(4) ডাকল fib(2) — অথচ ধাপ ৬-এ আমরা fib(2) = 1 হিসাব করেই ফেলেছিলাম। সরল রিকার্শনের কোনো স্মৃতি নেই, তাই পুরো হিসাব নতুন করে শুরু। কল ৭টি।
  9. পুনরাবৃত্তি চলছে। দ্বিতীয় fib(2) আবার fib(1) ডাকল → 1. কল ৮টি, গভীরতা ৩.
  10. শেষ পাতা। তারপর fib(0) → 0. কল ৯টি। এটি গাছের শেষ node.
  11. উত্তর। fib(2) = 1, তারপর fib(4) = 2 + 1 = 3. মোট কল ৯টি, অথচ ভিন্ন ভিন্ন প্রশ্ন ছিল মাত্র ৫টি (fib(0) থেকে fib(4)). বাড়তি ৪টি কল শুধু পুরোনো কাজের পুনরাবৃত্তি। সর্বোচ্চ stack গভীরতা ছিল ৪ = n.

fib(4)-এ অপচয় সামান্য। কিন্তু n বাড়লে কী হয়? ধরি C(n) = fib(n) ডাকলে মোট কলের সংখ্যা। তাহলে C(0) = C(1) = 1, আর C(n) = 1 + C(n − 1) + C(n − 2). হিসাব করলে দেখা যায় C(n) = 2·F(n + 1) − 1 — অর্থাৎ কলের সংখ্যাও Fibonacci-র মতোই বাড়ে:

nF(n)fib(n)-এ মোট কলসর্বোচ্চ stack গভীরতা
4394
105517710
206,76521,89120
30832,0402,692,53730
40102,334,155331,160,28140

n মাত্র ১০ করে বাড়লে কলের সংখ্যা প্রায় ১২০ গুণ হয়ে যায়। fib(40)-এ ৩৩ কোটির বেশি কল — একটি সাধারণ ল্যাপটপে C++-এ কয়েক সেকেন্ড, Python-এ আধা মিনিট বা তার বেশি। fib(60)-এ কল কয়েক ট্রিলিয়ন — C++-এও ঘণ্টার পর ঘণ্টা, আর fib(100) হলে হাজার হাজার বছর! এটি exponential time: সময় Θ(φⁿ), যেখানে φ ≈ 1.618 (golden ratio). অথচ stack-এর জায়গা মাত্র Θ(n), কারণ যেকোনো মুহূর্তে শুধু একটি শাখা (মূল থেকে একটি পাতা পর্যন্ত পথ) stack-এ থাকে। সময় আর জায়গা এখানে সম্পূর্ণ আলাদা গল্প বলে।

সমাধানের আভাস

সমস্যা রিকার্শন নয় — সমস্যা হলো একই উপসমস্যা বারবার সমাধান করা। প্রথমবার fib(k) হিসাব করে একটি টেবিলে লিখে রাখুন; পরের বার শুধু টেবিল থেকে পড়ুন। একে বলে memoization. তাহলে প্রতিটি k-এর জন্য মাত্র একবার কাজ হয়: সময় Θ(n). এই সরল ধারণাটিই dynamic programming-এর ভিত্তি — পর্ব ৫২-তে এটি নিয়ে পুরো আলোচনা থাকবে।

আরও গভীরে: কেন ঠিক φⁿ? C(n) + 1 = 2·F(n + 1), আর Binet-এর সূত্র অনুযায়ী F(n) হলো φⁿ/√5-এর সবচেয়ে কাছের পূর্ণসংখ্যা — তাই C(n) = Θ(φⁿ) ≈ Θ(1.618ⁿ)। অনেক বই ঢিলেঢালাভাবে “O(2ⁿ)” লেখে — সেটি সত্য ঊর্ধ্বসীমা, কিন্তু tight নয়। আর Fibonacci বের করার সবচেয়ে দ্রুত উপায় রিকার্শন বা লুপ কোনোটিই নয়: 2×2 matrix-এর ঘাত, যা অংশ ৬-এর fast power দিয়ে O(log n) গুণে হয়।

৫ · Stack overflow, recursion depth আর tail recursion

Call stack অসীম নয়। প্রতিটি প্রোগ্রাম শুরুতেই stack-এর জন্য একটি নির্দিষ্ট জায়গা পায় — Linux-এ main thread-এর জন্য সাধারণত 8 MB, Windows-এ ডিফল্ট 1 MB. প্রতিটি frame যদি ধরুন ৫০ বাইট নেয়, তাহলে Windows-এ মোটামুটি বিশ হাজারের মতো গভীর রিকার্শনেই জায়গা শেষ। এই অবস্থার নাম stack overflow.

C++-এ কী হয়

কোনো সতর্কবার্তা ছাড়াই প্রোগ্রাম crash করে — Linux-এ Segmentation fault, Windows-এ 0xC00000FD (stack overflow) কোড দিয়ে বন্ধ। কনটেস্টে এটি দেখায় “Runtime Error”.

সীমা: OS-এর stack size

Python-এ কী হয়

Python নিজেই একটি recursion limit রাখে, ডিফল্ট 1000 (sys.getrecursionlimit())। গভীরতা এটি ছাড়ালে পরিষ্কার RecursionError: maximum recursion depth exceeded আসে।

সীমা: প্রায় 1000 frame

sys.setrecursionlimit-এর ফাঁদ। Python-এ sys.setrecursionlimit(10**6) লিখলে সীমা বাড়ে, কিন্তু আসল stack বাড়ে না। Python-এর প্রতিটি frame-এর পেছনে interpreter-এর নিজের (C ভাষার) stack-ও খরচ হয়, তাই সীমা অনেক বাড়িয়ে গভীর রিকার্শন চালালে পরিষ্কার error-এর বদলে পুরো interpreter হঠাৎ crash করতে পারে। কনটেস্টে অনেকে এর সাথে threading.stack_size বাড়িয়ে নতুন thread-এ কোড চালান। নিরাপদ পথ: গভীরতা কয়েক হাজার ছাড়ানোর সম্ভাবনা থাকলে Python-এ লুপ বা নিজের stack ব্যবহার করুন (অংশ ৯)।

Tail recursion — শেষ কাজটিই যদি ডাক হয়

fact-এ return n * fact(n - 1); লাইনটি খেয়াল করুন। রিকার্সিভ কল ফেরার পরেও একটি কাজ বাকি থাকে — n দিয়ে গুণ। তাই frame-টিকে অপেক্ষায় রাখতেই হয়। কিন্তু যদি ডাকটাই একেবারে শেষ কাজ হয়?

// tail-recursive factorial: acc carries the partial product
long long fact_tail(int n, long long acc = 1) {
    if (n <= 1) return acc;
    return fact_tail(n - 1, acc * n);   // nothing left to do after the call
}

এখানে গুণটি আগেই acc-এ করা হয়ে গেছে; রিকার্সিভ কল যা ফেরত দেবে, সেটিই হুবহু ফেরত যাবে। একে বলে tail recursion. এই অবস্থায় পুরোনো frame-এর আর কোনো কাজ নেই, তাই compiler চাইলে নতুন frame না বানিয়ে পুরোনোটিই আবার ব্যবহার করতে পারে — রিকার্শন কার্যত একটি লুপে পরিণত হয়, জায়গা লাগে O(1). এর নাম tail call optimization (TCO).

ভাষাTail call optimizationব্যবহারিক মানে
C / C++ (GCC, Clang)-O2-এ সাধারণত করে, কিন্তু standard-এ নিশ্চয়তা নেই-O0 (debug build)-এ একই কোড stack overflow করতে পারে — এর ওপর ভরসা করে গভীর রিকার্শন লিখবেন না
Python (CPython)একেবারেই করে না — ইচ্ছাকৃত সিদ্ধান্তfact_tail(5000)-ও RecursionError দেবে; tail form লিখে কোনো লাভ নেই
JavaJVM সাধারণত করে নাগভীর রিকার্শনে StackOverflowError
Schemeভাষার নিয়মেই বাধ্যতামূলকসেখানে লুপের বদলে tail recursion-ই স্বাভাবিক

সারকথা: tail recursion একটি সুন্দর ধারণা, এবং “এটি একটি লুপের সমান” — এই উপলব্ধিটাই আসল লাভ। কিন্তু C++ বা Python-এ গভীরতার সমস্যার সমাধান হিসেবে এর ওপর ভরসা না করে সরাসরি লুপ লিখুন।

৬ · Fast power — অর্ধেক করে O(log n)

এবার এমন একটি রিকার্শন দেখি, যা লুপের চেয়ে দ্রুত সমাধান দেয়। xe বের করতে সরল লুপে e বার গুণ লাগে। e = 1018 হলে সেটি অসম্ভব। কিন্তু একটি পর্যবেক্ষণ সব বদলে দেয়:

xe = (xe/2)²   (e জোড় হলে)
xe = (x⌊e/2⌋)² × x   (e বিজোড় হলে)

অর্থাৎ 313 জানতে শুধু 36 জানলেই চলে; 36 জানতে 33; 33 জানতে 31; আর 30 = 1 হলো base case. প্রতিটি ধাপে ঘাত অর্ধেক হয়ে যায়। এই কৌশলের নাম binary exponentiation বা fast power. Leap of faith দিয়ে ভাবুন: “power(x, e/2) ঠিক উত্তর দেবে — আমি শুধু সেটিকে বর্গ করব, বিজোড় হলে আরেকবার x দিয়ে গুণ করব।” নিচে 313-এর হিসাব ধাপে ধাপে:

1long long power(long long x, int e) {2    ++calls;3    if (e == 0) return 1;              // base case4    long long half = power(x, e / 2);  // ONE call5    long long res = half * half;6    if (e % 2 == 1) res *= x;7    return res;8}
ওপরের frame
কল
half
res
calls
Call stackওপরে = এখন

                    
  1. শুরু: e = 13. base case নয়। লাইন ৪-এ একটিমাত্র রিকার্সিভ কল: power(3, 13/2) = power(3, 6). integer ভাগে 13/2 = 6.
  2. e = 6. আবার অর্ধেক: power(3, 3) ডাকা হলো। কল ২টি।
  3. e = 3. 3/2 = 1, তাই power(3, 1). কল ৩টি।
  4. e = 1. 1/2 = 0, তাই power(3, 0). কল ৪টি, stack-এ ৪টি frame অপেক্ষায়।
  5. Base case: e = 0. লাইন ৩ সরাসরি 1 ফেরত দেয় — 3⁰ = 1. মোট কল ৫টি, আর বাড়বে না। এখন উত্তর নিচে নামবে।
  6. e = 1 ফিরল। half = 1, res = 1 × 1 = 1; e বিজোড়, তাই res × 3 = 3. অর্থাৎ 3¹ = 3.
  7. e = 3 ফিরল। half = 3, res = 9; বিজোড়, তাই 9 × 3 = 27 = 3³.
  8. e = 6 ফিরল। half = 27, res = 27 × 27 = 729; e জোড়, তাই বাড়তি গুণ নেই। 3⁶ = 729.
  9. e = 13 ফিরল। half = 729, res = 729 × 729 = 531441; বিজোড়, তাই × 3 = 1594323 = 3¹³. সরল লুপে লাগত ১৩টি গুণ; এখানে ৫টি কলে মোট ৭টি গুণ। e যত বড়, পার্থক্য তত নাটকীয়।

বিশ্লেষণ। প্রতিটি কলে e অন্তত অর্ধেক হয়, আর প্রতিটি কলে কাজ O(1) (একটি বা দুটি গুণ)। e ≥ 1 হলে মোট কল ঠিক ⌊log₂ e⌋ + 2 টি। তাই সময় Θ(log e), আর stack গভীরতাও Θ(log e). পর্ব ০৮-এ এটিকে recurrence হিসেবে লিখব: T(e) = T(e/2) + O(1) ⇒ T(e) = O(log e). এবার চালিয়ে দেখুন — তিনটি ভিন্ন ইনপুটে কতগুলো কল লাগে:

#include <iostream>
#include <utility>
using namespace std;

int calls = 0;

long long power(long long x, int e) {
    ++calls;
    if (e == 0) return 1;                  // base case
    long long half = power(x, e / 2);      // ONE recursive call
    long long res = half * half;
    if (e % 2 == 1) res *= x;              // odd exponent: one extra x
    return res;
}

int main() {
    for (auto [x, e] : {pair{3LL, 13}, pair{2LL, 62}, pair{10LL, 18}}) {
        calls = 0;
        long long r = power(x, e);
        cout << x << "^" << e << " = " << r << "  calls = " << calls
             << "  (naive loop: " << e << " multiplications)\n";
    }
}

আউটপুট:

3^13 = 1594323  calls = 5  (naive loop: 13 multiplications)
2^62 = 4611686018427387904  calls = 7  (naive loop: 62 multiplications)
10^18 = 1000000000000000000  calls = 6  (naive loop: 18 multiplications)

কলের সংখ্যা ⌊log₂ e⌋ + 2 সূত্র মেনে চলে: e = 13 → 3 + 2 = 5, e = 62 → 5 + 2 = 7, e = 18 → 4 + 2 = 6. প্রতিটি নতুন হিসাবের আগে calls = 0 করা জরুরি — না করলে আগের গণনা যোগ হয়ে ভুল সংখ্যা দেখাত (global counter-এর পরিচিত ভুল)। আরেকটি সতর্কতা: long long-এ সর্বোচ্চ মান প্রায় 9.2 × 1018, তাই 263 বা 1019 চাইলে overflow হবে। বাস্তবে এই কারণেই fast power প্রায় সবসময় % mod সহ লেখা হয় (পর্ব ৬২)।

calls = 0

def power(x, e):
    global calls
    calls += 1
    if e == 0:                       # base case
        return 1
    half = power(x, e // 2)          # ONE recursive call
    res = half * half
    if e % 2 == 1:                   # odd exponent: one extra x
        res *= x
    return res

for x, e in [(3, 13), (2, 62), (10, 18)]:
    calls = 0
    r = power(x, e)
    print(f"{x}^{e} = {r}  calls = {calls}  (naive loop: {e} multiplications)")

আউটপুট C++-এর সাথে হুবহু এক:

3^13 = 1594323  calls = 5  (naive loop: 13 multiplications)
2^62 = 4611686018427387904  calls = 7  (naive loop: 62 multiplications)
10^18 = 1000000000000000000  calls = 6  (naive loop: 18 multiplications)

Python-এ integer-এর কোনো ঊর্ধ্বসীমা নেই, তাই 21000-ও ঠিকঠাক আসবে — কিন্তু তখন প্রতিটি গুণ আর O(1) নয়, সংখ্যা যত বড়, গুণ তত ধীর। // হলো integer ভাগ (C++-এর e / 2-এর সমান)। Python-এ এই কাজের জন্য তৈরি ফাংশনও আছে: pow(x, e, mod), যা ভেতরে একই কৌশল ব্যবহার করে।

একটি ছোট পরিবর্তন, বিশাল পার্থক্য। কেউ যদি লেখেন return power(x, e/2) * power(x, e/2); — অর্থাৎ half ভ্যারিয়েবলে না রেখে দুবার ডাকেন — তাহলে প্রতিটি কল দুটি কল তৈরি করে, recursion tree আবার চওড়া হয়ে যায়, আর মোট কাজ হয় Θ(e): সব সুবিধা শেষ! AI-এর লেখা কোডেও এই ভুল মাঝে মাঝে দেখা যায়।

৭ · Tower of Hanoi — রিকার্শনের সবচেয়ে সুন্দর ধাঁধা

১৮৮৩ সালে ফরাসি গণিতবিদ Édouard Lucas একটি খেলনা-ধাঁধা বাজারে আনেন, “N. Claus de Siam” ছদ্মনামে (নামটি “Lucas d'Amiens”-এর অক্ষর উলটপালট করে বানানো)। সাথে ছিল একটি কাল্পনিক কিংবদন্তি: এক মন্দিরে পুরোহিতেরা ৬৪টি সোনার চাকতি এক খুঁটি থেকে আরেক খুঁটিতে সরাচ্ছেন — কাজ শেষ হলেই পৃথিবী ধ্বংস হবে। চিন্তার কিছু নেই; একটু পরেই দেখবেন কেন।

Tower of Hanoi-এর নিয়ম তিনটি। তিনটি খুঁটি A, B, C. A-তে n টি চাকতি, নিচে সবচেয়ে বড়, ওপরে সবচেয়ে ছোট। লক্ষ্য সবগুলো C-তে নেওয়া। (১) একবারে একটি চাকতি সরানো যায়, (২) শুধু ওপরের চাকতিটি সরানো যায়, (৩) বড় চাকতি কখনো ছোটটির ওপর রাখা যাবে না।

সরাসরি ভাবলে মাথা ঘুরে যায়। কিন্তু leap of faith দিয়ে ভাবুন — সবচেয়ে বড় চাকতিটির (n নম্বর) কথা ভাবুন। এটিকে C-তে নিতে হলে তার ওপরের n − 1 টি চাকতি অবশ্যই B-তে থাকতে হবে (A-তে থাকলে বড়টি ঢাকা, C-তে থাকলে বড়টি সেখানে বসতে পারবে না)। তাহলে সমাধান তিন লাইনের:

  1. ওপরের n − 1 টি চাকতি A থেকে B-তে নিন (C-কে সাহায্যকারী ধরে) — এটি একই সমস্যা, এক চাকতি ছোট: বিশ্বাস করুন।
  2. সবচেয়ে বড় চাকতিটি A থেকে C-তে নিন — একটি চাল।
  3. B-র n − 1 টি চাকতি C-তে নিন (A-কে সাহায্যকারী ধরে) — আবার একই সমস্যা, আবার বিশ্বাস।

Base case: শূন্য চাকতি হলে কিছুই করার নেই। নিচে n = 3-এর পুরো সমাধান দেখুন। “চলমান কল” সারিতে খেয়াল করুন — কোন রিকার্সিভ কল কোন চালটি দিচ্ছে:

খুঁটির অবস্থা (বাঁ থেকে ডানে = নিচ থেকে ওপরে; 3 = সবচেয়ে বড়)
খুঁটি A
খুঁটি B
খুঁটি C
চাল সংখ্যা
চলমান কলhanoi(n, from→to)

            
  1. শুরু। A-তে তিনটি চাকতি (নিচে 3, ওপরে 1). hanoi(3, A→C)-এর প্রথম কাজ: ওপরের দুটিকে B-তে সরানো — অর্থাৎ hanoi(2, A→B) ডাকা।
  2. চাল ১: disk 1, A → C. hanoi(2, A→B)-কেও প্রথমে নিজের ওপরের একটি চাকতি সরাতে হবে — তার “সাহায্যকারী” খুঁটি C-তে। তাই hanoi(1, A→C). খেয়াল করুন, ছোট চাকতি প্রথমে C-তে যাচ্ছে, যদিও শেষ লক্ষ্য সেখানে নয় — রিকার্শন নিজেই ঠিক করে দেয়।
  3. চাল ২: disk 2, A → B. hanoi(2, A→B)-এর মাঝের ধাপ — তার “বড় চাকতি” (disk 2) সরাসরি B-তে।
  4. চাল ৩: disk 1, C → B. hanoi(2, A→B)-এর শেষ ধাপ: hanoi(1, C→B). এখন B-তে 2 আর 1 — উপরের দুটি চাকতি সরানোর কাজ শেষ, ঠিক যেমন “বিশ্বাস” করেছিলাম।
  5. চাল ৪: disk 3, A → C. এবার hanoi(3, A→C) নিজে সবচেয়ে বড় চাকতিটি সরায়। এটি ঠিক মাঝের চাল — ৭টির মধ্যে ৪ নম্বর।
  6. চাল ৫: disk 1, B → A. শেষ ধাপ শুরু: hanoi(2, B→C), এখন A হলো সাহায্যকারী। প্রথমে hanoi(1, B→A).
  7. চাল ৬: disk 2, B → C. disk 2 বসল disk 3-এর ওপর — বড়র ওপর ছোট, নিয়ম ঠিক আছে।
  8. চাল ৭: disk 1, A → C. সমাধান সম্পূর্ণ: C-তে 3 2 1. মোট চাল ৭ = 2³ − 1. একটু খেয়াল করলে দেখবেন disk 1 সরেছে চাল ১, ৩, ৫, ৭-এ — প্রতি দ্বিতীয় চালে; disk 2 চাল ২ ও ৬-এ; disk 3 শুধু চাল ৪-এ।

চাল কতগুলো? ধরি T(n) = n চাকতিতে চালের সংখ্যা। তিন ধাপের সমাধান থেকে T(0) = 0 এবং T(n) = 2·T(n − 1) + 1. দাবি: T(n) = 2ⁿ − 1. Induction দিয়ে প্রমাণ — ভিত্তি: T(0) = 0 = 2⁰ − 1. ধাপ: T(n) = 2(2ⁿ⁻¹ − 1) + 1 = 2ⁿ − 1. ✔

এর চেয়ে কম কি সম্ভব? না — আর এটিও induction দিয়ে প্রমাণ করা যায়। যেকোনো সমাধানে সবচেয়ে বড় চাকতিটি অন্তত একবার সরাতেই হবে। প্রথমবার সরানোর মুহূর্তে বাকি n − 1 টি চাকতি তৃতীয় খুঁটিতে জড়ো থাকতে হবে, সেখানে নিতে অন্তত T(n − 1) চাল লাগে। বড়টিকে শেষবার সরানোর পর ওগুলোকে আবার তার ওপরে আনতে আরও অন্তত T(n − 1) চাল। তাই যেকোনো সমাধানে ≥ 2·T(n − 1) + 1 চাল — রিকার্সিভ সমাধানটিই সর্বোত্তম।

এবার আসল কোড — চালগুলো ছাপবে আর গুনবে:

#include <iostream>
using namespace std;

int moves = 0;

void hanoi(int n, char from, char to, char via) {
    if (n == 0) return;                 // base case: nothing to move
    hanoi(n - 1, from, via, to);        // 1) move n-1 disks out of the way
    ++moves;                            // 2) move the biggest disk
    cout << "Move disk " << n << ": " << from << " -> " << to << '\n';
    hanoi(n - 1, via, to, from);        // 3) put the n-1 disks back on top
}

int main() {
    hanoi(3, 'A', 'C', 'B');
    cout << "Total moves: " << moves << " (2^3 - 1 = " << (1 << 3) - 1 << ")\n";
}

আউটপুট:

Move disk 1: A -> C
Move disk 2: A -> B
Move disk 1: C -> B
Move disk 3: A -> C
Move disk 1: B -> A
Move disk 2: B -> C
Move disk 1: A -> C
Total moves: 7 (2^3 - 1 = 7)

ঠিক ওপরের ডায়াগ্রামের সাত চাল। ফাংশনের ভেতরে কোথাও “কোন চাকতি কোথায় আছে” তার হিসাব নেই — খুঁটির ভূমিকা (from, to, via) প্রতিটি কলে অদলবদল হয়, বাকিটা রিকার্শন সামলায়। সময় Θ(2ⁿ) — যেহেতু চালই 2ⁿ − 1 টি, এর চেয়ে দ্রুত কোনো অ্যালগরিদম সম্ভব নয়। stack গভীরতা মাত্র n + 1. hanoi(3, ...)-এর বদলে hanoi(4, ...) লিখে চালান — ১৫টি চাল আসবে।

moves = 0

def hanoi(n, src, dst, via):
    global moves
    if n == 0:                       # base case: nothing to move
        return
    hanoi(n - 1, src, via, dst)      # 1) move n-1 disks out of the way
    moves += 1                       # 2) move the biggest disk
    print(f"Move disk {n}: {src} -> {dst}")
    hanoi(n - 1, via, dst, src)      # 3) put the n-1 disks back on top

hanoi(3, 'A', 'C', 'B')
print(f"Total moves: {moves} (2^3 - 1 = {2**3 - 1})")

আউটপুট C++-এর সাথে হুবহু এক:

Move disk 1: A -> C
Move disk 2: A -> B
Move disk 1: C -> B
Move disk 3: A -> C
Move disk 1: B -> A
Move disk 2: B -> C
Move disk 1: A -> C
Total moves: 7 (2^3 - 1 = 7)

Python-এ from একটি সংরক্ষিত শব্দ (keyword), তাই নাম দেওয়া হয়েছে src, dst. ফাংশনের ভেতর থেকে বাইরের moves বদলাতে global moves লিখতে হয় — না লিখলে UnboundLocalError আসত। এখানে গভীরতা মাত্র n + 1, তাই Python-এর ১০০০-এর সীমা কোনো সমস্যা নয়; সমস্যা হবে সময়ে — n = 30 হলেই প্রায় একশো কোটি চাল।

৮ · আরও ক্লাসিক — উল্টানো, যোগফল, subset আর permutation

রিকার্শনের আসল দক্ষতা হলো সমস্যার ভেতরে “নিজের ছোট সংস্করণ” খুঁজে পাওয়া। কয়েকটি ক্লাসিক ধাঁচ দেখি — প্রতিটিতে প্রশ্ন একটাই: কোন মাপটি ছোট হচ্ছে, আর base case কী?

String উল্টানো আর অ্যারের যোগফল

// reverse s[i..j] in place: swap the ends, recurse on the middle
void rev(string& s, int i, int j) {
    if (i >= j) return;                  // 0 or 1 character left
    swap(s[i], s[j]);
    rev(s, i + 1, j - 1);
}

// sum of a[0..n-1] = sum of a[0..n-2] + a[n-1]
long long sum(const vector<int>& a, int n) {
    if (n == 0) return 0;                // empty array
    return sum(a, n - 1) + a[n - 1];
}

rev-এ প্রতি কলে string-এর দুই প্রান্ত থেকে একটি করে অক্ষর কমে: সময় Θ(n), stack গভীরতা প্রায় n/2. sum-এ প্রতি কলে অ্যারে এক ঘর ছোট: সময় Θ(n), গভীরতা n — তাই দশ লাখ ঘরের অ্যারেতে এটি stack overflow ঘটাতে পারে, যেখানে একটি সাধারণ লুপ কোনো সমস্যা ছাড়াই চলে।

দুটি কৌশল মনে রাখুন। (১) Python-এ অনেকে লেখেন return rev(s[1:]) + s[0] — দেখতে সুন্দর, কিন্তু প্রতিটি s[1:] একটি নতুন string কপি করে, তাই মোট সময় Θ(n²)! index পাঠানো (i, j) এই কপি এড়ায়। (২) যোগফলকে “শেষ ঘর বাদে বাকিটা” না ভেঙে “বাঁ অর্ধেক + ডান অর্ধেক” ভাঙলে কাজ একই থাকে (Θ(n)), কিন্তু stack গভীরতা নেমে আসে Θ(log n)-এ। ভাগ করে জয় করার (divide and conquer) এই ধারণা পর্ব ২১-এর merge sort-এর প্রাণ।

সব subset তৈরি — প্রতিটি উপাদানে দুটি সিদ্ধান্ত

{1, 2, 3}-এর সব subset চাই। প্রতিটি উপাদানের সামনে দুটি পথ: “নেব না” বা “নেব”। প্রথম উপাদানের সিদ্ধান্ত নিয়ে বাকি উপাদানের জন্য একই প্রশ্ন রিকার্শনে পাঠাই — সমস্যা এক উপাদান ছোট। সব উপাদানের সিদ্ধান্ত হয়ে গেলে (base case) একটি subset সম্পূর্ণ।

a = [1, 2, 3]

def subsets(i, cur):
    if i == len(a):                  # base case: every element decided
        print(cur)
        return
    subsets(i + 1, cur)              # choice 1: skip a[i]
    subsets(i + 1, cur + [a[i]])     # choice 2: take a[i]

subsets(0, [])   # [] [3] [2] [2, 3] [1] [1, 3] [1, 2] [1, 2, 3]

এর recursion tree একটি পূর্ণ binary tree: উচ্চতা n, পাতা 2ⁿ টি — প্রতিটি পাতা একটি subset. প্রতিটি subset ছাপতে O(n), তাই মোট সময় Θ(n·2ⁿ), stack গভীরতা n + 1. এখানে সূচকীয় সময় কোনো ভুল নয় — উত্তরই 2ⁿ টি।

সব permutation — প্রতিটি ঘরে কে বসবে

// print every ordering of s[i..]; s[0..i-1] is already fixed
void permute(string& s, int i) {
    if (i == (int)s.size()) { cout << s << '\n'; return; }
    for (int j = i; j < (int)s.size(); ++j) {
        swap(s[i], s[j]);      // choose s[j] for position i
        permute(s, i + 1);     // arrange the rest (leap of faith)
        swap(s[i], s[j]);      // undo the choice
    }
}
// permute("abc") prints: abc acb bac bca cba cab

ঘর i-তে কে বসবে, সেটি একে একে বেছে নিয়ে বাকি ঘরগুলো রিকার্শনকে দিই। পাতা n! টি, মোট সময় Θ(n·n!)। লক্ষ করুন শেষ লাইনটি: রিকার্শন থেকে ফিরে এসে নিজের সিদ্ধান্ত ফিরিয়ে নেওয়া (undo). “চেষ্টা করো, ফিরে এসে আগের অবস্থায় ফেরাও, পরের বিকল্প চেষ্টা করো” — এই ধাঁচের নাম backtracking. পর্ব ৫৬-এ এর সাথে “অচল পথ আগেভাগে কেটে ফেলা” যোগ করে N-queens আর subset sum সমাধান করব।

আরেকটি বৈচিত্র্যও চিনে রাখুন: mutual recursion, যেখানে দুটি ফাংশন একে অপরকে ডাকে — যেমন গাণিতিক expression পার্স করতে expr() ডাকে term()-কে, আর বন্ধনী পেলে term() আবার expr()-কে।

৯ · Recursion বনাম iteration — নিজের stack দিয়ে রূপান্তর

প্রতিটি লুপকে রিকার্শনে লেখা যায়, আর প্রতিটি রিকার্শনকে লুপে — কারণ রিকার্শন আসলে “compiler যে stack সামলায়” তার ওপর চলা একটি লুপ। কখন কোনটি?

বিষয়রিকার্শনIteration (লুপ)
কখন স্বাভাবিকসমস্যা নিজেই গাছের মতো: tree, divide and conquer, backtracking, DFSসরল ক্রম: অ্যারে ঘোরা, গণনা, factorial, fib-এর টেবিল
বাড়তি জায়গাগভীরতা × frame-এর আকারসাধারণত O(1)
গতিপ্রতিটি কলে frame তৈরির সামান্য খরচসামান্য দ্রুত
ঝুঁকিstack overflow, Python-এ 1000-এর সীমাoff-by-one, অসীম লুপ
পড়া ও প্রমাণপ্রায়ই সংজ্ঞার হুবহু রূপ, induction দিয়ে সহজ প্রমাণloop invariant দিয়ে প্রমাণ (পর্ব ০৩)

লেজের মতো সরল রিকার্শন (যেমন fact_tail) সহজেই লুপ হয়: parameter-গুলো হয়ে যায় লুপের ভ্যারিয়েবল। কিন্তু Hanoi বা DFS-এর মতো একাধিক রিকার্সিভ কল থাকলে কী করবেন? তখন compiler-এর call stack-এর কাজটি নিজেই করুন — একটি explicit stack রাখুন, যাতে থাকবে “এখনো যে কাজগুলো বাকি”। নিয়ম একটি: stack-এ উল্টো ক্রমে push করুন, কারণ শেষে যা ঢোকে, সেটিই আগে বের হয়।

// Tower of Hanoi without recursion: our own stack of pending tasks
struct Task { int n; char from, to, via; bool moveNow; };

void hanoi_iter(int n) {
    vector<Task> st{{n, 'A', 'C', 'B', false}};
    while (!st.empty()) {
        Task t = st.back(); st.pop_back();
        if (t.n == 0) continue;
        if (t.moveNow) {                      // the "middle" step: one real move
            cout << "Move disk " << t.n << ": " << t.from << " -> " << t.to << '\n';
            continue;
        }
        // push in REVERSE order of execution (last pushed runs first)
        st.push_back({t.n - 1, t.via, t.to, t.from, false});   // 3rd
        st.push_back({t.n, t.from, t.to, t.via, true});         // 2nd
        st.push_back({t.n - 1, t.from, t.via, t.to, false});   // 1st
    }
}

এটি হুবহু রিকার্সিভ সংস্করণের সাত লাইন আউটপুট দেয়। পার্থক্য: stack এখন heap মেমরিতে (vector), যার আকার কয়েক MB-র stack-সীমায় বাঁধা নয়। কনটেস্টে ১০৫–১০৬ node-এর গ্রাফে DFS (পর্ব ৪১) চালাতে ঠিক এই কৌশলই লাগে — বিশেষ করে Python-এ।

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

  • রিকার্শনে base case না থাকলে কী হয়? — কল কখনো থামে না; stack ভরে গিয়ে C++-এ crash, Python-এ RecursionError.
  • সরল রিকার্সিভ fib(n)-এর সময় ও জায়গা? — সময় Θ(φⁿ) (ঢিলেভাবে O(2ⁿ)), stack-এর জায়গা Θ(n).
  • n চাকতির Tower of Hanoi-তে কত চাল? — 2ⁿ − 1; recurrence T(n) = 2T(n − 1) + 1 থেকে induction-এ প্রমাণ।
  • রিকার্সিভ binary search-এর বাড়তি জায়গা কত? — O(log n), কারণ গভীরতা log n; লুপে লিখলে O(1).
  • xn O(log n)-এ কীভাবে? — xn = (xn/2)², বিজোড় হলে × x; অর্ধেক-ঘাত একবারই হিসাব করে রাখতে হবে।

১০ · AI-এর যুগে রিকার্শন — বিশ্বাস করুন, কিন্তু যাচাই করে

“রিকার্সিভ ফাংশন লিখে দাও” বললে AI কয়েক সেকেন্ডে সুন্দর কোড দেয়। ছোট উদাহরণে প্রায় সবসময় চলে। সমস্যা লুকিয়ে থাকে কিনারায় — খালি ইনপুটে, বড় ইনপুটে, আর complexity-র দাবিতে। leap of faith আপনার নিজের লেখা কোডে বৈধ, কারণ আপনি base case আর ছোট হওয়ার নিয়ম যাচাই করেছেন; AI-এর কোডে সেই যাচাই আপনাকেই করতে হবে।

রিকার্শনে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকীভাবে ধরবেন
Base case অসম্পূর্ণ — n == 1 আছে, কিন্তু 0 বা খালি অ্যারে নেইসবচেয়ে ছোট ইনপুট (0, খালি, এক উপাদান) দিয়ে নিজে চালান
Complexity ভুল — সরল fib বা power(x, e/2) * power(x, e/2)-কে O(n) বা O(log n) বলাrecursion tree আঁকুন, কল গুনতে counter বসান, n দ্বিগুণ করে সময় দেখুন
Python slicing (s[1:], a[1:]) দিয়ে লিখে O(n) দাবিপ্রতিটি slice একটি কপি — মোট Θ(n²); index পাঠাতে বলুন
গভীরতা ভুলে যাওয়া — 10⁵ উপাদানে রিকার্সিভ DFS বা sumসবচেয়ে খারাপ গভীরতা হিসাব করুন; Python-এ 1000-এর ওপরে গেলে লুপ/explicit stack
“tail recursive, তাই Python-এ নিরাপদ” জাতীয় দাবিPython TCO করে না — 5000 গভীরতায় চালিয়ে দেখুন
Global counter বা list রিসেট না করা, backtracking-এ undo ভুলে যাওয়াএকই প্রোগ্রামে দুবার ডেকে ফল মেলান

AI-কে কোড লিখতে না বলে বরং আপনার বোঝাপড়া যাচাই করতে ব্যবহার করুন। নিচের prompt-টি আপনাকে চিন্তা করতে বাধ্য করে, আর AI-এর ভুলও ধরিয়ে দেয়:

আমি DSA শিখছি, এখন রিকার্শন। নিচে আমার লেখা একটি রিকার্সিভ ফাংশন দিলাম: [এখানে আপনার কোড পেস্ট করুন] পুরো সমাধান লিখে দিও না। শুধু এগুলো করো: ১. base case কী কী, আর এমন কোনো ইনপুট আছে কি যা base case-এ কখনো পৌঁছায় না? ২. ছোট একটি ইনপুটে (n = 4) recursion tree এঁকে মোট কল আর সর্বোচ্চ stack গভীরতা দেখাও। ৩. সময় আর জায়গার complexity কত, কেন — recurrence লিখে যুক্তি দাও। ৪. Python-এ n = 100000 হলে কী হবে, আর আমি কীভাবে লুপ বা explicit stack-এ বদলাব — শুধু ইঙ্গিত দাও।

AI-এর (বা নিজের) রিকার্সিভ কোড যাচাইয়ের চেকলিস্ট

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

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

প্র ০১প্রথম দিকের প্রোগ্রামিং ভাষায় রিকার্শন ছিল না কেন, আর call stack কীভাবে এলো?

আজ রিকার্শনকে আমরা স্বাভাবিক ধরে নিই, কিন্তু ১৯৫০-এর দশকে এটি ছিল বিতর্কের বিষয়। প্রথম জনপ্রিয় উচ্চ-স্তরের ভাষা FORTRAN (১৯৫৭) প্রতিটি ফাংশনের ভ্যারিয়েবলের জন্য মেমরিতে একটিই স্থির জায়গা রাখত। এতে কোড দ্রুত চলত আর compiler সহজ হতো। কিন্তু এর মানে, একটি ফাংশন চলাকালীন সেটি আবার ডাকা হলে নতুন কল পুরোনো কলের n মুছে দেবে — fact(4)-এর n = 4 হারিয়ে যাবে fact(3)-এর n = 3-এর নিচে। তাই প্রথম দিকের FORTRAN-এ রিকার্শন নিষিদ্ধ ছিল; অনেক পরে Fortran 90-এ RECURSIVE শব্দটি যোগ হয়।

অন্য দিকে John McCarthy তাঁর LISP ভাষার (১৯৫৮–৬০) কেন্দ্রেই রেখেছিলেন রিকার্শন; ১৯৬০ সালের তাঁর বিখ্যাত প্রবন্ধের নামই ছিল “Recursive Functions of Symbolic Expressions and Their Computation by Machine”. একই সময়ে ALGOL 60-এর নকশায় রিকার্শনের অনুমতি দেওয়া হয়, এবং Edsger Dijkstra ১৯৬০ সালে “Recursive Programming” প্রবন্ধে দেখান কীভাবে একটি stack ব্যবহার করে প্রতিটি কলের জন্য আলাদা জায়গা বরাদ্দ করে এটি দক্ষতার সাথে বাস্তবায়ন করা যায় — আজকের call stack-এর সরাসরি পূর্বসূরি।

এর প্রভাব ছিল বিশাল। Tony Hoare পরে স্মৃতিচারণ করেছেন যে quicksort-এর ধারণা তাঁর মাথায় এসেছিল ১৯৫৯ সালের দিকে, কিন্তু সেটি পরিষ্কারভাবে প্রকাশ করতে পেরেছিলেন রিকার্শন-সমর্থিত ALGOL 60 শেখার পর। আজ প্রায় প্রতিটি CPU-তে stack pointer নামে একটি বিশেষ register আর call/ret নির্দেশ আছে — হার্ডওয়্যার নিজেই রিকার্শনের জন্য তৈরি।

তবে FORTRAN-এর যুক্তিও পুরোপুরি হারিয়ে যায়নি। আজও কিছু নিরাপত্তা-সংবেদনশীল ক্ষেত্রে, যেমন গাড়ি বা বিমানের embedded সফটওয়্যারের কোডিং নিয়মে (MISRA C ইত্যাদি), রিকার্শন নিষেধ বা কঠোরভাবে সীমিত — কারণ সেখানে আগে থেকেই প্রমাণ করতে হয় যে stack কখনো ফুরোবে না।

মূল উপলব্ধি: রিকার্শন একটি ভাষার বৈশিষ্ট্য মাত্র নয় — এর পেছনে আছে মেমরি ব্যবস্থাপনার একটি নকশাগত সিদ্ধান্ত (প্রতি কলে নতুন frame). সেই সিদ্ধান্তের দাম হলো stack-এর জায়গা, আর লাভ হলো প্রকাশের অসাধারণ শক্তি।

প্র ০২Python কেন tail call optimization করে না — এটি কি ভুল সিদ্ধান্ত?

২০০৯ সালে Python-এর স্রষ্টা Guido van Rossum তাঁর ব্লগে দুটি লেখায় ব্যাখ্যা করেন কেন Python-এ tail recursion elimination আসবে না। তাঁর যুক্তিগুলো ছিল মূলত তিনটি।

  • Traceback হারিয়ে যায়: TCO মানে পুরোনো frame মুছে নতুনটি বসানো। তখন error হলে traceback-এ মাঝের কলগুলো আর দেখা যায় না — debug করা কঠিন হয়ে পড়ে। Python সবসময় পাঠযোগ্যতা আর debug-সহজতাকে অগ্রাধিকার দেয়।
  • এটি “optimization” নয়, অর্থের পরিবর্তন: কোনো কোড যদি TCO-র ওপর নির্ভর করে লেখা হয়, তাহলে যে Python বাস্তবায়নে (যেমন অন্য interpreter) TCO নেই, সেখানে সেই কোড crash করবে। তাই এটি চালু করলে ভাষার নিয়মেই বাধ্যতামূলক করতে হতো।
  • Python-এ লুপই স্বাভাবিক: Python-এর ধারা হলো সরল ক্রমের কাজ লুপে করা; tail recursion-কে উৎসাহ দেওয়া ভাষার চরিত্রের সাথে মেলে না।

বিপরীত দর্শন দেখা যায় Scheme-এ: এর standard-এ “proper tail calls” বাধ্যতামূলক, তাই সেখানে আলাদা লুপ-সিনট্যাক্স প্রায় লাগেই না — সব পুনরাবৃত্তি tail recursion দিয়ে। C ও C++-এ মাঝামাঝি অবস্থা: GCC আর Clang optimization চালু থাকলে প্রায়ই tail call-কে jump-এ বদলে দেয়, কিন্তু standard কোনো নিশ্চয়তা দেয় না। এজন্য Clang একটি বিশেষ attribute ([[clang::musttail]]) যোগ করেছে, যা দিয়ে প্রোগ্রামার বলতে পারেন “এখানে tail call অবশ্যই হতে হবে, না হলে compile error দাও” — দ্রুত interpreter লেখার মতো কাজে এটি ব্যবহৃত হয়।

তাহলে ভুল সিদ্ধান্ত? না — এটি একটি সচেতন trade-off. Python বেছে নিয়েছে পরিষ্কার error আর সহজ debug; বিনিময়ে গভীর রিকার্শনের জন্য প্রোগ্রামারকে লুপ লিখতে হয়।

মূল উপলব্ধি: একই ধারণা (tail call) এক ভাষায় বাধ্যতামূলক, আরেকটিতে ঐচ্ছিক, তৃতীয়টিতে নিষিদ্ধ — কারণ প্রতিটি ভাষা আলাদা মূল্যবোধকে প্রাধান্য দেয়। অ্যালগরিদম লেখার সময় ভাষার এই বাস্তবতা জানা থাকলে অপ্রত্যাশিত crash এড়ানো যায়।

প্র ০৩Lucas-এর কিংবদন্তির ৬৪ চাকতি সরাতে কত সময় লাগবে? আর Hanoi-র চালে কোন লুকানো ছক আছে?

৬৪টি চাকতিতে সর্বনিম্ন চাল 2⁶⁴ − 1 = 18,446,744,073,709,551,615 — প্রায় ১.৮ × ১০¹⁹. পুরোহিতেরা যদি প্রতি সেকেন্ডে একটি চাল দেন, কোনো ভুল ছাড়া, বিরতিহীন, তাহলে লাগবে প্রায় ৫৮,৫০০ কোটি (৫৮৫ বিলিয়ন) বছর। মহাবিশ্বের বর্তমান বয়স প্রায় ১৩৮০ কোটি বছর — অর্থাৎ তার চল্লিশ গুণেরও বেশি। পৃথিবী ধ্বংসের দুশ্চিন্তা তাই অপ্রয়োজনীয়! একটি আধুনিক কম্পিউটার সেকেন্ডে কয়েকশো কোটি চাল “হিসাব” করলেও কয়েকশো বছর লাগবে। এটিই সূচকীয় বৃদ্ধির আসল চেহারা: প্রতিটি নতুন চাকতি কাজ দ্বিগুণ করে।

এবার লুকানো ছক। চালগুলোকে ১, ২, ৩, … নম্বর দিন। চাল k-তে কোন চাকতি সরে? উত্তর: k-কে binary-তে লিখলে ডান দিক থেকে যতগুলো শূন্য, তার চেয়ে ১ বেশি নম্বরের চাকতি। চাল ১ (1) → disk 1; চাল ২ (10) → disk 2; চাল ৪ (100) → disk 3; চাল ৬ (110) → disk 2. ওপরের ডায়াগ্রামের সাতটি চালের সাথে মিলিয়ে দেখুন। অর্থাৎ Hanoi সমাধান করা আর binary-তে ১ থেকে 2ⁿ − 1 পর্যন্ত গোনা আসলে একই কাজ। এখান থেকেই Gray code (যেখানে পরপর দুটি সংখ্যায় মাত্র একটি bit বদলায়) আর Hanoi-র গভীর সম্পর্ক বেরিয়ে আসে, যা গণিতবিদেরা বিস্তারিত বিশ্লেষণ করেছেন।

আরেকটি মজার তথ্য: সবচেয়ে ছোট চাকতিটি সবসময় একই দিকে চক্রাকারে ঘোরে (n বিজোড় হলে A → C → B → A, জোড় হলে A → B → C → A), আর প্রতি দ্বিতীয় চালে সেটিই সরে। এই পর্যবেক্ষণ দিয়ে রিকার্শন ছাড়াই একটি সরল লুপে Hanoi সমাধান করা যায়।

Lucas নিজেও আরও বহু কাজের জন্য পরিচিত — Fibonacci-র আত্মীয় “Lucas numbers” আর বড় সংখ্যা মৌলিক কি না যাচাইয়ের পরীক্ষা (যা পরে Lucas–Lehmer test হিসেবে Mersenne prime খোঁজায় ব্যবহৃত হয়) তাঁর নামেই।

মূল উপলব্ধি: 2ⁿ − 1 শুধু একটি সূত্র নয় — এটি একটি সতর্কবার্তা। যে অ্যালগরিদম প্রতিটি ধাপে কাজ দ্বিগুণ করে, তা n = ৬০-এর পরে মানুষের সময়ের মাপকাঠিতে অসম্ভব। ভালো অ্যালগরিদম চেনার প্রথম ধাপ হলো এই বৃদ্ধি চিনে ফেলা।

প্র ০৪Fast power শুধু ক্লাসরুমের কৌশল, নাকি আমাদের প্রতিদিনের ইন্টারনেট এর ওপর দাঁড়িয়ে আছে?

আপনি যখন bKash অ্যাপ খোলেন, ব্যাংকের ওয়েবসাইটে লগইন করেন বা ব্রাউজারে তালার চিহ্নসহ (HTTPS) কোনো সাইট দেখেন, তখন পেছনে public-key cryptography কাজ করে। এর সবচেয়ে পরিচিত পদ্ধতি RSA (১৯৭৭, Rivest, Shamir ও Adleman). RSA-র মূল হিসাব হলো ae mod n, যেখানে n সাধারণত ২০৪৮ bit — দশমিকে ছয়শোর বেশি অঙ্কের সংখ্যা — আর গোপন ঘাত d-ও প্রায় সেই মাপের।

সরল লুপে d বার গুণ করলে কী হতো? d যদি প্রায় 2²⁰⁴⁸ হয়, তাহলে গুণের সংখ্যা মহাবিশ্বের পরমাণুর সংখ্যার চেয়েও বহুগুণ বেশি — অসম্ভব। কিন্তু fast power-এ লাগে মোটামুটি log₂ d ≈ ২০৪৮টি বর্গ আর তার চেয়ে কম বা সমান সংখ্যক বাড়তি গুণ — কয়েক হাজার গুণ, এক সেকেন্ডের ক্ষুদ্র ভগ্নাংশে শেষ। প্রতিটি গুণের পর mod n নেওয়া হয় বলে সংখ্যাগুলো কখনো ২০৪৮ bit-এর অনেক বেশি বড় হয় না। এই “square-and-multiply” ছাড়া আজকের অনলাইন নিরাপত্তা কল্পনাই করা যেত না।

কৌশলটি নতুনও নয়। Donald Knuth তাঁর The Art of Computer Programming-এ এর ইতিহাস আলোচনা করতে গিয়ে উল্লেখ করেছেন যে এর ধারণা প্রাচীন ভারতের Pingala-র ছন্দশাস্ত্রে পাওয়া যায় — অর্থাৎ দুই হাজার বছরেরও বেশি পুরোনো। একই কৌশল সংখ্যা ছাড়াও যেকোনো “গুণ”-এ চলে, যতক্ষণ সেই গুণ সহযোজী (associative): matrix-এর ঘাত দিয়ে O(log n)-এ n-তম Fibonacci সংখ্যা, গ্রাফের adjacency matrix-এর ঘাত দিয়ে ঠিক k ধাপের পথ গোনা, এমনকি permutation বারবার প্রয়োগের ফল।

প্রতিযোগিতামূলক প্রোগ্রামিংয়ে modpow সম্ভবত সবচেয়ে বেশি লেখা ফাংশন: modular inverse (Fermat-এর সূত্রে ap−2 mod p), combinatorics-এর nCr mod p — সবকিছুতেই এটি লাগে (পর্ব ৬২)। বাস্তবে এটি প্রায়ই লুপে লেখা হয় (e-এর bit ধরে ধরে), কিন্তু চিন্তাটি হুবহু এই রিকার্সিভ সূত্রটিই।

মূল উপলব্ধি: “সমস্যাকে অর্ধেক করো, ফলাফল একবার হিসাব করে দুবার ব্যবহার করো” — এই একটি রিকার্সিভ চিন্তা Θ(e) কাজকে Θ(log e)-এ নামিয়ে আনে, আর সেই পার্থক্যই অসম্ভবকে সম্ভব করে।

প্র ০৫সব রিকার্শনকে কি লুপে বদলানো যায়? তাহলে বাস্তব সফটওয়্যার কখন রিকার্শন এড়িয়ে চলে?

তত্ত্বে উত্তর হ্যাঁ — যেকোনো রিকার্শনকে একটি লুপ আর একটি explicit stack দিয়ে লেখা যায়, যেমন অংশ ৯-এ Hanoi-র ক্ষেত্রে দেখলেন। কম্পিউটার তো আসলে তাই-ই করে: CPU রিকার্শন বোঝে না, সে শুধু stack-এ push-pop আর jump করে।

কিন্তু “শুধু লুপ, কোনো stack ছাড়া, আর আগে থেকে নির্দিষ্ট সংখ্যক ঘুর্ণন” — এই সীমার মধ্যে সব হয় না। ১৯২৮ সালে Wilhelm Ackermann একটি ফাংশন দেন, যা রিকার্শনে কয়েক লাইনে লেখা যায় এবং সব ইনপুটে থামে, অথচ প্রমাণিতভাবে “primitive recursive” নয় — অর্থাৎ আগে থেকে ঘুর্ণনের সংখ্যা জানা থাকে এমন for-লুপের কোনো নির্দিষ্ট কাঠামো দিয়ে লেখা যায় না। এর মান এত দ্রুত বাড়ে যে A(4, 2)-তেই ১৯,৭২৯ অঙ্কের সংখ্যা আসে। মজার ব্যাপার, এর উল্টো ফাংশন (inverse Ackermann, α(n)) এত ধীরে বাড়ে যে বাস্তবের যেকোনো n-এ এটি ৫-এর কম — এবং এটিই পর্ব ৩৩-এর union-find-এর complexity-তে দেখা দেবে।

বাস্তব সফটওয়্যার কোথায় রিকার্শন এড়ায়? Linux kernel-এ প্রতিটি thread-এর kernel stack মাত্র কয়েক কিলোবাইট (x86-64-এ সাধারণত 16 KB) — সেখানে গভীর রিকার্শন সরাসরি সিস্টেম crash ঘটাতে পারে, তাই kernel কোডে রিকার্শন সাধারণত এড়িয়ে চলা হয়। JSON বা XML parser রিকার্সিভ হলেও প্রায়ই একটি সর্বোচ্চ গভীরতা বেঁধে দেয়, কারণ আক্রমণকারী ইচ্ছা করে হাজার স্তরের বন্ধনী পাঠিয়ে সার্ভার crash করাতে পারে — এটি একটি পরিচিত denial-of-service কৌশল। কনটেস্টেও একই শিক্ষা: ১০⁶ node-এর একটি লম্বা শিকলের মতো গ্রাফে রিকার্সিভ DFS অনেক judge-এ “Runtime Error” দেয়।

অন্য দিকে compiler-এর parser, tree আর গ্রাফের অ্যালগরিদম, divide and conquer — এসব জায়গায় রিকার্শনই সবচেয়ে পরিষ্কার ও কম ভুলপ্রবণ সমাধান, যতক্ষণ গভীরতা নিয়ন্ত্রণে থাকে (যেমন balanced tree-তে log n).

মূল উপলব্ধি: রিকার্শন বনাম লুপ কোনো ধর্মযুদ্ধ নয়। প্রশ্ন দুটি: “সমস্যার স্বাভাবিক আকার কী?” আর “সবচেয়ে খারাপ ক্ষেত্রে গভীরতা কত?” প্রথমটি বলে কোনটি লেখা সহজ, দ্বিতীয়টি বলে কোনটি নিরাপদ।

অনুশীলন

খাতা-কলম নিন। প্রথম তিনটি হাতে করার, শেষটিতে কোড লিখতে হবে।

  1. হাতে trace করুন: নিচের ফাংশনে f(3) ডাকলে স্ক্রিনে ঠিক কী ছাপা হবে? মোট কয়টি কল হবে, আর stack-এর সর্বোচ্চ গভীরতা কত?
    void f(int n) {
        if (n == 0) return;
        cout << n;
        f(n - 1);
        cout << n;
    }

    আউটপুট: 321123. কারণ প্রতিটি কল রিকার্শনে নামার আগে একবার n ছাপে (3, 2, 1 — নামার পথে), আর ফিরে আসার পরে আবার ছাপে। ফেরার ক্রম stack-এর নিয়মে উল্টো: আগে f(1), তারপর f(2), শেষে f(3) — তাই 1, 2, 3.

    কল: f(3), f(2), f(1), f(0) — মোট ৪টি। সর্বোচ্চ গভীরতা ৪ (f(0) চলার মুহূর্তে চারটি frame). এই “নামার পথে একবার, ওঠার পথে একবার” ধাঁচটি tree traversal-এর preorder আর postorder-এর ভিত্তি (পর্ব ২৬)।

  2. কল গুনুন: সরল রিকার্সিভ fib(6)-এ মোট কতবার fib ডাকা হয়? এর মধ্যে fib(2) কতবার? stack-এর সর্বোচ্চ গভীরতা কত?

    C(n) = 1 + C(n − 1) + C(n − 2), C(0) = C(1) = 1 ধরে: C(2) = 3, C(3) = 5, C(4) = 9, C(5) = 15, C(6) = 25. সূত্র দিয়েও মেলে: 2·F(7) − 1 = 2 × 13 − 1 = 25.

    fib(2) ডাকা হয় ৫ বার। সাধারণভাবে fib(n)-এর ভেতরে fib(k) ডাকা হয় F(n − k + 1) বার (k ≥ 1-এর জন্য): F(5) = 5. সর্বোচ্চ গভীরতা ৬ — fib(6) → fib(5) → fib(4) → fib(3) → fib(2) → fib(1), এই পথের ছয়টি frame. মাত্র ৭টি ভিন্ন প্রশ্নের (fib(0) থেকে fib(6)) জন্য ২৫টি কল — memoization থাকলে লাগত মাত্র ১১টির মতো।

  3. Hanoi নিয়ে চিন্তা: ৪টি চাকতি A থেকে C-তে নিতে মোট কত চাল লাগবে? ৮ নম্বর চালে কোন চাকতি সরে, কোথা থেকে কোথায়?

    মোট 2⁴ − 1 = ১৫টি চাল। রিকার্সিভ সমাধানে প্রথম 2³ − 1 = ৭টি চালে ওপরের তিনটি চাকতি A থেকে B-তে যায়, তারপর ঠিক মাঝখানের চালটি — ৮ নম্বর — সবচেয়ে বড় disk 4, A → C. শেষ ৭টি চালে B-র তিনটি চাকতি C-তে যায়।

    binary-র ছক দিয়েও মেলে: 8 = 1000, ডান দিকে ৩টি শূন্য, তাই disk 3 + 1 = 4. সাধারণভাবে n চাকতিতে চাল নম্বর 2ⁿ⁻¹-এ সবচেয়ে বড় চাকতিটি সরে, এবং মাত্র একবারই।

  4. কোড লিখুন: একটি রিকার্সিভ ফাংশন লিখুন যা একটি অঋণাত্মক পূর্ণসংখ্যা n-এর binary রূপ ছাপে (যেমন 13 → 1101, আর 0 → 0). কোনো লুপ বা string উল্টানো ব্যবহার করবেন না। সময় ও জায়গার complexity বলুন।

    মূল চিন্তা: n-এর শেষ bit হলো n % 2, আর বাকি bit-গুলো হলো n / 2-এর binary রূপ। তাই আগে বাকিটা ছাপুন (রিকার্শন), তারপর শেষ bit — তাহলে ক্রম আপনাআপনি ঠিক হয়।

    void printBinary(int n) {
        if (n < 2) { cout << n; return; }   // base case: 0 or 1
        printBinary(n / 2);                  // leap of faith: higher bits
        cout << n % 2;                       // then the last bit
    }

    Python-এ কাঠামো হুবহু এক, শুধু ছাপার জন্য print(n % 2, end=""). 13 → printBinary(6) → printBinary(3) → printBinary(1) ছাপে 1, তারপর ফেরার পথে 1, 0, 1 → 1101. base case n < 2 হওয়ায় 0 ইনপুটেও 0 ছাপে। কল ⌊log₂ n⌋ + 1 টি, তাই সময় ও stack-এর জায়গা দুটোই Θ(log n).

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

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

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

  1. ফাংশনটি ঠিক কী ফেরত দেবে, এক বাক্যে নির্ধারণ করা
  2. সবচেয়ে ছোট ইনপুটের উত্তর (base case) লেখা
  3. সমস্যাটিকে একই ধরনের ছোট সমস্যায় ভাঙা
  4. ছোট কল ঠিক উত্তর দেবে ধরে নিয়ে (leap of faith) বড় উত্তর গড়া
  5. প্রতিটি কলে মাপ কমে base case-এ পৌঁছায় কি না, আর stack গভীরতা কত — যাচাই করা

একটি রিকার্সিভ ফাংশনে base case নেই, আর প্রতিটি কল নিজেকে আবার ডাকে। প্রোগ্রাম চালালে সাধারণত কী ঘটে?

  • Compiler কোডটি compile করতেই অস্বীকার করে
  • Call stack ভরে গিয়ে stack overflow — C++-এ crash, Python-এ RecursionError
  • প্রোগ্রাম চিরকাল চলতেই থাকে, কোনো error ছাড়া
  • ফাংশন একবার চলে 0 ফেরত দেয়

fib(0) = 0, fib(1) = 1 আর fib(n) = fib(n−1) + fib(n−2) সংজ্ঞা হুবহু রিকার্শনে (memoization ছাড়া) লিখলে fib(5) হিসাব করতে মোট কতবার fib ডাকা হয়?

  • 5
  • 8
  • 15
  • 32

৫টি চাকতির Tower of Hanoi সমাধানে সর্বনিম্ন কতটি চাল লাগে?

  • 5
  • 25
  • 32
  • 31

xn বের করতে xn = (xn/2)² (বিজোড় হলে আরেকবার × x) সূত্রে রিকার্শন লেখা হলো, যেখানে xn/2 একবার হিসাব করে ভ্যারিয়েবলে রাখা হয়। গুণের সংখ্যার time complexity কত?

  • O(log n)
  • O(n)
  • O(n log n)
  • O(1)

নিচের কোন return লাইনে রিকার্সিভ কলটি একটি tail call (ডাকার পর আর কোনো কাজ বাকি থাকে না)?

  • return n * f(n - 1);
  • return f(n - 1, acc * n);
  • return 1 + f(n - 1);
  • return f(n - 1) + f(n - 2);

sum(a, n) = sum(a, n−1) + a[n−1] এবং sum(a, 0) = 0 — এভাবে রিকার্শনে n উপাদানের অ্যারের যোগফল বের করলে বাড়তি জায়গা (auxiliary space) কত?

  • O(1), কারণ কোনো নতুন অ্যারে তৈরি হয়নি
  • O(log n)
  • O(n), কারণ call stack-এ একসাথে n টির মতো frame থাকে
  • O(n²)

শব্দকোষ

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

মনে রাখুন

  1. রিকার্শন = base case (সরাসরি উত্তর) + recursive case (ছোট সমস্যায় নিজেকে ডাকা); প্রতিটি কলে কোনো মাপ কমতেই হবে।
  2. Leap of faith = induction: ছোট কল ঠিক ধরে নিয়ে শুধু নিজের অংশটুকু ঠিক করুন।
  3. প্রতিটি কল একটি stack frame; সময় = মোট কল × প্রতি কলের কাজ, জায়গা = সর্বোচ্চ গভীরতা × frame.
  4. সরল fib Θ(φⁿ) কারণ একই কাজ বারবার; fast power Θ(log e); Hanoi ঠিক 2ⁿ − 1 চাল।
  5. Python-এ সীমা প্রায় 1000, TCO নেই; গভীর হলে লুপ বা explicit stack.

পরের পর্বে: এই পর্বে বারবার লিখেছি T(n) = 2T(n − 1) + 1 বা T(e) = T(e/2) + O(1). এমন recurrence থেকে সরাসরি complexity কীভাবে বের করবেন — substitution আর recursion tree পদ্ধতি দিয়ে, ধাপে ধাপে।

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

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