Recursion and Backtracking in Java

Mastering recursion and backtracking techniques in Java, with examples like the N-Queens problem and Sudoku solver.


Subsets and Power Sets in Java (Recursive Backtracking)

Understanding Subsets and Power Sets

Subsets

A subset of a set is a set containing some or all of the elements of the original set, including the empty set. For example, if we have a set {1, 2, 3}, then {1, 2}, {3}, {} (the empty set), and {1, 2, 3} are all subsets of the original set.

Power Set

The power set of a set is the set of all possible subsets, including the empty set and the set itself. For example, the power set of {1, 2, 3} is {{}, {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}}. If a set has n elements, its power set contains 2n elements.

Generating Subsets and Power Sets with Recursive Backtracking

Recursive backtracking is a powerful technique for generating subsets and power sets. The basic idea is to explore all possible combinations by making choices at each step and then backtracking to undo those choices and explore other possibilities.

Recursive Backtracking Algorithm

  1. Base Case: When we have processed all elements of the input set, we have a subset (or a complete candidate solution for the power set).
  2. Recursive Step: For each element in the input set, we have two choices:
    • Include the element in the current subset.
    • Exclude the element from the current subset.
  3. Backtracking: After exploring both choices for an element, we undo the choice by removing the element from the subset if it was included, allowing us to explore alternative paths.

Java Implementation

Here's a Java implementation of the recursive backtracking algorithm to generate the power set of a given set (represented as an array of integers):

import java.util.ArrayList;
import java.util.List;

public class PowerSetGenerator {

    public static List<List<Integer>> generatePowerSet(int[] nums) {
    List<List<Integer>> powerSet = new ArrayList<>();
    generatePowerSetRecursive(nums, 0, new ArrayList<>, powerSet);
    return powerSet;
}

private static void generatePowerSetRecursive(int[] nums, int index, List<Integer> currentSubset, List<List<Integer>> powerSet) {
    // Base case: all elements have been processed
    if (index == nums.length) {
        powerSet.add(new ArrayList<>(currentSubset)); // Add a copy to the powerSet
        return;
    }

    // Include the current element
    currentSubset.add(nums[index]);
    generatePowerSetRecursive(nums, index + 1, currentSubset, powerSet);

    // Exclude the current element (backtrack)
    currentSubset.remove(currentSubset.size() - 1);
    generatePowerSetRecursive(nums, index + 1, currentSubset, powerSet);
}

public static void main(String[] args) {
    int[] nums = {1, 2, 3};
    List<List<Integer>> powerSet = generatePowerSet(nums);

    System.out.println("Power Set: " + powerSet);
}
}

Explanation of the Code:

  • generatePowerSet(int[] nums): This is the main function that initializes the power set and calls the recursive helper function.
  • generatePowerSetRecursive(int[] nums, int index, List<Integer> currentSubset, List<List<Integer>> powerSet):
    • nums: The input array of integers.
    • index: The current index of the element being considered.
    • currentSubset: A list representing the current subset being built.
    • powerSet: A list of lists that stores all generated subsets (the power set).
  • Base Case: When index reaches nums.length, it means we have considered all elements, so we add a copy of the currentSubset to the powerSet. We need to create a new ArrayList to copy the contents of `currentSubset` because `currentSubset` is modified in subsequent recursive calls. Without creating a new object, all elements in the powerset would be empty at the end.
  • Recursive Steps:
    • Include: We add nums[index] to the currentSubset and recursively call the function with index + 1.
    • Exclude: We remove the last element added to the currentSubset (backtracking) and recursively call the function with index + 1. This allows us to explore the branch where the element is not included in the subset.

Competitive Programming Examples

Example 1: Find all subsets that sum to a target value

Given a set of integers and a target value, find all subsets whose elements sum up to the target value.

import java.util.ArrayList;
import java.util.List;

public class SubsetSum {

    public static List<List<Integer>> findSubsetsWithSum(int[] nums, int target) {
    List<List<Integer>> result = new ArrayList<>();
    findSubsetsRecursive(nums, 0, new ArrayList<>(), target, 0, result);
    return result;
}

private static void findSubsetsRecursive(int[] nums, int index, List<Integer> currentSubset, int target, int currentSum, List<List<Integer>> result) {
    if (index == nums.length) {
        if (currentSum == target) {
            result.add(new ArrayList<>(currentSubset)); // Add a copy!
        }
        return;
    }

    // Include the current element
    currentSubset.add(nums[index]);
    findSubsetsRecursive(nums, index + 1, currentSubset, target, currentSum + nums[index], result);

    // Exclude the current element (backtrack)
    currentSum -= nums[index];  // Correctly adjust currentSum during backtracking
    currentSubset.remove(currentSubset.size() - 1);
    findSubsetsRecursive(nums, index + 1, currentSubset, target, currentSum, result);
}


public static void main(String[] args) {
    int[] nums = {1, 2, 3, 4, 5};
    int target = 7;
    List<List<Integer>> subsets = findSubsetsWithSum(nums, target);

    System.out.println("Subsets with sum " + target + ": " + subsets);
}
}

This code builds upon the power set generation to filter for subsets matching a specific sum. The findSubsetsRecursive method now includes parameters to track the target sum and the currentSum of the subset being built. During backtracking, the currentSum must also be adjusted. Critically, a *copy* of the currentSubset is made before adding to the result.

Example 2: Combination Sum

Given a set of candidate numbers (candidates) (without duplicates) and a target number (target), find all unique combinations in candidates where the candidate numbers sum to target. The same repeated number may be chosen from candidates unlimited number of times. For example, if candidates is [2, 3, 6, 7] and target is 7, one solution is [2, 2, 3] and another solution is [7].

import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;

public class CombinationSum {

    public static List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> result = new ArrayList<>();
    Arrays.sort(candidates); // Optional optimization for pruning
    findCombinations(candidates, target, 0, new ArrayList<Integer>(), result);
    return result;
}

private static void findCombinations(int[] candidates, int target, int start, List<Integer> currentCombination, List<List<Integer>> result) {
    if (target == 0) {
        result.add(new ArrayList<Integer>(currentCombination)); // Add a copy
        return;
    }

    if (target < 0) {
        return; // Pruning: No need to continue if target becomes negative
    }

    for (int i = start; i < candidates.length; i++) {
        currentCombination.add(candidates[i]);
        findCombinations(candidates, target - candidates[i], i, currentCombination, result); //Allow reuse of the same number
        currentCombination.remove(currentCombination.size() - 1); //Backtrack
    }
}

public static void main(String[] args) {
    int[] candidates = {2, 3, 6, 7};
    int target = 7;
    List<List<Integer>> combinations = combinationSum(candidates, target);
    System.out.println("Combinations for target " + target + ": " + combinations);
}
}

This problem allows for numbers to be reused multiple times. The key change from the previous example is that when making the recursive call, we pass i instead of i + 1 as the start index. This allows the same number to be chosen again. Also added pruning when target < 0 to improve performance.

Conclusion

Recursive backtracking is a versatile and powerful technique for solving combinatorial problems, including generating subsets, power sets, and finding combinations that satisfy specific constraints. Understanding the underlying principles of recursion and backtracking enables you to approach a wide range of problems efficiently and effectively in competitive programming and beyond.