Python • Recursion & Backtracking

Recursion & Backtracking Deep Dive

Solve big problems by breaking them into smaller, identical sub‑problems. Master recursion, backtracking, and build a permutations/combinations generator that ties together variables, loops, conditionals, and collections.

🔄

Recursion

Function calls itself with smaller input

🧮

Factorial / Fibonacci

Classic recursive mathematical problems

🧭

Backtracking

Explore all possibilities & abandon dead ends

Optimisation

lru_cache, recursion depth, yield

Recursion – A Function Calling Itself

Every recursive function needs two parts:

  • Base case – the simplest version of the problem (stops recursion).
  • Recursive case – reduces the problem and calls itself again.

The call stack remembers each call until the base case returns.

recursive_countdown.py
def countdown(n):
    if n == 0:           # base case
        print("Go!")
        return
    print(n)
    countdown(n - 1)     # recursive case

countdown(3)
3 2 1 Go!

Factorial – A Classic First Step

Factorial of n = n × (n‑1) × … × 1. Base case: 0! = 1.

factorial.py
def factorial(n):
    if n == 0:          # base case
        return 1
    return n * factorial(n - 1)   # recursive case

print(factorial(5))     # 120
120
📌 Note: factorial(1000) will exceed the default recursion depth. We'll later use iteration or lru_cache for deep recursion.

Fibonacci Sequence

Fibonacci series: 0, 1, 1, 2, 3, 5, 8, … where F(n) = F(n‑1) + F(n‑2). Naive recursion is extremely inefficient, but we can fix it with memoisation.

fibonacci_naive.py
def fib(n):
    if n == 0: return 0
    if n == 1: return 1
    return fib(n - 1) + fib(n - 2)

print(fib(10))   # 55

Backtracking – Explore & Undo

Backtracking builds candidates step by step and abandons (“backtracks”) any candidate as soon as it determines the candidate cannot lead to a valid solution. It follows the pattern choose → explore → unchoose (recursive). Ideal for generating all subsets, permutations, or valid parentheses combinations.

Generate Parentheses (LeetCode 22)

Given n pairs of parentheses, write a function to generate all combinations of well‑formed parentheses.

generate_parentheses.py
def generateParenthesis(n):
    result = []
    def backtrack(current, open_count, close_count):
        if len(current) == 2 * n:   # base: used all pairs
            result.append(current)
            return
        if open_count < n:
            backtrack(current + "(", open_count + 1, close_count)
        if close_count < open_count:
            backtrack(current + ")", open_count, close_count + 1)

    backtrack("", 0, 0)
    return result

print(generateParenthesis(3))
['((()))', '(()())', '(())()', '()(())', '()()()']

Subsets (Power Set – LeetCode 78)

Given an array of unique elements, return all possible subsets (the power set). Backtracking builds subsets by deciding for each element whether to include it or not.

subsets_backtracking.py
def subsets(nums):
    result = []
    def backtrack(start, path):
        result.append(path[:])         # snapshot current subset
        for i in range(start, len(nums)):
            path.append(nums[i])       # choose
            backtrack(i + 1, path)     # explore
            path.pop()                 # unchoose (backtrack)

    backtrack(0, [])
    return result

print(subsets([1, 2, 3]))
[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

Recursion Depth & lru_cache

Recursion depth limit prevents stack overflow (default ~1000). You can view/set it with sys.getrecursionlimit() and sys.setrecursionlimit().

@functools.lru_cache memoises results so repeated recursive calls return instantly. It turns the slow fib(n) into a lightning‑fast version.

fib_cached.py
from functools import lru_cache

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

print(fib(100))           # 354224848179261915075 (instant)
print(fib.cache_info())   # hits, misses, etc.
354224848179261915075 CacheInfo(hits=98, misses=101, maxsize=None, currsize=101)

Recursive Generators with yield

You can write recursive functions that yield values lazily, avoiding storing the entire result in memory. Use yield from to delegate to a sub‑generator.

recursive_generator.py
# Recursively flatten a nested list
def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)   # delegate recursion
        else:
            yield item

nested = [1, [2, [3, 4], 5], 6]
print(list(flatten(nested)))   # [1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]

🎯 Mini Project: Permutations & Combinations Generator

This project uses recursion and backtracking to generate all permutations and combinations of a given set of items. It also uses variables, input validation, type casting, loops, conditionals, lists, sets, and dictionaries – everything you’ve learned so far. You’ll build an interactive tool that generates and displays all arrangements of a user‑provided list of elements.

permutation_combination_generator.py
"""
Permutation & Combination Generator – Complete Mini Project
Concepts used: recursion, backtracking, variables, type casting,
loops, conditionals, lists, sets, dictionaries.
"""

def main():
    # User input
    items_input = input("Enter items separated by commas (e.g., a,b,c): ").strip()
    items = [x.strip() for x in items_input.split(",") if x.strip()]
    if not items:
        print("No valid items provided. Exiting.")
        return
    
    print("\n🎲 SELECT OPTION 🎲")
    print("1. Generate all permutations")
    print("2. Generate all combinations (of all lengths)")
    choice = input("Choose (1/2): ").strip()
    
    if choice == "1":
        # Permutations (backtracking)
        result = []
        used = [False] * len(items)
        def backtrack_perm(path):
            if len(path) == len(items):
                result.append(path[:])
                return
            for i in range(len(items)):
                if not used[i]:
                    used[i] = True
                    path.append(items[i])
                    backtrack_perm(path)
                    path.pop()
                    used[i] = False
        backtrack_perm([])
        print(f"\n📌 Total permutations: {len(result)}")
        for p in result:
            print(p)
    
    elif choice == "2":
        # Combinations (backtracking, include all lengths)
        result = {}
        items_set = set(items)          # ensures uniqueness (demonstrates set)
        items = list(items_set)         # convert back to list
        for r in range(1, len(items) + 1):
            current_combs = []
            def backtrack_comb(start, path, r):
                if len(path) == r:
                    current_combs.append(path[:])
                    return
                for i in range(start, len(items)):
                    path.append(items[i])
                    backtrack_comb(i + 1, path, r)
                    path.pop()
            backtrack_comb(0, [], r)
            result[r] = current_combs
        print("\n📌 All combinations:")
        total = 0
        for r, combs in result.items():
            print(f"\nLength {r} ({len(combs)} combinations):")
            for c in combs:
                print(c)
            total += len(combs)
        print(f"\n📊 Total combinations: {total}")
    else:
        print("Invalid choice.")

if __name__ == "__main__":
    main()
💡 Project Highlights:
Recursion & Backtracking generate permutations and combinations.
Lists store the user‑provided items and intermediate results.
Sets ensure unique elements before generating combinations.
Dictionaries (optional) group combinations by length.
Input validation and type casting handle user input.
Loops and conditionals drive the menu and generation process.
f‑strings format the output.
– Extend the project by adding a feature to count only unique permutations or by handling larger inputs with iterative methods.

Solved Practice Problems

🔰 Beginner Level

1. Sum of digits using recursion
def digit_sum(n):
    if n == 0: return 0
    return n % 10 + digit_sum(n // 10)

print(digit_sum(123))   # 6
2. Recursive power function (a^b)
def power(a, b):
    if b == 0: return 1
    return a * power(a, b - 1)

print(power(2, 5))   # 32
3. Print numbers from 1 to N using recursion
def print_numbers(n):
    if n == 0: return
    print_numbers(n - 1)
    print(n, end=' ')

print_numbers(5)   # 1 2 3 4 5

⚡ Intermediate Level

4. Generate all binary strings of length n (backtracking)
def binary_strings(n):
    result = []
    def backtrack(current):
        if len(current) == n:
            result.append(current)
            return
        backtrack(current + '0')
        backtrack(current + '1')
    backtrack('')
    return result

print(binary_strings(2))   # ['00', '01', '10', '11']
5. Palindrome check using recursion
def is_palindrome(s):
    s = ''.join(filter(str.isalnum, s)).lower()
    def recurse(left, right):
        if left >= right: return True
        if s[left] != s[right]: return False
        return recurse(left + 1, right - 1)
    return recurse(0, len(s)-1)

print(is_palindrome("A man, a plan, a canal: Panama"))  # True

Unsolved Exercises

Challenge yourself – no solutions provided, but you can use the solved examples and the mini project as a guide.

🔰 Beginner Level

1. Recursive reverse of a string
Write your code here – solution not shown.
2. Recursive GCD (Euclidean algorithm)
Write your code here – solution not shown.

⚡ Intermediate Level

3. Letter Case Permutation (backtracking)
Write your code here – solution not shown.
4. Permutations of a string (all distinct permutations)
Write your code here – solution not shown.

🔥 Advanced (DSA / Interview)

5. N‑Queens problem (backtracking)
Write your code here – solution not shown.
6. Maze solver using recursion (finding a path)
Write your code here – solution not shown.