0% found this document useful (0 votes)
4 views15 pages

5. Recursion and Backtracking, Dynamic Programming (DP)

Uploaded by

kingkundu777
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views15 pages

5. Recursion and Backtracking, Dynamic Programming (DP)

Uploaded by

kingkundu777
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 15

7.

Recursion and Backtracking

Recursion is a powerful concept in programming where a function calls itself to solve smaller
subproblems of a larger problem. Backtracking is a technique used to explore all possible
solutions by making choices and undoing them when needed.

🚀 1. What is Recursion?
📌 Definition: Recursion is a method where a function calls itself until a base condition is met.

🔹 Basic Structure of Recursion

def recursive_function():
if base_condition:
return result # Stop recursion (base case)
else:
return recursive_function() # Recursive call

🔥 2. Tail Recursion vs Normal Recursion


🔹 Normal Recursion: Recursive calls stay in memory until all calls are completed.
🔹 Tail Recursion: The recursive call is the last operation in the function, so it can be
optimized by compilers to avoid excessive memory usage (Tail Call Optimization - TCO).

✅ Example: Normal Recursion (Factorial)

def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1) # Recursive call is NOT the last
operation

print(factorial(5)) # Output: 120


✅ Example: Tail Recursion (Optimized)

def tail_factorial(n, accumulator=1):


if n == 0:
return accumulator
return tail_factorial(n - 1, accumulator * n) # Recursive call is
the last operation

print(tail_factorial(5)) # Output: 120

🎯 3. Problems Solved Using Recursion


🔥 A. Factorial of a Number
Factorial of n is n! = n × (n-1)!.

def factorial(n):
if n == 0 or n == 1:
return 1
return n * factorial(n - 1)

print(factorial(5)) # Output: 120

🔥 B. Fibonacci Sequence
Each number is the sum of the two previous numbers:
F(n) = F(n-1) + F(n-2)

def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(6)) # Output: 8

⚡ Optimization: Use memoization (DP) for O(n) complexity.

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(6)) # Output: 8

🔥 C. Tower of Hanoi
A classic problem where we move n disks from source → destination using an auxiliary
peg.

Steps:
11 Move n-1 disks from Source → Auxiliary
1️⃣
2️⃣Move 1 disk from Source → Destination
3️⃣Move n-1 disks from Auxiliary → Destination

def tower_of_hanoi(n, source, auxiliary, destination):


if n == 1:
print(f"Move disk 1 from {source} to {destination}")
return
tower_of_hanoi(n - 1, source, destination, auxiliary)
print(f"Move disk {n} from {source} to {destination}")
tower_of_hanoi(n - 1, auxiliary, source, destination)

tower_of_hanoi(3, 'A', 'B', 'C')

📌 Time Complexity: O(2^n) (Exponential)


🔥 D. N-Queens Problem (Backtracking)
The goal is to place N queens on an N×N chessboard such that no two queens attack each
other.

def is_safe(board, row, col, n):


# Check column
for i in range(row):
if board[i] == col:
return False
if abs(board[i] - col) == abs(i - row): # Check diagonals
return False
return True

def solve_n_queens(n, row=0, board=[]):


if row == n:
print(board) # Solution found
return

for col in range(n):


if is_safe(board, row, col, n):
board.append(col)
solve_n_queens(n, row + 1, board)
board.pop() # Backtrack

solve_n_queens(4) # Example for N=4

📌 Time Complexity: O(N!)

🔥 E. Sudoku Solver (Backtracking)


Fill a 9×9 Sudoku board by placing numbers 1-9 without breaking Sudoku rules.

def is_valid(board, row, col, num):


for i in range(9):
if board[row][i] == num or board[i][col] == num:
return False

box_x, box_y = row - row % 3, col - col % 3


for i in range(3):
for j in range(3):
if board[box_x + i][box_y + j] == num:
return False
return True

def solve_sudoku(board, row=0, col=0):


if row == 9:
return True

if col == 9:
return solve_sudoku(board, row + 1, 0)

if board[row][col] != 0:
return solve_sudoku(board, row, col + 1)

for num in range(1, 10):


if is_valid(board, row, col, num):
board[row][col] = num
if solve_sudoku(board, row, col + 1):
return True
board[row][col] = 0 # Backtrack

return False

📌 Time Complexity: O(9^N) (N = empty cells)

🔥 F. Rat in a Maze (Backtracking)


A rat moves in a N×N maze from (0,0) → (N-1,N-1), only moving right (R) or down (D).
def solve_maze(maze, x, y, path, n):
if x == n - 1 and y == n - 1: # Reached destination
print(path)
return

# Move Right
if y + 1 < n and maze[x][y + 1] == 1:
solve_maze(maze, x, y + 1, path + "R", n)

# Move Down
if x + 1 < n and maze[x + 1][y] == 1:
solve_maze(maze, x + 1, y, path + "D", n)

# Example Maze
maze = [[1, 0, 0, 0],
[1, 1, 0, 1],
[0, 1, 0, 0],
[1, 1, 1, 1]]

solve_maze(maze, 0, 0, "", 4)

📌 Time Complexity: O(2^(N^2))

🎯 Key Takeaways for Placements


✅ Recursion helps in problems with repetitive subproblems (Fibonacci, Factorial,
Hanoi).
✅ Backtracking is used for decision-making problems (N-Queens, Sudoku, Rat in a
Maze).
✅ Tail Recursion is optimized by compilers, but Python does NOT support TCO.

🔥 Best Practice: Use memoization or iterative approaches when recursion depth is high! 🚀
8. Dynamic Programming (DP)
Dynamic Programming (DP) is a powerful algorithmic technique used to solve problems by
breaking them into smaller overlapping subproblems and solving each only once to improve
efficiency.

🎯 1. Introduction to Dynamic
Programming (DP)
📌 Definition: DP is an optimization technique where we store already computed results to
avoid redundant calculations, making algorithms faster and more efficient.

📌 When to use DP?


✔ Optimal Substructure – The problem can be broken into smaller subproblems that have
optimal solutions.
✔ Overlapping Subproblems – The same subproblem is solved multiple times (e.g.,
Fibonacci).

📌 Approaches to Solve DP Problems


✅ Top-Down (Memoization) – Solve problems recursively and store results.
✅ Bottom-Up (Tabulation) – Solve problems iteratively and build up solutions.

🔥 2. Difference Between Memoization and


Tabulation
Feature Memoization (Top-Down) Tabulation (Bottom-Up)

Approach Recursion + Caching Iterative

Storage Uses a hash table (dict) or array Uses a table (array)

Function Calls Many recursive calls No recursion

Efficiency Can be slow due to recursion Usually faster (no recursion


overhead overhead)
🎯 3. Common DP Problems & Solutions
🔥 A. Fibonacci Using DP
Normal Recursion (Exponential Time Complexity O(2^n))

def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)

print(fib(6)) # Output: 8

❌ Inefficient for large values of n

✅ Optimized Using Memoization (Top-Down Approach)

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)

print(fib(100)) # Output: 354224848179261915075

⏳ Time Complexity: O(n), Space Complexity: O(n)

✅ Optimized Using Tabulation (Bottom-Up Approach)

def fib(n):
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
print(fib(100)) # Output: 354224848179261915075

⏳ Time Complexity: O(n), Space Complexity: O(n)

🚀 Further Optimization (Constant Space O(1))

def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a

print(fib(100)) # Output: 354224848179261915075

🔥 B.

0/1 Knapsack Problem


Given N items, each with a weight and value, and a bag with maximum weight W, determine
the maximum value we can carry.

📌 Recursion (Exponential O(2^n))

def knapsack(wt, val, W, n):


if n == 0 or W == 0:
return 0
if wt[n - 1] <= W:
return max(val[n - 1] + knapsack(wt, val, W - wt[n - 1], n -
1),
knapsack(wt, val, W, n - 1))
return knapsack(wt, val, W, n - 1)

weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 5
print(knapsack(weights, values, W, len(weights))) # Output: 7
❌ Inefficient for large n

✅ Optimized Using DP (Tabulation - O(nW))

def knapsack(wt, val, W, n):


dp = [[0 for _ in range(W + 1)] for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(1, W + 1):
if wt[i - 1] <= w:
dp[i][w] = max(val[i - 1] + dp[i - 1][w - wt[i - 1]],
dp[i - 1][w])
else:
dp[i][w] = dp[i - 1][w]
return dp[n][W]

print(knapsack(weights, values, W, len(weights))) # Output: 7

⏳ Time Complexity: O(nW), Space Complexity: O(nW)

🔥 C. Longest Common Subsequence (LCS)


Find the longest subsequence common to two strings (not necessarily contiguous).

📌 Recursion (O(2^n))

def lcs(x, y, m, n):


if m == 0 or n == 0:
return 0
if x[m - 1] == y[n - 1]:
return 1 + lcs(x, y, m - 1, n - 1)
return max(lcs(x, y, m - 1, n), lcs(x, y, m, n - 1))

print(lcs("abcde", "ace", 5, 3)) # Output: 3

✅ Optimized Using DP (O(m×n))


def lcs(x, y):
m, n = len(x), len(y)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if x[i - 1] == y[j - 1]:
dp[i][j] = 1 + dp[i - 1][j - 1]
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]

print(lcs("abcde", "ace")) # Output: 3

🔥 D. Matrix Chain Multiplication


Given a sequence of matrices, find the most efficient way to multiply them.

✅ Optimized DP Solution (O(n³))

import sys

def matrix_chain_order(p, i, j):


if i == j:
return 0
dp = [[0 for _ in range(j + 1)] for _ in range(j + 1)]
for l in range(2, j + 1):
for i in range(1, j - l + 2):
k = i + l - 1
dp[i][k] = sys.maxsize
for m in range(i, k):
cost = dp[i][m] + dp[m + 1][k] + p[i - 1] * p[m] *
p[k]
dp[i][k] = min(dp[i][k], cost)
return dp[1][j]

arr = [1, 2, 3, 4]
print(matrix_chain_order(arr, 1, len(arr) - 1)) # Output: 18
E. Subset Sum Problem

Problem Statement
Given a set of N positive integers and a target sum S, determine whether there exists a subset
whose sum is exactly S.

📌 Example:

Input: arr = [3, 34, 4, 12, 5, 2], S = 9


Output: True
Explanation: The subset {4, 5} adds up to 9.

🛠 1. Brute Force Solution (Recursion) -


Exponential O(2^N)
✔ Approach:

● For each element, we have two choices:


○ Include it in the subset and reduce the target (S - arr[i]).
○ Exclude it and move to the next element.
● Base Case:
○ If S == 0, return True (subset found).
○ If N == 0 and S != 0, return False (no subset possible).

✅ Python Code:

def subset_sum(arr, n, S):


if S == 0:
return True
if n == 0:
return False
if arr[n - 1] > S:
return subset_sum(arr, n - 1, S)
return subset_sum(arr, n - 1, S) or subset_sum(arr, n - 1, S -
arr[n - 1])

arr = [3, 34, 4, 12, 5, 2]


S = 9
print(subset_sum(arr, len(arr), S)) # Output: True

⏳ Time Complexity: O(2^N) (Exponential, very slow for large inputs)


❌ Inefficient for large N, leads to repeated computations.

🔥 2. Optimized Solution Using Dynamic


Programming (DP) - O(N * S)
✔ Approach:

● Use a DP table dp[i][j] where:


○ i represents the first i elements.
○ j represents the sum we want to achieve.
● Base Cases:
○ dp[0][j] = False (No elements → No sum except 0).
○ dp[i][0] = True (Sum 0 can be achieved with an empty subset).
● Recurrence Relation:
○ If the element is greater than j, we exclude it:
dp[i][j] = dp[i-1][j]
○ Otherwise, we include or exclude the element:
dp[i][j] = dp[i-1][j] or dp[i-1][j-arr[i-1]]

✅ Python Code (Tabulation Approach)

def subset_sum(arr, S):


n = len(arr)
dp = [[False] * (S + 1) for _ in range(n + 1)]

for i in range(n + 1):


dp[i][0] = True # Sum 0 is always possible (empty subset)

for i in range(1, n + 1):


for j in range(1, S + 1):
if arr[i - 1] > j:
dp[i][j] = dp[i - 1][j] # Exclude element
else:
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - arr[i - 1]]
# Include or exclude

return dp[n][S]

arr = [3, 34, 4, 12, 5, 2]


S = 9
print(subset_sum(arr, S)) # Output: True

⏳ Time Complexity: O(N * S)


📌 Space Complexity: O(N * S)

🚀 3. Space Optimized DP - O(S) Space


Complexity
Instead of using a 2D array (dp[n][S]), we use a 1D array (dp[S]).

✅ Python Code (Space Optimized DP)

def subset_sum(arr, S):


n = len(arr)
dp = [False] * (S + 1)
dp[0] = True # Base case: sum 0 is always possible

for num in arr:


for j in range(S, num - 1, -1):
dp[j] = dp[j] or dp[j - num]

return dp[S]

arr = [3, 34, 4, 12, 5, 2]


S = 9
print(subset_sum(arr, S)) # Output: True

⏳ Time Complexity: O(N * S)


📌 Space Complexity: O(S)

🎯 Key Takeaways for Placements


✅ Brute Force (Recursion) - O(2^N) → Exponential (Bad for large N).
✅ DP Table (O(N*S)) - Tabulation Approach → Faster & avoids redundant
work.
✅ Space Optimization (O(S)) → Efficient memory usage.
✅ Used in Knapsack-type problems → Important for DP-based interview
questions!

🎯 Key Takeaways for Placements


✅ Recognize DP problems (Optimal Substructure + Overlapping Subproblems).
✅ Use Memoization for recursive problems and Tabulation for iterative solutions.
✅ Optimize space complexity where possible (e.g., Fibonacci O(1) space).

🔥 Mastering DP is a game-changer for coding interviews! 🚀

You might also like