Backtracking is a powerful technique used in solving combinatorial problems like generating permutations, combinations, subsets, and solving constraint satisfaction problems such as the N-Queens problem, Sudoku, and more. In this post, we’ll break down the “choose-unchoose” paradigm of backtracking, provide a template code, and explain it with an example.
What is Backtracking?
Backtracking is a depth-first search approach for solving problems incrementally, where we build a solution step by step and backtrack when we realize that a certain path doesn’t lead to a valid solution. The idea is to explore all possible configurations and prune the paths that do not satisfy the constraints.
The Choose-Unchoose Paradigm
The “choose-unchoose” paradigm is a common pattern used in backtracking algorithms. It consists of three steps:
- Choose: Make a choice from the available options.
 - Recurse: Proceed with the choice and recursively solve the remaining problem.
 - Unchoose: Undo the choice (i.e., backtrack) and try another option.
 
This pattern ensures that all possible solutions are explored, and it backtracks when a dead end is reached.
Template Code for Backtracking
Below is a template for a typical backtracking solution using the “choose-unchoose” paradigm:
function backtrack(solution, choices) {
    // Base case: If the solution is complete or meets the problem requirements
    if (isSolution(solution)) {
        // Process the complete solution
        processSolution(solution);
        return;
    }
    // Iterate through all possible choices
    for (let i = 0; i < choices.length; i++) {
        // Choose: Add the current choice to the solution
        solution.push(choices[i]);
        // Recurse: Continue building the solution
        backtrack(solution, choices);
        // Unchoose: Remove the current choice to explore the next option
        solution.pop();
    }
}
// Helper function to check if a solution is valid (problem-specific)
function isSolution(solution) {
    // Define criteria for a valid solution
    return true; // Placeholder
}
// Helper function to process a complete solution (problem-specific)
function processSolution(solution) {
    console.log(solution);
}
Example: Generating All Subsets of a Set
Let’s apply this template to a concrete problem: generating all possible subsets of a given set of numbers. This problem is a classic example of backtracking because we explore all possible subsets (combinations of elements) by either including or excluding each element.
Problem Statement
Given a set of distinct integers, generate all possible subsets (the power set).
Example: Input: [1, 2, 3]
Output:
[[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
Backtracking Approach with Choose-Unchoose
function subsets(nums) {
    const result = []; // To store all subsets
    function backtrack(start, currentSubset) {
        // Add the current subset to the result (it's a valid subset)
        result.push([...currentSubset]);
        // Explore further choices
        for (let i = start; i < nums.length; i++) {
            // Choose: Include nums[i] in the current subset
            currentSubset.push(nums[i]);
            // Recurse: Move to the next element
            backtrack(i + 1, currentSubset);
            // Unchoose: Remove nums[i] from the current subset
            currentSubset.pop();
        }
    }
    backtrack(0, []); // Start with an empty subset
    return result;
}
// Example usage:
const nums = [1, 2, 3];
console.log(subsets(nums));
// Output: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
Explanation
- 
Initialization:
- We initialize an empty array 
resultto store all the subsets. 
 - We initialize an empty array 
 - 
Recursive Backtracking Function (
backtrack):- The 
backtrackfunction takes two parameters:start: The index to start from for the current recursive call.currentSubset: The current subset being formed.
 
 - The 
 - 
Base Case:
- There isn’t a strict base case in this example because every subset (including the empty set) is a valid result. We add the 
currentSubsettoresultevery time the function is called. 
 - There isn’t a strict base case in this example because every subset (including the empty set) is a valid result. We add the 
 - 
Choose-Unchoose Steps:
- Choose: Include the element 
nums[i]in thecurrentSubset. - Recurse: Call 
backtrack(i + 1, currentSubset)to proceed with the next elements. - Unchoose: Remove the element 
nums[i]fromcurrentSubsetto explore other possibilities without it. 
 - Choose: Include the element 
 
Key Takeaways
- Backtracking explores all possible configurations by making choices, recursing with those choices, and undoing them to try new paths.
 - The “choose-unchoose” pattern ensures that we explore all potential solutions and backtrack when a path doesn’t lead to a valid solution.
 - This pattern is versatile and can be adapted to various combinatorial problems, from generating subsets and permutations to solving more complex problems like the N-Queens problem.
 
Conclusion
Backtracking with the “choose-unchoose” pattern provides a structured approach to solving combinatorial problems. The template and example discussed in this post should give you a solid foundation to tackle similar problems. Try applying this approach to problems like generating permutations, solving Sudoku, or finding paths in a maze. Happy coding!