Module ৮ · Recursion
Recursion: যে function নিজেকেই call করে
এই lesson-এ যা শিখবে
- Base case আর step দিয়ে recursive function লিখতে পারবে: countdown, n পর্যন্ত যোগফল, factorial।
- Frame table দিয়ে recursion trace করতে পারবে, আর বলতে পারবে call-এর পরের কাজ কেন ফেরার পথে run হয়।
- Base case না থাকলে বা কখনো না পৌঁছালে Playground আর
gcc -Wallকী বলে, দেখে চিনতে পারবে।
Module 7-এ Bob শিখেছে, একটা function যেকোনো function-কে call করতে পারে। তারপর মাথায় একটা সাহসী প্রশ্ন আসে: function কি নিজেকেই call করতে পারে? ওর দরকার 1 + 2 + 3, আর ও খেয়াল করে, এটা আসলে 3 যোগ 1 + 2।
তাই ও এমন একটা function লেখে, যেটা n-এর সাথে n - 1-এর জন্য নিজেরই উত্তর যোগ করে। তারপর Run চাপে। কিছুই আসে না। 10 সেকেন্ড পরে Playground program-টা থামিয়ে দেয়, আর badge-এ লেখা ওঠে টাইম লিমিট পার হয়েছে।
ওর বইতে লেখা ছিল, এমন ভুলে নাটকীয় একটা "stack overflow" হবে। ও পেল শুধু চুপচাপ একটা ঘড়ি। Bob কোন লাইনটা বাদ দিয়েছে, আর ওই 10 সেকেন্ডে আসলে কী ঘটেছে, এই lesson সেটাই খুঁজে বের করবে।
Recursion: যে function নিজেকেই call করে
যে function নিজেকেই call করে, তাকে বলে recursive, আর পুরো ধারণাটার নাম recursion। এটা কাজ করে তখনই, যখন প্রতিটা call একই সমস্যার একটা ছোট রূপ পরের call-এর হাতে তুলে দেয়। Bob ঠিক যেভাবে run করেছিল, ওর program-টা এই:
#include <stdio.h>
int sum_to(int n);
int main(void)
{
printf("%d\n", sum_to(3));
return 0;
}
int sum_to(int n)
{
return n + sum_to(n - 1);
}
হাতে একবার trace করে দেখো। sum_to(3)-এর লাগে sum_to(2), সেটার লাগে sum_to(1), আর সেটার লাগে sum_to(0)। sum_to(0)-কে থামতে কেউ বলে না, তাই সে চায় sum_to(-1), তারপর sum_to(-2), এভাবে শেষই হয় না।
টিকিট কাউন্টারের একটা লম্বা লাইনের কথা ভাবো। তুমি কত নম্বরে আছ জানতে চাও, তাই সামনের জনকে জিজ্ঞেস করো, আর তার উত্তরের সাথে এক যোগ করো। সে-ও একই কাজ করে। এই শিকল থামে একদম সামনের জনের কাছে গিয়ে: সে কাউকে জিজ্ঞেস না করেই জানে, সে 1 নম্বর।
তাই recursive function-এ এমন একটা call লাগবেই, যেটা কাউকে জিজ্ঞেস না করেই উত্তর দেয়। Bob-এর program-এ প্রতিটা call শুধু জিজ্ঞেসই করে, উত্তর দেয় না কেউ।
দুইটা নিয়ম: base case আর step
যে recursive function শেষ হয়, সেটা দুইটা নিয়ম মানে। Base case হলো সবচেয়ে ছোট input, যার উত্তর সরাসরি দেওয়া হয়, কোনো call ছাড়া। Step একই function-কে একটা ছোট input দিয়ে call করে, base case-এর দিকে এক ধাপ এগিয়ে। কোনো কোনো বই step-কে বলে recursive case।
Recursive function-এর গড়ন
type name(int n)
{
if (n == smallest) {
return answer;
}
return ... name(n - 1) ...;
}
if (n == smallest)হলো base case-এর পরীক্ষা। এটা আসে সবার আগে, কোনো call-এর আগেই।return answer;সবচেয়ে ছোট input-এর উত্তর দেয় কোনো call ছাড়াই, তাই শিকলটা এখানেই থামে।name(n - 1)হলো step: একই function, base case-এর আরও কাছের একটা মান দিয়ে।...অংশটা ছোট উত্তরটাকে এই call-এর উত্তরে বদলে দেয়, যেমন যোগফলের জন্যn +।
Bob হারানো নিয়মটা যোগ করে। 0 পর্যন্ত সংখ্যার যোগফল তো 0, তাই sum_to(0) কোনো call ছাড়াই 0 return করে। সাথে ও উত্তরটাকে long long বানায়, কারণ n = 65536 থেকে যোগফল INT_MAX পার হয়ে যায়।
#include <stdio.h>
long long sum_to(int n);
int main(void)
{
printf("%lld\n", sum_to(3));
printf("%lld\n", sum_to(100));
return 0;
}
long long sum_to(int n)
{
if (n == 0) {
return 0;
}
return n + sum_to(n - 1);
}
Run করার আগে trace করো। Module 6-এ loop trace হতো প্রতিটা pass-এর জন্য এক সারি দিয়ে। Recursion-এ সারি হয় প্রতিটা call-এর জন্য একটা: call-টা, তার parameter, সে কীসের অপেক্ষায়, আর কী return করে। একে বলা যায় frame table।
| Call | n | অপেক্ষা করে | Return করে |
|---|---|---|---|
sum_to(3) | 3 | sum_to(2) | 3 + 3 = 6 |
sum_to(2) | 2 | sum_to(1) | 2 + 1 = 3 |
sum_to(1) | 1 | sum_to(0) | 1 + 0 = 1 |
sum_to(0) | 0 | কারও না: এটাই base case | 0 |
Table-টা দুই দফায় ভরো। Call যেমন যেমন হয়, প্রথম তিনটা কলাম উপর থেকে নিচে ভরে যাও। তারপর শেষ কলামটা ভরো নিচ থেকে উপরে: নিচের সারি return না করা পর্যন্ত উপরের সারি শেষই হতে পারে না।
6
5050
তার মানে উত্তর শুরু হয় base case থেকেই। Bob-এর program-এর মতো base case না থাকলে কোনো সারি কখনো return করে না।
Countdown: call-এর আগের কাজ
Module 6-এর সেই rocket game-এ Bob-এর countdown-টা আবার লাগবে, এবার recursion দিয়ে। n থেকে countdown মানে: n বলো, তারপর n - 1 থেকে countdown করো। আর 0 থেকে countdown মানে শুধু "liftoff"।
#include <stdio.h>
void countdown(int n);
int main(void)
{
countdown(3);
return 0;
}
void countdown(int n)
{
if (n == 0) {
printf("liftoff\n");
return;
}
printf("%d\n", n);
countdown(n - 1);
}
| Call | n | নিজের call-এর আগে print করে | তারপর call করে |
|---|---|---|---|
countdown(3) | 3 | 3 | countdown(2) |
countdown(2) | 2 | 2 | countdown(1) |
countdown(1) | 1 | 1 | countdown(0) |
countdown(0) | 0 | liftoff, এটাই base case | কাউকে না |
3
2
1
liftoff
প্রতিটা call আগে print করে, পরে call করে, তাই সংখ্যাগুলো আসে ঠিক সেই ক্রমে, যে ক্রমে call হয়। একদম নিচে base case print করে liftoff। void function কোনো মান return করে না, তাই ওর base case শেষ হয় শুধু return; দিয়ে।
তাই call-এর আগে রাখা কাজ হয় নামার পথে, call-গুলো যে ক্রমে হয় সেই ক্রমেই।
Call-এর পরের কাজ হয় ফেরার পথে
এবার কাজটা call-এর নিচে বসাও। Call-এর পরের লাইন ততক্ষণ run হতে পারে না, যতক্ষণ না ওই call return করছে। তাই call-এর পরের লাইনগুলো run হয় উল্টো ক্রমে, ফেরার পথে।
Alice দেখতে চায়, sum_to ধাপে ধাপে কীভাবে উত্তর বানায়। ও প্রতিটা যোগফল একটা local variable-এ রাখে, আর call-এর পরে সেটা print করে।
#include <stdio.h>
long long sum_to(int n);
int main(void)
{
long long total = sum_to(3);
printf("total: %lld\n", total);
return 0;
}
long long sum_to(int n)
{
long long total = 0;
if (n == 0) {
return 0;
}
total = n + sum_to(n - 1);
printf("sum to %d is %lld\n", n, total);
return total;
}
| Call | n | অপেক্ষা করে | অপেক্ষার পরে print করে | Return করে |
|---|---|---|---|---|
sum_to(3) | 3 | sum_to(2) | sum to 3 is 6 | 6 |
sum_to(2) | 2 | sum_to(1) | sum to 2 is 3 | 3 |
sum_to(1) | 1 | sum_to(0) | sum to 1 is 1 | 1 |
sum_to(0) | 0 | কারও না: এটাই base case | কিছু না | 0 |
Call হয় উপরের সারি থেকে নিচের দিকে, কিন্তু print হয় নিচের সারি থেকে উপরের দিকে। সবার আগে print করে sum_to(1), কারণ উত্তর সবার আগে ওর হাতেই আসে।
sum to 1 is 1
sum to 2 is 3
sum to 3 is 6
total: 6
প্রতিটা call নিজের parameter আর local variable-এর জন্য নিজস্ব বাক্স পায় (Module 7)। এই বাক্সগুলো মিলে হয় call-টার frame। যে call-গুলো এখনো অপেক্ষায়, ওদের frame জমতে থাকে memory-র একটা অংশে, যার নাম stack, প্রতি call-এ একটা করে। Stack খুলে দেখাবে lesson 2; এখানে ছবিটা দেখো।
বাঁ দিকের তীরগুলো call, নামার পথে। ডান দিকের তীরগুলো উত্তর, ফেরার পথে। কোনো call return করলে ওর frame-টা মুছে যায়।
তাই call-এর আগের কাজ চলে বাঁ দিকের তীর ধরে, আর পরের কাজ চলে ডান দিকের তীর ধরে।
Factorial, long long দিয়ে
Module 6 তোমার কাছে Kenji-র factorial চেয়েছিল একটা loop দিয়ে, আর কথা দিয়েছিল ওটা আবার ফিরবে। n-এর factorial, লেখা হয় n!, মানে 1 x 2 x ... x n, আর 0! হলো 1। এবার recursion-এর চোখে পড়ো: n! মানে n গুণ (n - 1)!, আর 0! হলো base case।
#include <stdio.h>
long long factorial(int n);
int main(void)
{
for (int i = 0; i <= 20; i++) {
printf("%d! = %lld\n", i, factorial(i));
}
return 0;
}
long long factorial(int n)
{
if (n == 0) {
return 1;
}
return n * factorial(n - 1);
}
ওই call-গুলোর একটা, factorial(4), তার frame table এরকম:
| Call | n | অপেক্ষা করে | Return করে |
|---|---|---|---|
factorial(4) | 4 | factorial(3) | 4 x 6 = 24 |
factorial(3) | 3 | factorial(2) | 3 x 2 = 6 |
factorial(2) | 2 | factorial(1) | 2 x 1 = 2 |
factorial(1) | 1 | factorial(0) | 1 x 1 = 1 |
factorial(0) | 0 | কারও না: এটাই base case | 1 |
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000
16! = 20922789888000
17! = 355687428096000
18! = 6402373705728000
19! = 121645100408832000
20! = 2432902008176640000
উত্তর খুব দ্রুত বাড়ে। 12! = 479001600 একটা int-এ এঁটে যায়, কিন্তু 13! = 6227020800 পার হয়ে যায় INT_MAX, মানে 2147483647। এজন্যই factorial return করে long long।
long long-এর দৌড় 20! পর্যন্ত। LLONG_MAX হলো 9223372036854775807, আর 20! তার মোটামুটি চার ভাগের এক ভাগ। 21! হলো 20!-এর 21 গুণ, প্রায় 5.1 x 10^19, মানে LLONG_MAX-এর পাঁচ গুণেরও বেশি। ওটা হিসাব করতে গেলেই signed overflow, যেটা undefined behaviour (Module 2), তাই এই lesson ওটা কখনো print করে না।
তাই recursive factorial-এর লাগে base case 0! = 1, আর উত্তর ধরার মতো বড় একটা type: 12! পর্যন্ত int, 20! পর্যন্ত long long।
Base case না থাকলে কী হয়, মেপে দেখা
আবার Bob-এর program-এ ফিরি। ওর বই বলে, base case ছাড়া function "Stack overflow" error দেখায়। C এমন কোনো বার্তা print করে না, আর Playground-এ Bob-এর program crash-ই করেনি। কারণটা দেখো।
Playground compile করে GCC 12 দিয়ে, -O2-তে, আর সেটা optimise করে। মানে তোমার program-কে একই কাজের দ্রুততর machine code হিসেবে নতুন করে লেখে। GCC 12 দেখেছে, প্রতিটা call পরের call-এর উত্তরের সাথে শুধু একটা সংখ্যা যোগ করে। তাই ও call-গুলোর জায়গায় একটা loop বসিয়ে দিয়েছে, আর সেই loop থেকে বেরোনোর কোনো পথ নেই।
এই machine code-কে বলে listing। Compiler Explorer-এ, Playground-এর -O2-তে, GCC 12 যেটা print করেছে সেটা এই:
main:
.L2:
jmp .L2
sum_to:
.L5:
jmp .L5
jmp .L2 মানে ".L2 label-এ লাফ দাও", আর .L2 ঠিক এই লাইনটাই। তাই main চিরকাল নিজের জায়গাতেই লাফাতে থাকে, নিজের printf পর্যন্ত কখনো পৌঁছায় না। কোনো call বাকি নেই, কিছু জমেও না, program শুধু ঘুরতে থাকে, যতক্ষণ না 10 সেকেন্ডের limit আসে।
তুমি কী দেখবে, সেটা নির্ভর করে function-টা নিজের call-এর আশপাশে কী করে তার উপর:
| Base case ছাড়া function | -O2-তে GCC 12 যা বানায় | Playground যা দেখায় |
|---|---|---|
Call-এর আশপাশে শুধু হিসাব, Bob-এর sum_to-এর মতো | একটা অশেষ loop | (no output), আর 10090 ms পরে টাইম লিমিট পার হয়েছে |
প্রতিটা call-এ print করে, if ছাড়া countdown-এর মতো | একটা অশেষ loop, যেটা print করতেই থাকে | Playground-এর 1 MB output cap ভরে যাওয়া পর্যন্ত print করে, তারপর run থামে Output limit পার হয়েছে (1 MB) badge দেখিয়ে। |
| Call-এর পরে এমন কাজ, যেটাকে GCC loop বানাতে পারে না | আসল call, প্রতিটার একটা করে frame | Program-এর 256,000 KB memory limit পর্যন্ত stack বাড়তে থাকে: Memory limit exceeded, আর (no output)। ঠিক কোথায় গিয়ে, সেটা lesson 2 মাপে। |
বাংলা Playground page-এ Memory limit-এর badge-টা বাংলায় দেখায়, ফাঁকা output-এর লেখাটাও।
Machine code build-এর উপরও নির্ভর করে। Bob-এর file optimisation ছাড়া build করো, -O0-তে, তাহলে প্রতিটা call একটা আসল frame পায়। Compiler Explorer-এর GCC 12-এ একবার run করে দেখা গেছে, ওই build 193 ms-এই crash করে, আর বলে Program terminated with signal SIGSEGV (11)।
Linux terminal-এ একই crash print করে Segmentation fault। কোনো program যে memory ছোঁয়ার অনুমতি পায়নি সেটা ছুঁলে, operating system এভাবেই জানায়। Signal আর stack-এর সীমার কথা আসবে lesson 5-এ।
এর কোনো ফলেই recursion-এর নাম নেই, তবে compiler নামটা বলতে পারে। Playground চুপ থাকে। নিজের মেশিনে GCC 12-এর gcc -Wall বলে warning: infinite recursion detected [-Winfinite-recursion], সাথে একটা note, যেটা দেখিয়ে দেয় call-টা কোথায়। এই warning GCC 12-এ নতুন।
তাই base case না থাকলে সেটা দেখতে time limit, output error বা memory limit-এর মতো লাগতে পারে। Run করার আগেই -Wall আসল সমস্যার নাম বলে দেয়।
Base case চাইলে কিছুই না করে শুধু থামতে পারে। এটা প্রতি call-এ একটা করে তারা print করে।
#include <stdio.h>
void stars(int n);
int main(void)
{
stars(3);
printf("\n");
return 0;
}
void stars(int n)
{
if (n == 0) {
return;
}
printf("*");
stars(n - 1);
}
| Call | n | Print করে | তারপর call করে |
|---|---|---|---|
stars(3) | 3 | * | stars(2) |
stars(2) | 2 | * | stars(1) |
stars(1) | 1 | * | stars(0) |
stars(0) | 0 | কিছু না: এটাই base case | কাউকে না |
***
প্রতিটা call একটা তারা print করে, আর সারির বাকিটা তুলে দেয় একটা ছোট call-এর হাতে। নতুন লাইনটা আসে main থেকে, শেষ call return করার পরে।
Amara ওর দোকানে কমলা সাজায় বর্গাকার পিরামিড করে। সবচেয়ে উপরের স্তরে 1টা কমলা, পরেরটায় 4টা, তারপর 9টা: k নম্বর স্তরে k x k টা। n স্তরের পিরামিডে মোট কয়টা কমলা?
#include <stdio.h>
long long pyramid(int layers);
int main(void)
{
printf("4 layers: %lld oranges\n", pyramid(4));
printf("10 layers: %lld oranges\n", pyramid(10));
return 0;
}
long long pyramid(int layers)
{
if (layers == 0) {
return 0;
}
return layers * layers + pyramid(layers - 1);
}
| Call | layers | অপেক্ষা করে | Return করে |
|---|---|---|---|
pyramid(4) | 4 | pyramid(3) | 16 + 14 = 30 |
pyramid(3) | 3 | pyramid(2) | 9 + 5 = 14 |
pyramid(2) | 2 | pyramid(1) | 4 + 1 = 5 |
pyramid(1) | 1 | pyramid(0) | 1 + 0 = 1 |
pyramid(0) | 0 | কারও না: এটাই base case | 0 |
4 layers: 30 oranges
10 layers: 385 oranges
Step-কে সব সময় n-ই যোগ করতে হবে, এমন না। এখানে সে সবচেয়ে নিচের স্তরটা যোগ করে, তারপর তার উপরে বসানো ছোট পিরামিডটা চায়। মোট সংখ্যাটা long long, কারণ 1861 স্তর থেকে এটা INT_MAX পার হয়ে যায়।
Maria চায় a থেকে b পর্যন্ত প্রতিটা পূর্ণসংখ্যার যোগফল। এখানে ছোট সমস্যা মানে ছোট range: a যোগ করো, তারপর a + 1 থেকে b পর্যন্ত range-এর যোগফল নাও।
#include <stdio.h>
long long sum_range(int a, int b);
int main(void)
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("%lld\n", sum_range(a, b));
return 0;
}
long long sum_range(int a, int b)
{
if (a > b) {
return 0;
}
return a + sum_range(a + 1, b);
}
| Call | a | b | অপেক্ষা করে | Return করে |
|---|---|---|---|---|
sum_range(3, 7) | 3 | 7 | sum_range(4, 7) | 3 + 22 = 25 |
sum_range(4, 7) | 4 | 7 | sum_range(5, 7) | 4 + 18 = 22 |
sum_range(5, 7) | 5 | 7 | sum_range(6, 7) | 5 + 13 = 18 |
sum_range(6, 7) | 6 | 7 | sum_range(7, 7) | 6 + 7 = 13 |
sum_range(7, 7) | 7 | 7 | sum_range(8, 7) | 7 + 0 = 7 |
sum_range(8, 7) | 8 | 7 | কারও না: এটাই base case | 0 |
25
ওই output-টা 3 7 input-এর জন্য। এখানে step a-কে বাড়ায়, কমায় না, আর base case হলো ফাঁকা range।
Zara আগে edge-গুলো চালিয়ে দেখে। 5 5 দিলে 5, এক সংখ্যার range। 5 3 দিলে সাথে সাথে 0, কারণ range-টা ফাঁকা। Base case a == b লিখলে সেটা 5 3-এর বেলায় কখনো মিলত না, কারণ a শুধু বাড়তেই থাকে।
এটা কোথায় কাজে লাগে
- Folder-এর গাছ। একটা folder-এর size মানে ওর নিজের file-গুলো, সাথে ভিতরের প্রতিটা folder-এর size। GNU coreutils-এর
du --helpবলে, ও কাজ করে "recursively for directories", আরrm -r-এর-rমানে--recursive। - JSON parser। cJSON নামের ছোট একটা C library একটা JSON মান পড়ে
parse_valueদিয়ে। মানটা list বা object হলেparse_arrayবাparse_objectভিতরের প্রতিটা item-এর জন্য আবারparse_value-কে call করে। Stack ফুরানোর আগেই ওরCJSON_NESTING_LIMIT, মানে 1000, call থামিয়ে দেয়। - GCC-র নিজের C parser। 2006-এর GCC 4.1 থেকে GCC হাতে লেখা একটা recursive-descent parser দিয়ে C পড়ে। বন্ধনীর ভিতরের expression পড়া হয় পুরো expression পড়ার সেই একই function দিয়ে, শুধু এক call গভীরে।
- Screen-এ fractal। Python-এর
turtledemopackage-এfractalcurvesনামে একটা demo আছে, যেটা Koch curve আঁকে। ওর function নিজেকেই চারবার call করে, প্রতিবার তিন ভাগের এক ভাগ লম্বা রেখার জন্য। Depth 0 হলে সোজা একটা রেখা আঁকে: এটাই base case।
যে ভুলগুলো সবাই করে
১. Step যখন base case টপকে যায়।
long long every_other(int n)
{
if (n == 0) {
return 0;
}
return n + every_other(n - 2);
}
কোনো command line-এই বার্তা নেই। 6 থেকে ঠিকঠাক চলে, কিন্তু 7 থেকে যায় 7, 5, 3, 1, -1, আর কখনো 0-তে নামে না। -O2-তে GCC 12 7-এর call-টাকে Bob-এর মতোই অশেষ লাফ বানিয়ে দিয়েছে। পরীক্ষা লেখো n <= 0, তাহলে 0 টপকে যাওয়া step-ও ধরা পড়ে। তুমি == 0 লিখবে, কারণ প্রথম যে input দিয়ে চালিয়েছিলে, তাতে ঠিক কাজ করেছিল।
২. Recursive লাইনে return নেই।
long long sum_to(int n)
{
if (n == 0) {
return 0;
}
n + sum_to(n - 1);
}
Playground-এ নীরব। নিজের মেশিনে GCC 12-এর gcc -Wall বলে warning: value computed is not used [-Wunused-value] আর warning: control reaches end of non-void function [-Wreturn-type]। যোগফলটা হিসাব হয়, তারপর ফেলে দেওয়া হয়, তাই main এরপর যা print করে, সেটা undefined behaviour। লেখো return n + sum_to(n - 1);। return তুমি ভুলবে, কারণ ওটা ছাড়াও অঙ্কটা পড়তে ঠিকঠাক লাগে।
৩. 0 বৈধ input, অথচ base case 1-এ।
long long factorial(int n)
{
if (n == 1) {
return 1;
}
return n * factorial(n - 1);
}
n input থেকে এলে কোনো command line-এই বার্তা নেই। 1 থেকে 20 পর্যন্ত ঠিক, কিন্তু factorial(0) চলে যায় -1, -2 আর আরও নিচে, 1 থেকে দূরে। শেষে int overflow করে, যেটা undefined behaviour, তাই এরপর কী হবে তার কোনো কথাই C দেয় না। Compiler Explorer-এর GCC 12-এ, Playground-এর -O2-তে, পুরো program একবার run করে দেখা গেছে: input 0-তে লেগেছে 7247 ms, আর উত্তর এসেছে ভুল।
Base case বসাও 0-তে, মানে সবচেয়ে ছোট বৈধ input-এ। তুমি 1 বেছে নেবে, কারণ 1! তো 1, আর গুণফল যেন ওখান থেকেই শুরু হয়।
Bob এবার ওর যোগফল একেবারে ঠিক করবে। Recursion দিয়ে long long sum_to(int n) লেখো, আর player যে n টাইপ করে, তার যোগফল print করো।
Input. এক লাইনে একটা পূর্ণসংখ্যা n।
Output. এক লাইনে 1 + 2 + ... + n।
Constraints. 0 <= n <= 10000।
Sample. Input 3 দিলে 6। Input 0 দিলে 0।
#include <stdio.h>
long long sum_to(int n);
int main(void)
{
int n = 0;
scanf("%d", &n);
printf("%lld\n", sum_to(n));
return 0;
}
long long sum_to(int n)
{
/* The base case comes first: what is the sum of the numbers up to 0?
Then return n plus one call with a smaller n. */
return 0;
}
আলাদা করে গ্রেড হয় না। Module 6-এর sum-to-n ঠিক এই output-টাই গ্রেড করে, আর judge শুধু output দেখে।
Alice গণিত ক্লাবের জন্য factorial print করে। সদস্যরা একটার পর একটা সংখ্যা বলতে থাকে, যতক্ষণ না ফুরায়, আর Alice প্রতিটার factorial print করে। Recursion দিয়ে long long factorial(int n) লেখো।
Input. Space বা নতুন লাইন দিয়ে আলাদা করা পূর্ণসংখ্যা, input-এর শেষ পর্যন্ত। অন্তত একটা থাকবেই।
Output. প্রতিটা পূর্ণসংখ্যা n-এর জন্য এক লাইনে n!, যে ক্রমে পড়া হয়েছে সেই ক্রমে।
Constraints. সর্বোচ্চ 21টা পূর্ণসংখ্যা, প্রতিটা 0 <= n <= 20।
Sample. Input 0 5 20 দিলে তিন লাইনে 1, 120 আর 2432902008176640000।
#include <stdio.h>
long long factorial(int n);
int main(void)
{
int n = 0;
while (scanf("%d", &n) == 1) {
printf("%lld\n", factorial(n));
}
return 0;
}
long long factorial(int n)
{
/* The base case comes first. Which n is the smallest one allowed?
Every other n is n times the factorial of n - 1. */
return 0;
}
factorials নামে গ্রেড হয়। Hidden test-এ আছে একা একটা 0, 13 (INT_MAX পার হওয়া প্রথম factorial), আর 20।
Bob-এর countdown 1 পর্যন্ত নামে, তারপর যেখান থেকে শুরু করেছিল সেখানে উঠে আসে। ও এটা চায় একটাই function থেকে, যেটা নিজের call-এর দুই পাশেই print করে। void down_and_up(int n) লেখো।
Input. এক লাইনে একটা পূর্ণসংখ্যা n।
Output. n, n - 1, ..., 1, তারপর 1, 2, ..., n, প্রতি লাইনে একটা সংখ্যা। তাই 1 আসে দুইবার।
Constraints. 1 <= n <= 1000।
Sample. Input 3 দিলে ছয় লাইনে 3, 2, 1, 1, 2 আর 3।
#include <stdio.h>
void down_and_up(int n);
int main(void)
{
int n = 0;
scanf("%d", &n);
down_and_up(n);
return 0;
}
void down_and_up(int n)
{
/* One function that prints on both sides of its own call.
How many times must 1 be printed? */
}
down-and-up নামে গ্রেড হয়। Hidden test-এ আছে n = 1, যেখানে 1 দুইবার print করতে হবে, আর n = 1000।
সচরাচর যে প্রশ্নগুলো আসে
Bob stack overflow না পেয়ে টাইম লিমিট পার হয়েছে পেল কেন?
-O2-তে GCC 12 ওর recursion-কে একটা loop বানিয়ে দিয়েছে, আর loop-এর প্রতি pass-এ নতুন memory লাগে না। "Stack overflow" এমন কোনো বার্তা C আসলে print-ই করে না। Optimisation ছাড়া build করলে ঠিকই crash করে,-O0-এর run-এ যেমন দেখলে।Recursion কি loop-এর চেয়ে ধীর?
একটা আসল call-এর খরচ loop-এর এক pass-এর চেয়ে একটু বেশি। কিন্তু GCC 12 প্রায়ই সহজ recursion থেকে call-গুলো সরিয়ে দেয়। Playground-এ recursive
sum_to100000000-এর উত্তর দিয়েছে 70 ms-এ। দুইটাই মেপে দেখায় lesson 4।প্রতিটা call-ই n নামটা ব্যবহার করে। তাহলে প্রতিটা call নিজের n আলাদা রাখে কীভাবে?
প্রতিটা call ওর parameter-এর জন্য নতুন একটা বাক্স পায়, argument-এর একটা copy দিয়ে ভরা (Module 7)। তাই
sum_to(3)n নামের চারটা বাক্স বানায়, প্রতি frame-এ একটা। Lesson 2 ওগুলো এঁকে দেখায়।Base case কি 0-ই হতে হবে?
না। এটা সেই সবচেয়ে ছোট input, যার উত্তর function-কে দিতে হবে। Factorial-এর base case 0, কারণ 0! জিজ্ঞেস করা একদম ন্যায্য প্রশ্ন। যে function-কে কখনো 1-এর কম কিছু জিজ্ঞেস করা হয় না, সেটা 1-এ থামতে পারে।
মূল কথা
- Recursive function একটা ছোট সমস্যা নিয়ে নিজেকেই call করে, আর প্রতিটা call নিজের করা call-এর অপেক্ষায় থাকে।
- এর লাগে একটা base case, যেটা call ছাড়াই উত্তর দেয় আর সবার আগে পরীক্ষা হয়, আর একটা step, যেটা ওর দিকে এগোয়।
- Frame table-এ প্রতিটা call-এর জন্য এক সারি: call নামে সারি ধরে নিচে, উত্তর ওঠে উপরে।
- Call-এর আগের কাজ হয় নামার পথে; পরের কাজ হয় ফেরার পথে, উল্টো ক্রমে।
factorial-এর লাগেlong long:int-এ শেষ যেটা এঁটে যায় সেটা 12!, আরlong long-এ 20!।- Base case না থাকলে সেটা time, output বা memory limit-এর মতো দেখায়; GCC 12-এর
gcc -Wallআসল নামটা বলে দেয়।
এরপর Amara কাগজে frame আঁকে, প্রতি call-এ একটা বাক্স, আর Run চাপার আগেই output বলে দেয়।
lesson ১ শেষ
শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।
পরেরটা: Call stack, এক frame করে