Backtracking Algorithm Fundamentals and Practical Implementations

Backtracking Theory Overview

Backtracking is an algorithmic technique used to solve problems recursively by trying to build a solution incrementally. It explores potential candidates step-by-step and abandons a candidate ("backtracks") as soon as it determines that the candidate cannot possibly lead to a valid solution.

Common Problem Types:

  • Combinations: Selecting k elements from a set of n numbers.
  • Partitioning: Dividing a string according to specific rules.
  • Subsets: Finding all valid subsets from a collection.
  • Permutations: Arranging elements where order matters.
  • Board Problems: Solving puzzles like N-Queens or Sudoku.

Conceptual Model:

All backtracking algorithms can be visualized as traversing an N-ary tree. The depth of the tree represents the number of decisions made, while the width represents the choices available at each decision point.

Generic Template Structure:

def backtrack(args):
    # Termination Condition
    if condition_met:
        return

    # Iterative Choice Selection
    for choice in choices:
        # Make the choice
        process_node()
        
        # Recurse
        backtrack(new_args)
        
        # Undo the choice (Backtrack)
        undo_process()

LeetCode 77: Combinations

This problem requires generating all possible combinations of k numbers chosen from the range [1, n].

The core logic involves controlling the recursion depth with a loop. Each level of recursion corresponds to one iteration of a for-loop.

Basic Implementation

In the initial version, we iterate through the entire range at every step, wich results in redundant computation.

class Solution:
    def generate_combinations(self, n: int, k: int):
        result = []
        current_path = []
        
        def dfs(start: int, k_left: int):
            # Base Case: Target length reached
            if k_left == 0:
                result.append(current_path.copy())
                return

            # Recursive Step: Iterate through options
            for i in range(start, n + 1):
                current_path.append(i)
                dfs(i + 1, k_left - 1)
                current_path.pop()  # Backtrack

        dfs(1, k)
        return result

Optimization via Pruning

To improve efficiency, we can calculate the valid range of the loop limit. If we need k elements total and currently have len(path) elements, we only need to select k - len(path) more. We must leave enough remaining numbers to fulfill this requirement. Thus, the starting point i does not need to exceed a certain threshold.

class Solution:
    def combine_optimized(self, n: int, k: int) -> List[List[int]]:
        result = []
        
        def dfs(start: int, path: List[int]):
            if len(path) == k:
                result.append(path[:])
                return
            
            # Pruning calculation: 
            # Remaining needed = k - len(path)
            # Max start index = n - (Remaining needed) + 1
            # Range upper bound (exclusive) = n - (Remaining needed) + 2
            max_start = n - (k - len(path)) + 2
            
            for i in range(start, max_start):
                path.append(i)
                dfs(i + 1, path)
                path.pop()

        dfs(1, [])
        return result

LeetCode 216: Combination Sum III

This variation restricts the combination to using only numbers 1 through 9. The goal is to find all unique combinations of k numbers that sum up to a target value n.

The search space is strictly limited to digits 1-9. Pruning is applied based on the current sum exceeding the target before completing the recursion depth.

class Solution:
    def combinationSum3(self, k: int, n: int) -> List[List[int]]:
        res = []
        
        def dfs(idx: int, target: int, count: int, path: List[int], current_sum: int):
            # Optimization: Current sum exceeds target
            if current_sum > target:
                return
            
            # Termination: Count reached
            if count == k:
                if current_sum == target:
                    res.append(path[:])
                return
            
            # Search Space: 1 to 9
            # Pruning based on remaining slots
            max_val = 9 - (k - count) + 1
            
            for i in range(idx, max_val + 1):
                path.append(i)
                dfs(i + 1, target, count + 1, path, current_sum + i)
                path.pop()

        dfs(1, n, 0, [], 0)
        return res

LeetCode 17: Letter Combinations of a Phone Number

This problem maps digits to their corresponding characters (e.g., '2' -> "abc"). The task is to find all letter combinations representing a given phone number digit string.

We treat the digit string indices as the depth of recursion. For each index, we iterate through the character list associated with that digit.

class Solution:
    def __init__(self):
        # Mapping from digit to string characters
        self.digit_map = {
            "2": "abc", "3": "def", "4": "ghi", 
            "5": "jkl", "6": "mno", "7": "pqrs", 
            "8": "tuv", "9": "wxyz"
        }
        self.outcome = []

    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:
            return []
            
        self._backtrack(digits, 0, "")
        return self.outcome

    def _backtrack(self, digits: str, index: int, current_string: str):
        # Base Case: Traversed all digits
        if index == len(digits):
            self.outcome.append(current_string)
            return

        # Retrieve characters for current digit
        mapped_chars = self.digit_map[digits[index]]

        # Iterate over characters
        for char in mapped_chars:
            # Extend current path
            self._backtrack(digits, index + 1, current_string + char)

Tags: backtracking Recursion python algorithms LeetCode

Posted on Thu, 15 Oct 2026 16:35:21 +0000 by kgahn