Competitive Coding
Unit 3: Recursion & Advanced Techniques
From base cases to backtracking โ master recursive thinking, optimise with memoization, and solve competition-level problems that unlock high-paying tech careers.
โฑ๏ธ Time: 7 hrs theory + 5 hrs practice | ๐ฐ Earning: โน8Kโโน30K/month | ๐ 30 MCQs (Bloom's Mapped)
๐ผ Jobs this unlocks: SDE-1 (โน8โ15 LPA) | Competitive Programmer (โน10โ25 LPA) | Algorithm Engineer (โน12โ30 LPA)
Opening Hook โ The Invisible Power Behind Every Search
๐บ๏ธ How Google Maps Finds the Shortest Path in Milliseconds
Every time you open Google Maps and ask for directions, a recursive algorithm called Depth-First Search (DFS) explores a graph of 1 billion+ nodes โ intersections, roads, and landmarks across the planet. The algorithm recursively explores each path, backtracks when it hits a dead end, and finds the optimal route in under 200 milliseconds. That's recursion at planetary scale.
But recursion isn't just for tech giants. Every file explorer on your laptop โ Windows Explorer, macOS Finder, or Linux's ls -R โ uses recursive directory traversal. When you click "Search this PC," the OS recursively enters each folder, checks its contents, enters sub-folders, checks again... until every nested file is found. That's a recursive function calling itself on every subdirectory.
What if YOU could think recursively? What if you could take a massive problem, break it into identical smaller pieces, and let the computer solve billions of sub-problems automatically? That's exactly what this chapter teaches you โ the most elegant and powerful problem-solving technique in all of computer science.
Learning Outcomes โ Bloom's Taxonomy Mapped (12 Outcomes)
| Bloom's Level | Learning Outcome |
|---|---|
| ๐ต Remember | Define recursion and identify the two essential components: base case and recursive case |
| ๐ต Remember | List and distinguish the types of recursion: direct, indirect, tail, and non-tail |
| ๐ต Understand | Explain how the call stack works during recursive function execution, including push/pop of stack frames |
| ๐ต Understand | Describe how memoization eliminates overlapping subproblems and reduces exponential time to linear |
| ๐ข Apply | Write recursive solutions for factorial, Fibonacci, power(x,n), and sum-of-digits problems in C++ and Python |
| ๐ข Apply | Implement a backtracking solution for the N-Queens problem with constraint checking |
| ๐ข Analyze | Compare tail recursion vs non-tail recursion in terms of stack usage, performance, and compiler optimisation |
| ๐ข Analyze | Analyze the time complexity difference between naive recursive Fibonacci O(2โฟ) and memoized Fibonacci O(n) |
| ๐ Evaluate | Judge when recursion is a better choice than iteration, considering readability, performance, and stack limits |
| ๐ Evaluate | Evaluate the trade-offs between top-down memoization and bottom-up tabulation for dynamic programming |
| ๐ Create | Design a recursive Sudoku solver that uses backtracking with constraint propagation |
| ๐ Create | Build a complete backtracking solution for the subset-sum problem with pruning optimisation |
Concept Explanation โ Recursion & Advanced Techniques from Scratch
1. Introduction to Recursion
Plain English: Recursion is when a function calls itself to solve a smaller version of the same problem. Imagine you're standing in a queue and want to know your position. You ask the person in front, "What's your position?" They ask the person in front of them, and so on โ until the first person says "I'm #1." Then the answers cascade back: #2, #3, #4... That's recursion.
Technical Definition: A recursive function is a function that calls itself with modified arguments, working toward a base case that terminates the recursion. Every recursive function has exactly two components:
๐ The Two Essential Components of Recursion
1. Base Case (Termination Condition): The condition under which the function stops calling itself and returns a known value directly. Without this, the function recurses forever.
2. Recursive Case (Self-call): The function calls itself with a smaller or simpler input, moving closer to the base case with each call.
Factorial Example โ The "Hello World" of Recursion
The factorial of n (written n!) is: n! = n ร (n-1) ร (n-2) ร ... ร 1, with 0! = 1.
C++ // Recursive Factorial in C++ #include <iostream> using namespace std; int factorial(int n) { // Base case: 0! = 1 if (n == 0) return 1; // Recursive case: n! = n * (n-1)! return n * factorial(n - 1); } int main() { cout << "5! = " << factorial(5) << endl; return 0; }
Python # Recursive Factorial in Python def factorial(n): # Base case if n == 0: return 1 # Recursive case return n * factorial(n - 1) print(f"5! = {factorial(5)}")
How the Call Stack Works โ ASCII Stack Diagram
When factorial(4) is called, each recursive call is pushed onto the call stack. When the base case is reached, results pop back up:
2. Base Condition โ The Emergency Brake of Recursion
The base case is the most critical part of any recursive function. It's the condition that says "STOP โ don't call yourself again." Without it, the function calls itself infinitely until the program crashes.
What Happens Without a Base Case?
C++ // โ DANGEROUS: No base case! void infinite(int n) { cout << n << " "; infinite(n - 1); // Never stops! } // Output: 5 4 3 2 1 0 -1 -2 ... CRASH! // Error: Stack Overflow (Segmentation Fault)
Stack Overflow Error: Each recursive call creates a new stack frame in memory. Without a base case, frames pile up endlessly until the stack memory is exhausted. On most systems, the default stack size is 1โ8 MB, which allows roughly 10,000โ100,000 recursive calls before crashing.
Rules for Writing Good Base Cases
| Rule | Example |
|---|---|
| Base case must be reachable | factorial(n): n decreases by 1 each call, eventually reaching 0 |
| Base case must return a known value | factorial(0) = 1 โ a known mathematical fact |
| Arguments must move toward the base case | If base is n=0, each call must use n-1, not n+1 |
| Sometimes you need multiple base cases | Fibonacci: fib(0)=0 AND fib(1)=1 |
3. Solving Problems Using Recursion
The Recursive Thinking Pattern: Every recursive solution follows this mental model:
- Assume the recursive call magically gives the correct answer for a smaller input
- Use that answer to build the solution for the current input
- Define the base case where you know the answer directly
Problem 1: Fibonacci Numbers
The Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21... Each number is the sum of the two preceding ones.
C++ // Recursive Fibonacci int fib(int n) { if (n <= 0) return 0; // Base case 1 if (n == 1) return 1; // Base case 2 return fib(n-1) + fib(n-2); // Recursive case }
Python # Recursive Fibonacci def fib(n): if n <= 0: return 0 if n == 1: return 1 return fib(n-1) + fib(n-2)
Recursion Tree for fib(5)
Problem 2: Power(x, n) โ Two Approaches
C++ // Naive: O(n) time double power_naive(double x, int n) { if (n == 0) return 1; return x * power_naive(x, n - 1); } // Optimised: O(log n) time โ using divide and conquer double power_fast(double x, int n) { if (n == 0) return 1; double half = power_fast(x, n / 2); if (n % 2 == 0) return half * half; else return half * half * x; }
Problem 3: Sum of Digits
Python # Sum of digits of a number def sum_digits(n): if n == 0: return 0 # Base case return (n % 10) + sum_digits(n // 10) # Last digit + sum of rest print(sum_digits(9876)) # 9+8+7+6 = 30
4. Classic and Modern Approaches
Classic Recursion: A function calls itself directly to solve progressively smaller instances. Examples: factorial, fibonacci, sum-of-digits. The pattern is straightforward โ each call reduces the problem by a small amount (usually by 1).
Divide and Conquer: A more powerful pattern where the problem is split into independent subproblems of roughly equal size, solved recursively, and then combined. This often leads to O(n log n) algorithms instead of O(nยฒ).
| Aspect | Classic Recursion | Divide & Conquer |
|---|---|---|
| Problem split | Reduce by 1 (or small constant) | Split into 2+ equal halves |
| Subproblems | 1 subproblem | 2+ independent subproblems |
| Typical complexity | O(n) or O(2โฟ) | O(n log n) |
| Examples | Factorial, Fibonacci, Sum of digits | Merge Sort, Quick Sort, Binary Search |
Binary Search โ Recursive Version
C++ // Recursive Binary Search โ O(log n) int binarySearch(int arr[], int low, int high, int target) { if (low > high) return -1; // Base case: not found int mid = low + (high - low) / 2; if (arr[mid] == target) return mid; // Found! if (arr[mid] > target) return binarySearch(arr, low, mid - 1, target); else return binarySearch(arr, mid + 1, high, target); }
Python # Recursive Binary Search def binary_search(arr, low, high, target): if low > high: return -1 mid = (low + high) // 2 if arr[mid] == target: return mid elif arr[mid] > target: return binary_search(arr, low, mid - 1, target) else: return binary_search(arr, mid + 1, high, target)
5. Direct vs Indirect Recursion
Direct Recursion: A function calls itself directly. This is the most common form โ factorial, fibonacci, binary search all use direct recursion.
Indirect Recursion: Function A calls function B, and function B calls function A back. The recursion happens through a chain of two or more functions.
Direct Recursion Example
C++ // Direct: function calls itself void countdown(int n) { if (n == 0) { cout << "Go!" << endl; return; } cout << n << "... "; countdown(n - 1); // Calls ITSELF }
Indirect Recursion Example
C++ // Indirect: A calls B, B calls A void funcB(int n); // Forward declaration void funcA(int n) { if (n <= 0) return; cout << n << " "; funcB(n - 1); // A calls B } void funcB(int n) { if (n <= 1) return; cout << n << " "; funcA(n / 2); // B calls A } // funcA(10) โ prints: 10 9 4 3 1
| Property | Direct Recursion | Indirect Recursion |
|---|---|---|
| Call pattern | A โ A โ A โ ... | A โ B โ A โ B โ ... |
| Readability | Easy to trace | Harder to trace |
| Common usage | Most recursive problems | Even/odd checks, mutual parsers, state machines |
| Debugging | Straightforward | Requires tracing multiple functions |
| Stack behaviour | Frames of one function | Frames of multiple functions interleaved |
6. Tail vs Non-Tail Recursion
Tail Recursion: A recursive call is in tail position when it is the very last operation in the function โ nothing is done after the recursive call returns. Compilers can optimise tail-recursive functions into loops (called Tail-Call Optimisation / TCO), eliminating stack frame overhead.
Non-Tail Recursion: After the recursive call returns, there's still work to do (like multiplication in factorial). Each call must keep its stack frame alive until the child returns.
Comparison: Non-Tail vs Tail Factorial
C++ // NON-TAIL Recursive Factorial // After factorial(n-1) returns, we still multiply by n int factorial_nontail(int n) { if (n == 0) return 1; return n * factorial_nontail(n - 1); // โ multiplication AFTER call } // TAIL Recursive Factorial // The recursive call is the LAST operation โ result is accumulated int factorial_tail(int n, int acc = 1) { if (n == 0) return acc; return factorial_tail(n - 1, n * acc); // โ call is LAST operation }
Python # Non-tail factorial def fact_nontail(n): if n == 0: return 1 return n * fact_nontail(n - 1) # multiply AFTER return # Tail factorial (Python doesn't optimise this, but conceptually correct) def fact_tail(n, acc=1): if n == 0: return acc return fact_tail(n - 1, n * acc) # call is LAST
| Feature | Non-Tail Recursion | Tail Recursion |
|---|---|---|
| Last operation | Computation after recursive call | Recursive call IS the last operation |
| Stack frames | O(n) frames kept alive | O(1) with TCO (reuses single frame) |
| Compiler optimisation | Cannot be optimised to loop | Can be optimised to loop (TCO) |
| Accumulator parameter | Not needed | Required to carry partial results |
| Supported in | All languages | C/C++ (with -O2), Scheme, Scala (not Python/Java) |
sys.setrecursionlimit()). For deep recursion in Python, convert to iteration. C++ with -O2 flag enables TCO.
7. Memory Allocation in Recursion
Every recursive call creates a new stack frame in the program's call stack. Each frame contains:
- Local variables of the function
- Function parameters
- Return address (where to go back after the function completes)
- Saved registers
Stack Frame Diagram for factorial(3)
Why Stack Overflow Happens: If n = 1,000,000 and you call factorial(1000000), the program needs 1 million stack frames ร ~50 bytes each = ~50 MB. But the stack is only ~1 MB. Result: Stack Overflow โ the program crashes.
-Wl,-z,stacksize=67108864 for 64 MB stack. In Python: import sys; sys.setrecursionlimit(100000) and use import threading; threading.stack_size(67108864).
8. Advantages & Disadvantages โ Recursion vs Iteration
| Parameter | Recursion | Iteration |
|---|---|---|
| Code Readability | โ Clean, elegant, mirrors mathematical definition | โ ๏ธ Can be verbose with explicit stacks |
| Time Complexity | โ ๏ธ Can be exponential without memoization (e.g., O(2โฟ) for naive Fibonacci) | โ Usually linear โ no repeated subproblems |
| Space Complexity | โ O(n) stack space for depth n | โ O(1) โ no extra stack frames |
| Debugging Ease | โ ๏ธ Hard to trace deep recursion; stack traces can be long | โ Easy to step through with debugger |
| Performance | โ Function call overhead (push/pop frames) | โ No overhead โ direct loop instructions |
| Problem Suitability | โ Trees, graphs, backtracking, divide-and-conquer | โ Simple loops, array traversals, counting |
9. Backtracking
Backtracking is a refined form of recursion where you build a solution incrementally, one piece at a time, and abandon ("backtrack") a path as soon as you determine it cannot lead to a valid solution. Think of it as exploring a maze: walk forward, hit a dead end, go back to the last junction, try another path.
The N-Queens Problem
Problem: Place N queens on an NรN chessboard such that no two queens threaten each other (no two in the same row, column, or diagonal).
Complete Board Trace for 4-Queens
N-Queens Code
C++ #include <iostream> #include <vector> using namespace std; bool isSafe(vector<vector<int>> &board, int row, int col, int n) { // Check column above for (int i = 0; i < row; i++) if (board[i][col]) return false; // Check upper-left diagonal for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) if (board[i][j]) return false; // Check upper-right diagonal for (int i=row-1, j=col+1; i>=0 && j<n; i--, j++) if (board[i][j]) return false; return true; } bool solveNQueens(vector<vector<int>> &board, int row, int n) { if (row == n) return true; // All queens placed! for (int col = 0; col < n; col++) { if (isSafe(board, row, col, n)) { board[row][col] = 1; // Place queen if (solveNQueens(board, row+1, n)) return true; board[row][col] = 0; // BACKTRACK: remove queen } } return false; // No valid position in this row } int main() { int n = 4; vector<vector<int>> board(n, vector<int>(n, 0)); if (solveNQueens(board, 0, n)) { for (auto &row : board) { for (int cell : row) cout << (cell ? "Q " : ". "); cout << endl; } } return 0; }
Sudoku Solver Concept
A Sudoku solver uses the same backtracking pattern: find an empty cell, try digits 1โ9, check validity (row, column, 3ร3 box), recurse to next cell. If no digit works, backtrack to the previous cell and try the next digit. The recursion tree can have up to 9โธยน nodes, but constraint checking prunes most branches early.
Subset Sum Problem
Problem: Given a set of integers and a target sum, find if any subset sums to the target.
C++ bool subsetSum(vector<int> &arr, int idx, int target) { if (target == 0) return true; // Found a valid subset! if (idx < 0 || target < 0) return false; // Include arr[idx] OR exclude it return subsetSum(arr, idx-1, target-arr[idx]) // Include || subsetSum(arr, idx-1, target); // Exclude }
Generating All Permutations
C++ void permute(vector<int> &arr, int start, int end) { if (start == end) { for (int x : arr) cout << x << " "; cout << endl; return; } for (int i = start; i <= end; i++) { swap(arr[start], arr[i]); // Choose permute(arr, start + 1, end); // Explore swap(arr[start], arr[i]); // Un-choose (backtrack) } }
Recursion Tree for Permutations of {1, 2, 3}
10. Memoization โ Caching Recursive Results
The Problem: Look at the Fibonacci recursion tree above. fib(3) is computed 2 times. fib(2) is computed 3 times. For fib(50), there are over 2โตโฐ โ 10ยนโต redundant calls. This is catastrophically slow.
The Solution โ Memoization: Store the result of each subproblem the first time it's computed. If the same subproblem is encountered again, return the cached result instead of recomputing. This is called memoization (from "memo" โ a note to yourself).
Fibonacci Without vs With Memoization
C++ #include <iostream> #include <unordered_map> using namespace std; unordered_map<int, long long> memo; long long fib_memo(int n) { if (n <= 0) return 0; if (n == 1) return 1; if (memo.count(n)) return memo[n]; // Return cached result memo[n] = fib_memo(n-1) + fib_memo(n-2); // Compute & store return memo[n]; } int main() { cout << "fib(50) = " << fib_memo(50) << endl; return 0; }
Python # Memoized Fibonacci using dictionary def fib_memo(n, memo={}): if n <= 0: return 0 if n == 1: return 1 if n in memo: return memo[n] memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo) return memo[n] print(f"fib(50) = {fib_memo(50)}")
| Metric | Naive Fibonacci | Memoized Fibonacci |
|---|---|---|
| Time Complexity | O(2โฟ) | O(n) |
| Space Complexity | O(n) stack | O(n) stack + O(n) cache |
| fib(40) time | ~55 seconds | ~0.00001 seconds |
| fib(50) feasibility | โ Takes hours | โ Instant |
11. Classic Recursive Problems
Problem 1: Next Happy Number
๐ข Next Happy Number
A happy number is a number where repeatedly summing the squares of its digits eventually reaches 1. Example: 19 โ 1ยฒ+9ยฒ = 82 โ 8ยฒ+2ยฒ = 68 โ 6ยฒ+8ยฒ = 100 โ 1ยฒ+0ยฒ+0ยฒ = 1 โ . Given a number N, find the next happy number after N.
APPROACH1. For each candidate starting from N+1, check if it's happy. 2. To check happiness: recursively sum squares of digits. Use a set to detect cycles (if we see a number again, it's not happy). 3. Return the first happy number found.
C++ #include <iostream> #include <unordered_set> using namespace std; int sumOfSquares(int n) { if (n == 0) return 0; int digit = n % 10; return digit * digit + sumOfSquares(n / 10); } bool isHappy(int n, unordered_set<int> &seen) { if (n == 1) return true; if (seen.count(n)) return false; // Cycle detected seen.insert(n); return isHappy(sumOfSquares(n), seen); } int nextHappy(int n) { int candidate = n + 1; while (true) { unordered_set<int> seen; if (isHappy(candidate, seen)) return candidate; candidate++; } } int main() { cout << "Next happy after 6: " << nextHappy(6) << endl; cout << "Next happy after 19: " << nextHappy(19) << endl; return 0; }
Complexity: Time: O(k ร log n) per candidate, where k is the cycle length. Space: O(log n) for the set.
Problem 2: Sum String
๐ค Sum String
A string is called a sum-string if the rightmost substring can be written as the sum of the two preceding substrings, and this property holds recursively. Example: "12358132134" โ 1, 2, 3, 5, 8, 13, 21, 34 (each number is sum of two previous). Given a numeric string, check if it is a sum string.
APPROACH1. Try all possible lengths for the first two numbers. 2. For each pair, compute their sum and check if the string starts with that sum. 3. If yes, recursively check the remaining string with the second number and the sum as the new pair. 4. Base case: if we've consumed the entire string, return true.
C++ #include <iostream> #include <string> using namespace std; string addStrings(string a, string b) { string result; int carry = 0, i = a.size()-1, j = b.size()-1; while (i >= 0 || j >= 0 || carry) { int sum = carry; if (i >= 0) sum += a[i--] - '0'; if (j >= 0) sum += b[j--] - '0'; result = char(sum % 10 + '0') + result; carry = sum / 10; } return result; } bool checkSum(string s, int len1, int len2) { string s1 = s.substr(0, len1); string s2 = s.substr(len1, len2); string sum = addStrings(s1, s2); int sumLen = sum.size(); if (len1 + len2 + sumLen > s.size()) return false; if (s.substr(len1 + len2, sumLen) != sum) return false; if (len1 + len2 + sumLen == s.size()) return true; return checkSum(s.substr(len1), len2, sumLen); } bool isSumString(string s) { int n = s.size(); for (int i = 1; i < n; i++) for (int j = 1; i+j < n; j++) if (checkSum(s, i, j)) return true; return false; } int main() { cout << isSumString("12358132134") << endl; // 1 cout << isSumString("1111112223") << endl; // 1 return 0; }
Complexity: Time: O(nยณ) in worst case. Space: O(n) for string operations.
Problem 3: Water Overflow (Champagne Tower)
๐ฅ Water Overflow โ Champagne Tower Problem
Glasses are arranged in a Pascal triangle formation. K litres of water are poured into the topmost glass. When a glass overflows, excess water splits equally to the two glasses directly below it. Given K litres poured, find how much water is in the j-th glass of the i-th row (0-indexed).
APPROACHSimulate the pouring process. Use a 2D array. Pour K litres into glass[0][0]. For each glass, if it has more than 1 litre, the excess overflows equally to the two glasses below. Process row by row.
C++ #include <iostream> #include <vector> #include <algorithm> using namespace std; double champagneTower(int poured, int query_row, int query_glass) { vector<vector<double>> tower(101, vector<double>(101, 0.0)); tower[0][0] = (double)poured; for (int row = 0; row <= query_row; row++) { for (int col = 0; col <= row; col++) { double overflow = (tower[row][col] - 1.0) / 2.0; if (overflow > 0) { tower[row+1][col] += overflow; tower[row+1][col+1] += overflow; } } } return min(1.0, tower[query_row][query_glass]); } int main() { cout << "Row 2, Glass 1 (pour 4): " << champagneTower(4, 2, 1) << endl; cout << "Row 1, Glass 0 (pour 2): " << champagneTower(2, 1, 0) << endl; return 0; }
Complexity: Time: O(rowยฒ). Space: O(rowยฒ) for the 2D array.
Learn by Doing โ 3-Tier Lab Structure
๐ข Tier 1 โ GUIDED TASK: Factorial & Fibonacci โ Your First Recursive Programs
Step 1: Set Up Your Environment
Open your C++ IDE (VS Code, CodeBlocks, or online: ideone.com / repl.it). Create a new file recursion_basics.cpp.
Step 2: Write Recursive Factorial
Type the factorial function from Section C. Add print statements to trace each call:
C++ #include <iostream> using namespace std; int factorial(int n) { cout << "Called factorial(" << n << ")" << endl; if (n == 0) { cout << " Base case! Returning 1" << endl; return 1; } int result = n * factorial(n - 1); cout << " factorial(" << n << ") = " << result << endl; return result; } int main() { cout << "Answer: " << factorial(5) << endl; return 0; }
Step 3: Run and Observe the Trace
Compile and run. You should see the cascade of calls going down (push), then results coming back up (pop). Draw this on paper โ draw boxes stacked on top of each other for each call.
Step 4: Write Recursive Fibonacci with Trace
C++ int fib(int n, int depth = 0) { for(int i=0;i<depth;i++) cout << " "; // Indent for depth cout << "fib(" << n << ")" << endl; if (n <= 0) return 0; if (n == 1) return 1; return fib(n-1, depth+1) + fib(n-2, depth+1); }
Step 5: Count the Calls
Run fib(5) and count how many times each subproblem is computed. Verify it matches the recursion tree diagram from Section C. Then add memoization and observe the dramatic reduction in calls.
๐ Congratulations! You've implemented and traced your first recursive programs. You now understand the call stack intuitively.
๐ก Tier 2 โ SEMI-GUIDED TASK: Solve the N-Queens Problem
Your Mission:
Implement the N-Queens solver for N=8. Print all 92 solutions, with each solution showing the board configuration.
Hints:
- Data Structure: Use a 1D array
queen[i] = colwhere row i has queen at columncol. This automatically ensures one queen per row. - Safety Check: Two queens at (r1, c1) and (r2, c2) attack each other if:
c1 == c2(same column) OR|r1-r2| == |c1-c2|(same diagonal). - Backtracking Pattern: For each row, try every column. If safe, place and recurse to next row. If stuck, backtrack.
- Counting: Use a global counter. Increment when a complete solution is found (row == N).
- Print Board: For each solution, print the board with 'Q' and '.' characters.
๐ด Tier 3 โ OPEN CHALLENGE: Build a Complete Sudoku Solver
The Brief:
Build a Sudoku solver that takes a 9ร9 grid (0 = empty) and fills in all missing numbers using backtracking. Your solver must:
- Find the next empty cell (left to right, top to bottom)
- Try digits 1โ9 in that cell
- Check validity: no duplicate in same row, column, or 3ร3 sub-box
- If valid, recurse to next empty cell
- If no digit works, backtrack (set cell back to 0)
- Print the completed grid when solved
Test with this puzzle:
Input
5 3 0 | 0 7 0 | 0 0 0
6 0 0 | 1 9 5 | 0 0 0
0 9 8 | 0 0 0 | 0 6 0
------+-------+------
8 0 0 | 0 6 0 | 0 0 3
4 0 0 | 8 0 3 | 0 0 1
7 0 0 | 0 2 0 | 0 0 6
------+-------+------
0 6 0 | 0 0 0 | 2 8 0
0 0 0 | 4 1 9 | 0 0 5
0 0 0 | 0 8 0 | 0 7 9
Problem Set โ Practice Problems
Tracing Problems โ Draw the Recursion Tree (5)
| # | Problem | Difficulty |
|---|---|---|
| T1 | Trace factorial(6): Draw the complete call stack at maximum depth. Show each frame's local variable n and return value. | Beginner |
| T2 | Draw the full recursion tree for fib(6). Count total nodes. Identify which subproblems are computed more than once. | Beginner |
| T3 | Trace power_fast(2, 10). Show each recursive call, the value of half, and whether n is even or odd at each step. | Intermediate |
| T4 | Draw the backtracking tree for placing 4 queens on a 4ร4 board. Show all attempted placements and backtracks until the first solution. | Intermediate |
| T5 | Trace permute({1,2,3}, 0, 2). Draw the complete recursion tree showing all swaps and the final permutation at each leaf node. | Advanced |
Programming Problems (8)
| # | Problem | Difficulty |
|---|---|---|
| P1 | Reverse a String Recursively: Write a function that reverses a string using recursion (no loops, no built-in reverse). Test with "Hello" โ "olleH". | Beginner |
| P2 | Tower of Hanoi: Implement the classic Tower of Hanoi for n disks. Print each move. Verify that n disks require 2โฟ - 1 moves. | Beginner |
| P3 | Check Palindrome Recursively: Write a recursive function to check if a string is a palindrome. Handle both even and odd length strings. | Beginner |
| P4 | Generate All Subsets: Given a set of n distinct integers, generate all 2โฟ subsets using recursion (include/exclude pattern). | Intermediate |
| P5 | Rat in a Maze: Given an nรn grid with blocked cells (0) and open cells (1), find a path from (0,0) to (n-1,n-1) using backtracking. Print the path. | Intermediate |
| P6 | Word Search: Given a 2D grid of characters and a word, check if the word exists in the grid by traversing adjacent cells (up/down/left/right). Each cell used at most once. | Intermediate |
| P7 | Combination Sum: Given an array of distinct integers and a target, find all unique combinations that sum to target. Each number may be used unlimited times. | Advanced |
| P8 | Regular Expression Matching: Implement a simplified regex matcher supporting '.' (any character) and '*' (zero or more of preceding) using recursion. | Advanced |
Industry Application Problems (3)
| # | Problem | Difficulty |
|---|---|---|
| I1 | File System Size Calculator: Write a recursive function that calculates the total size of a directory and all its subdirectories (simulated with a tree data structure). | Intermediate |
| I2 | JSON Parser (Nested Objects): Write a recursive descent parser that validates and pretty-prints nested JSON objects up to arbitrary depth. | Advanced |
| I3 | Network Packet Router: Simulate a recursive DFS-based packet routing algorithm that finds a path from source to destination in a network graph with 20 nodes. | Advanced |
Interview Problems (3)
| # | Problem | Company | Difficulty |
|---|---|---|---|
| V1 | Letter Combinations of a Phone Number: Given digits 2-9, return all possible letter combinations (like T9 keyboard). [LeetCode #17] | Amazon, Google | Intermediate |
| V2 | Generate Parentheses: Given n, generate all valid combinations of n pairs of parentheses. [LeetCode #22] | Microsoft, Facebook | Intermediate |
| V3 | Decode Ways: Given a string of digits, count the number of ways to decode it (A=1, B=2, ..., Z=26). Use memoization. [LeetCode #91] | Amazon, Flipkart | Advanced |
MCQ Assessment Bank โ 30 Questions (Bloom's Mapped)
Remember / Identify (Q1โQ5)
A recursive function must have:
- Only a base case
- Only a recursive case
- Both a base case and a recursive case
- Neither โ recursion works automatically
What is the base case for the recursive factorial function factorial(n)?
- n == 1
- n == 0
- n < 0
- n == n-1
In tail recursion, the recursive call is:
- The first statement in the function
- Inside a loop
- The last operation before the function returns
- Made to a different function
When function A calls function B and function B calls function A, this is called:
- Direct recursion
- Indirect recursion
- Tail recursion
- Mutual exclusion
The space complexity of a recursive function with depth n is typically:
- O(1)
- O(log n)
- O(n)
- O(nยฒ)
Understand / Explain (Q6โQ10)
Why does the naive recursive Fibonacci have O(2โฟ) time complexity?
- Because it uses a loop of size 2โฟ
- Because each call makes two more calls, creating a binary tree of calls
- Because the base case is reached after 2โฟ steps
- Because it uses an array of size 2โฟ
What happens when a recursive function has no base case?
- It returns 0
- It runs once and stops
- It calls itself infinitely until the stack overflows
- The compiler automatically adds a base case
How does memoization improve the Fibonacci function?
- It converts recursion to iteration
- It stores already-computed results to avoid redundant calculations
- It reduces the number of base cases
- It removes the recursive calls entirely
In the N-Queens problem, "backtracking" means:
- Removing all queens and starting over
- Undoing the last placement and trying the next option when a conflict is detected
- Sorting the queens by column
- Using dynamic programming to find the optimal placement
Why can tail-recursive functions be optimised by compilers but non-tail-recursive functions cannot?
- Tail-recursive functions use less memory
- The compiler can reuse the current stack frame since no work remains after the recursive call
- Tail-recursive functions are always faster
- Non-tail-recursive functions have bugs
Apply / Implement (Q11โQ15)
What is the output of the following code?
int f(int n) {
if (n <= 1) return n;
return f(n-1) + f(n-2);
}
cout << f(7);
- 7
- 13
- 21
- 8
What does this recursive function compute?
int mystery(int n) {
if (n == 0) return 0;
return (n % 10) + mystery(n / 10);
}
- Number of digits in n
- Sum of digits of n
- Reverse of n
- Product of digits of n
To convert the non-tail factorial to tail-recursive form, you need to add:
- A loop
- An accumulator parameter
- A global variable
- A second base case
factorial(n, acc)) stores the running product, making the recursive call the last operation.In a recursive binary search on a sorted array of 1024 elements, the maximum number of recursive calls is:
- 1024
- 512
- 10
- 11
What is the output of power_fast(3, 4) using the optimised O(log n) approach?
- 12
- 27
- 81
- 64
Analyze / Compare (Q16โQ20)
How many times is fib(2) computed in the naive recursive call to fib(6)?
- 3
- 5
- 8
- 13
Comparing recursion and iteration for computing factorial of n, which statement is TRUE?
- Recursion is always faster
- Iteration uses O(n) extra space
- Recursion uses O(n) stack space while iteration uses O(1)
- Both have the same space complexity
The N-Queens problem for an 8ร8 board has 92 solutions. What type of algorithm is best suited to find ALL solutions?
- Greedy
- Dynamic Programming
- Backtracking
- Binary Search
What is the time complexity of the subset-sum problem solved via naive recursion on an array of n elements?
- O(n)
- O(nยฒ)
- O(2โฟ)
- O(n!)
Memoized Fibonacci uses O(n) extra space for caching. Compared to the bottom-up tabulation approach:
- Memoization is always more space-efficient
- Both use O(n) space, but tabulation can be optimised to O(1) space
- Tabulation uses O(nยฒ) space
- Memoization uses O(1) space
Evaluate / Judge (Q21โQ25)
A student says: "Recursion is always better than iteration because it's more elegant." What is wrong with this claim?
- Nothing โ recursion is always better
- Recursion can be slower due to function call overhead and uses more stack space
- Iteration cannot solve the same problems as recursion
- Recursion doesn't work in C++
For which of these problems is recursion clearly the BEST approach?
- Finding the maximum in an array
- Traversing a tree data structure
- Summing numbers 1 to n
- Printing "Hello" 10 times
A recursive Fibonacci function works correctly for fib(30) but crashes for fib(100000). Which is the best fix?
- Increase the recursion limit to infinity
- Add more base cases
- Use memoization or convert to iterative bottom-up approach
- Use a faster computer
In a coding interview, you solve a problem with recursion but the interviewer asks about the space complexity. Your recursive solution uses O(n) stack space. What should you suggest?
- "O(n) is the best possible"
- "I can convert this to tail recursion for O(1) space with TCO, or rewrite iteratively"
- "Space complexity doesn't matter"
- "I'll use a bigger computer"
Which approach is better for the N-Queens problem: brute force (try all N^N placements) or backtracking?
- Brute force โ it's simpler to implement
- Backtracking โ it prunes invalid branches early, drastically reducing the search space
- Both are equally efficient
- Neither โ use dynamic programming instead
Create / Design (Q26โQ30)
To generate all permutations of n elements using recursion, the correct approach is:
- Sort the elements first, then print
- For each position, swap with remaining elements, recurse, then swap back (backtrack)
- Use nested loops of depth n
- Use binary search on each element
To add memoization to a recursive function, you need to:
- Add a loop inside the function
- Create a cache (array/map), check it before computing, and store results after computing
- Remove the base case
- Call the function from main() multiple times
To design a recursive Sudoku solver, the correct recursive structure is:
- Try all 9โธยน combinations at once
- Find empty cell โ try digits 1-9 โ check constraints โ recurse to next cell โ backtrack if stuck
- Fill row by row without checking constraints
- Use sorting to place numbers
To convert the following non-tail recursive function to tail-recursive form, the accumulator should track:
int sum(int n) {
if (n == 0) return 0;
return n + sum(n-1);
}
- The value of n
- The running total of the sum computed so far
- The number of recursive calls made
- The base case value
sum(n, acc=0) { if (n==0) return acc; return sum(n-1, acc+n); }. The accumulator carries the running total forward.You're designing a solution for the "generate all valid parentheses" problem. The recursive function should track:
- Only the number of opening brackets
- The count of opening and closing brackets used so far, and the current string
- Just the total length of the string
- A counter for the number of solutions
open_count (number of '(' used), close_count (number of ')' used), and the current string being built. Constraints: open_count โค n, close_count โค open_count.Short Answer Questions (8)
Q1. Define recursion and state its two essential components.
Answer: Recursion is a programming technique where a function calls itself to solve smaller instances of the same problem. The two essential components are: (1) Base case โ the condition that stops recursion and returns a known value directly, and (2) Recursive case โ the function calls itself with modified (smaller) arguments, making progress toward the base case.
Q2. What is the difference between direct and indirect recursion? Give one example of each.
Answer: In direct recursion, a function calls itself (e.g., factorial(n) calls factorial(n-1)). In indirect recursion, function A calls function B, and function B calls function A (e.g., isEven(n) calls isOdd(n-1), and isOdd(n) calls isEven(n-1)).
Q3. Why does naive recursive Fibonacci have exponential time complexity?
Answer: Each call to fib(n) makes two recursive calls: fib(n-1) and fib(n-2). This creates a binary tree of calls where the same subproblems are recomputed multiple times. The total number of calls grows approximately as 2โฟ, giving O(2โฟ) time complexity. For example, fib(5) makes 15 calls, but only 6 are unique subproblems.
Q4. Explain tail-call optimisation (TCO) in one paragraph.
Answer: Tail-Call Optimisation is a compiler technique where a tail-recursive function (one whose last operation is the recursive call) is transformed into an iterative loop. Since no work remains after the recursive call returns, the compiler can reuse the current stack frame instead of creating a new one. This reduces space from O(n) to O(1). TCO is supported in C/C++ (with -O2), Scheme, and Scala, but NOT in Python or Java.
Q5. What is backtracking? How does it differ from brute force?
Answer: Backtracking is a refined recursive technique that builds solutions incrementally and abandons (backtracks from) partial solutions as soon as they violate constraints. Unlike brute force, which tries ALL possible combinations, backtracking prunes invalid branches early. For N-Queens (N=8), brute force checks 8โธ = 16.7 million placements, while backtracking explores only ~15,720 nodes โ a 99.9% reduction.
Q6. What is memoization? How does it improve time complexity?
Answer: Memoization is an optimisation technique where the results of expensive recursive calls are cached (stored in a hash map or array). When the same subproblem is encountered again, the cached result is returned in O(1) instead of recomputing. For Fibonacci, memoization reduces time from O(2โฟ) to O(n) because each of the n subproblems is computed exactly once.
Q7. What causes a Stack Overflow error in recursion?
Answer: Each recursive call creates a new stack frame in memory. If the recursion is too deep (no base case, or input too large), stack frames accumulate beyond the available stack memory (typically 1โ8 MB). When the stack limit is exceeded, the program crashes with a Stack Overflow error (segmentation fault in C++, RecursionError in Python).
Q8. Give two advantages and two disadvantages of recursion over iteration.
Advantages: (1) Code is cleaner and more readable, especially for tree/graph problems. (2) Naturally expresses divide-and-conquer and backtracking algorithms. Disadvantages: (1) Uses O(n) stack space, risking stack overflow. (2) Function call overhead (pushing/popping frames) makes it slower than equivalent iteration for simple problems.
Long Answer Questions (3)
Q1. Explain the N-Queens problem and solve it for N=4 using backtracking. Show the complete trace with board states.
Answer Outline:
- Problem Definition: Place N queens on an NรN chessboard so that no two queens attack each other (no shared row, column, or diagonal).
- Why Backtracking: Brute force would check 4โด = 256 placements. Backtracking prunes most branches. For N=4, backtracking explores ~45 nodes vs 256.
- Algorithm: Place one queen per row. For each row, try each column. Check safety (column and diagonal conflicts). If safe, recurse to next row. If no column works, backtrack.
- Trace for 4-Queens: Start Row 0, Col 0. Try Row 1: Col 0 (conflict), Col 1 (conflict), Col 2 (safe). Try Row 2: all columns conflict โ BACKTRACK. Row 1: Col 3 (safe). Row 2: Col 1 (safe). Row 3: all conflict โ BACKTRACK further. Eventually place Q1 at (0,1), Q2 at (1,3), Q3 at (2,0), Q4 at (2,2) โ solution found.
- Two solutions exist for N=4: {(0,1),(1,3),(2,0),(3,2)} and {(0,2),(1,0),(2,3),(3,1)}.
- isSafe function: Check column (any queen in same column above), upper-left diagonal, upper-right diagonal.
- Time Complexity: O(N!) in the worst case. For N=8, only 92 solutions exist out of 8โธ โ 16.7M possible placements.
- Code: Provide the complete C++ implementation with isSafe() and solveNQueens() functions.
Q2. Compare memoization (top-down) and tabulation (bottom-up) approaches for solving the Fibonacci problem. Include code for both and analyze time/space complexity.
Answer Outline:
- Naive Fibonacci Problem: O(2โฟ) time due to overlapping subproblems. fib(5) computes fib(3) twice, fib(2) three times.
- Memoization (Top-Down): Start from fib(n), recursively break down, cache results. Code: use a dictionary/map. Time: O(n). Space: O(n) for cache + O(n) for stack = O(n).
- Tabulation (Bottom-Up): Start from fib(0), build up iteratively. Code: use an array. Time: O(n). Space: O(n) for array, but can be optimised to O(1) by keeping only last 2 values.
- Key Differences: Memoization is recursive (risk of stack overflow for large n), solves only needed subproblems. Tabulation is iterative (no stack risk), solves all subproblems from bottom.
- When to Use: Memoization when not all subproblems are needed (sparse problems). Tabulation when all subproblems are needed and space optimisation is critical.
- Provide both C++ and Python code for each approach.
- Performance comparison table: Time, space, stack risk, ease of implementation.
- Bridge to DP: Memoization is the entry point to Dynamic Programming. Mastering this makes DP (Unit 4) much easier.
Q3. Explain how memory is allocated during recursion. Draw the stack frame diagram for factorial(5) and discuss stack overflow prevention strategies.
Answer Outline:
- Stack Memory: Programs use a stack segment for function calls. Each call creates a "stack frame" containing local variables, parameters, return address, and saved registers.
- Stack Frame Diagram for factorial(5): Draw 6 frames (main + factorial(5) through factorial(0)). Show each frame's
nvalue and return value. Show the stack growing upward (or downward depending on convention). - Frame Size: Each frame is typically 32โ64 bytes. For factorial(5): 6 frames ร ~50 bytes = ~300 bytes. For factorial(100000): 100001 frames ร ~50 bytes = ~5 MB โ exceeds default stack size.
- Stack Overflow: Occurs when recursion depth exceeds stack memory. Default stack: ~1 MB (Windows/Linux). Maximum safe depth: ~10,000โ30,000 calls.
- Prevention Strategies: (a) Always ensure base case is reachable. (b) Use tail recursion with TCO. (c) Convert to iteration for deep recursion. (d) Increase stack size via compiler flags. (e) Use memoization to reduce redundant deep calls.
- Python-specific:
sys.setrecursionlimit()only raises the Python limit, not the OS stack. For truly deep recursion, usethreading.stack_size()or convert to iteration. - Real-world impact: Google Maps' DFS on billion-node graphs uses iterative DFS with explicit stack (a
std::stackon the heap) to avoid stack overflow.
Syllabus Lab
๐ Lab Status
No specific lab is prescribed from the syllabus for this unit.
However, the 3-tier labs in Section D provide comprehensive hands-on practice:
- Tier 1 (Guided): Factorial & Fibonacci with call tracing โ builds foundational understanding
- Tier 2 (Semi-Guided): N-Queens solver โ applies backtracking concepts
- Tier 3 (Open Challenge): Sudoku solver โ competition-level implementation
Complete all three tiers to build a strong recursion portfolio.
Industry Spotlight โ A Day in the Life
๐จโ๐ป Rohan Desai, 25 โ SDE-1 at Amazon India, Hyderabad
Background: B.Tech in Computer Science from a tier-2 college in Pune (PICT). No coding before college. Started competitive programming in 2nd year on Codeforces. Reached Expert rating (1600+) in 6 months. Practiced 500+ problems on LeetCode focusing on recursion, backtracking, and dynamic programming. Cleared Amazon's SDE-1 interview in 4th year โ 2 out of 3 coding rounds had recursion/backtracking problems.
A Typical Day:
9:30 AM โ Morning standup with the Amazon Logistics team. Review sprint tasks โ currently optimising the package routing algorithm.
10:00 AM โ Deep work: implementing a recursive graph traversal algorithm (DFS) for finding optimal delivery routes across 50 fulfilment centres. Uses backtracking to handle constraints (weight limits, time windows).
12:30 PM โ Code review with senior engineer. Discussion about converting a recursive solution to iterative for production (stack overflow risk with 1M+ nodes).
1:30 PM โ Lunch at the campus cafeteria. Discuss LeetCode weekly contest problems with teammates.
2:30 PM โ Write unit tests for the routing algorithm. Edge cases: circular routes, unreachable nodes, maximum depth limits.
4:00 PM โ 1-on-1 with manager. Career growth discussion โ targeting SDE-2 promotion by demonstrating system design skills.
5:30 PM โ Personal development: solving 2 LeetCode mediums to stay sharp. Today's theme: backtracking problems.
| Detail | Info |
|---|---|
| Tools Used Daily | C++, Java, Python, Git, AWS (Lambda, DynamoDB), LeetCode |
| Entry Salary (SDE-1) | โน12โ18 LPA + RSUs + signing bonus |
| Mid-Level (SDE-2, 3โ5 yrs) | โน25โ45 LPA |
| Senior (SDE-3, 7+ yrs) | โน50โ80 LPA |
| Key Interview Topics | Recursion, Backtracking, DP, Graphs, System Design |
| Companies Hiring | Amazon, Google, Microsoft, Flipkart, Swiggy, PhonePe, Atlassian, Uber, Goldman Sachs, DE Shaw |
Earn With It โ Competitive Coding Tutoring
๐ฐ Your Earning Path After This Chapter
Portfolio Piece: GitHub repository with solved recursion problems (15+ problems), N-Queens solver, and Sudoku solver โ all with clean code, comments, and README documentation.
Earning Opportunities:
โข 1-on-1 competitive coding tutoring for college students โ โน300โโน800/hour
โข Group coaching for placement preparation (recursion + backtracking module) โ โน500โโน1500/session
โข Creating LeetCode solution explanations on YouTube/blog โ ad revenue + sponsorships
โข Freelance algorithm implementation for startups โ โน5,000โโน20,000/project
| Platform | Best For | Typical Rate |
|---|---|---|
| SuperProf | 1-on-1 tutoring for coding/DSA | โน400โโน1,200/hour |
| Vedantu | Online group classes for Indian students | โน300โโน800/hour |
| Fiverr | Algorithm implementation gigs | $20โ$100/gig (โน1,600โโน8,000) |
| Upwork | Competitive programming mentoring | $15โ$50/hour |
| Internshala | Coding bootcamp teaching assistant | โน5,000โโน15,000/month |
| YouTube/Blog | Algorithm tutorials & explanations | โน2,000โโน20,000/month (ad revenue) |
โฑ๏ธ Time to First Earning: 3โ4 weeks (complete labs, build GitHub portfolio, register on SuperProf/Fiverr)
Chapter Summary
๐ Key Takeaways โ Recursion & Advanced Techniques
- Recursion = a function calling itself. Every recursive function needs a base case (stop) and recursive case (progress).
- The call stack stores a frame for each recursive call. Depth n uses O(n) space.
- Base case is critical โ without it, infinite recursion causes Stack Overflow.
- Fibonacci illustrates the danger of naive recursion: O(2โฟ) time due to overlapping subproblems.
- Divide and Conquer splits problems into independent halves โ merge sort, binary search, fast exponentiation.
- Direct recursion (AโA) vs Indirect recursion (AโBโA).
- Tail recursion = recursive call is the last operation. Can be optimised to O(1) space by compilers (TCO). Non-tail recursion requires O(n) stack.
- Memory: Each stack frame stores local variables, parameters, and return address. ~32โ64 bytes per frame. Default stack ~1 MB.
- Recursion vs Iteration: Recursion is elegant for trees/graphs; iteration is faster for simple linear problems.
- Backtracking: Build solutions incrementally, abandon invalid paths early. Key problems: N-Queens, Sudoku, subset sum, permutations.
- Memoization: Cache results of subproblems. Transforms O(2โฟ) Fibonacci to O(n). Bridges to Dynamic Programming (Unit 4).
- Classic problems: Happy numbers, sum strings, champagne tower โ all leverage recursive thinking.
Earning Checkpoint โ Are You Ready to Monetise?
| Topic/Skill | Tool/Language | Portfolio Output | Earning Ready? |
|---|---|---|---|
| Recursion Basics (Factorial, Fibonacci) | C++, Python | Traced programs with call stack diagrams | โ Yes โ can tutor juniors |
| Backtracking (N-Queens, Permutations) | C++ | N-Queens solver on GitHub | โ Yes โ impressive for interviews & portfolio |
| Memoization | C++, Python | Memoized Fibonacci + complexity analysis | โ Yes โ key interview topic |
| Sudoku Solver | C++ | Complete Sudoku solver on GitHub | โ Yes โ portfolio standout project |
| Competitive Coding Practice | LeetCode, Codeforces | 15+ solved problems with explanations | โ Yes โ can start tutoring at โน500/hr |
| Problem-Solving Patterns | Conceptual | โ | โ Yes โ can create tutorial content |
โ Unit 3 complete. Ready for Unit 4: Dynamic Programming!
[QR: Link to EduArtha video tutorial โ Recursion & Advanced Techniques]