Brute force search

排列、组合、子集问题

生成子集是所有的问题的基本框架

List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>(); // 记录回溯算法的递归路径

public List<List<Integer>> subsets(int[] nums) {
    backtrack(nums, 0);
    return res;
}

void backtrack(int[] nums, int start) {
    res.add(new LinkedList<>(track)); // 前序位置,每个节点的值都是一个子集
    for (int i = start; i < nums.length; i++) { // 从start位置行进到nums数组的末尾
        track.addLast(nums[i]); // 做选择
        backtrack(nums, i + 1);  // 通过为i+1的start参数控制树枝的遍历,避免产生重复的子集, 如果是i,那就允许重复使用
        track.removeLast(); // 撤销选择
    }
}

形式一、元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次

框架

 /* 组合/子集 框架 */
void backtrack(int[] nums, int start) {
    for (int i = start; i < nums.length; i++) {
        track.addLast(nums[i]);  // 做选择
        backtrack(nums, i + 1); 
        track.removeLast(); // 撤销选择
    }
}

/* 排列框架 */
void backtrack(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) { // 剪枝逻辑
            continue;
        }
        // 做选择
        used[i] = true;
        track.addLast(nums[i]);

        backtrack(nums);
        
        // 撤销选择
        track.removeLast();
        used[i] = false;
    }
}

组合

List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();// 记录回溯算法的递归路径

public List<List<Integer>> combine(int n, int k) {
    backtrack(1, n, k);
    return res;
}

void backtrack(int start, int n, int k) {
    if (k == track.size()) { // base case, 遍历到了第 k 层,收集当前节点的值
        res.add(new LinkedList<>(track)); 
        return;
    }
    for (int i = start; i <= n; i++) {
        track.addLast(i);  // 选择
        backtrack(i + 1, n, k); // 通过 start 参数控制树枝的遍历,避免产生重复的子集
        track.removeLast(); // 撤销选择
    }
}

排列

List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();// 记录回溯算法的递归路径
boolean[] used; // track 中的元素会被标记为 true

public List<List<Integer>> permute(int[] nums) {
    used = new boolean[nums.length];
    backtrack(nums);
    return res;
}

void backtrack(int[] nums) {
    if (track.size() == nums.length) { // base case,到达叶子节点,叶子保存着最后的排序结果
        res.add(new LinkedList(track));
        return;
    }
    for (int i = 0; i < nums.length; i++) { // 遍历的是可能进行排序的数
        if (used[i]) { // 已经存在 track 中的元素,不能重复使用,跳过
            continue;
        }

        used[i] = true; // 做选择
        track.addLast(nums[i]); // 把选中的要排列的数放入轨迹内
        
        backtrack(nums); // 进入下一层回溯树
        
        track.removeLast(); // 取消选择
        used[i] = false; 
    }
}

形式二、元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次

以组合为例,如果输入 nums = [2,5,2,1,2],和为 7 的组合应该有两种 [2,2,2,1][5,2]

框架

Arrays.sort(nums);
/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    for (int i = start; i < nums.length; i++) {
        if (i > start && nums[i] == nums[i - 1]) { // 剪枝逻辑,跳过值相同的相邻树枝
            continue;
        }
        
        track.addLast(nums[i]); // 做选择
        backtrack(nums, i + 1);
        track.removeLast();// 撤销选择
    }
}

Arrays.sort(nums);
/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) { // 剪枝逻辑
            continue;
        }
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { // 剪枝逻辑,固定相同的元素在排列中的相对位置
            continue;
        }
        // 做选择
        used[i] = true;
        track.addLast(nums[i]);

        backtrack(nums);
        
        // 撤销选择
        track.removeLast();
        used[i] = false;
    }
}

子集

给你一个整数数组 nums,其中可能包含重复元素,请你返回该数组所有可能的子集。比如输入 nums = [1,2,2],你应该输出:[ [],[1],[2],[1,2],[2,2],[1,2,2] ]

List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();

public List<List<Integer>> subsetsWithDup(int[] nums) {
    Arrays.sort(nums); // 先排序,让相同的元素靠在一起
    backtrack(nums, 0);
    return res;
}

void backtrack(int[] nums, int start) {
    res.add(new LinkedList<>(track));// 前序位置,每个节点的值都是一个子集
    
    for (int i = start; i < nums.length; i++) {
        if (i > start && nums[i] == nums[i - 1]){ // 剪枝,值相同的相邻树枝,只遍历第一条。比如输入数组有俩2,那有的时候相邻的会出现[1,2],[1,2']
            continue;
        }
        track.addLast(nums[i]);
        backtrack(nums, i + 1);
        track.removeLast();
    }
}

40. 组合总和 II - 力扣(LeetCode)

这题本质上跟子集是一样的,唯一不同的是只有路径和是正好等于target的时候才add到result set

List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
int trackSum = 0;

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Arrays.sort(candidates); // 先排序,让相同的元素靠在一起
    backtrack(candidates, 0, target);
    return res;
}

void backtrack(int[] nums, int start, int target) {
    if(trackSum == target){
        res.add(new LinkedList<>(track));// 前序位置,每个节点的值都是一个子集
    }
    
    if (trackSum > target) { // base case,超过目标和,直接结束,否则时间太长了
        return;
    }

    for (int i = start; i < nums.length; i++) {
        if (i > start && nums[i] == nums[i - 1]){ // 剪枝,值相同的相邻树枝,只遍历第一条。比如输入数组有俩2,那有的时候相邻的会出现[1,2],[1,2']
            continue;
        }
        track.addLast(nums[i]);
        trackSum += nums[i];
        backtrack(nums, i + 1, target);
        track.removeLast();
        trackSum -= nums[i];
    }
}

排列

比如输入 nums = [1,2,2],函数返回 [ [1,2,2],[2,1,2],[2,2,1] ]

本质上仍是全排列,但是因为有两个2,所以会出现12‘2和122’,但是他们其实是一样的,所以就要用一个used数组来去除已经用过的元素

List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
boolean[] used; //记录nums数组里的元素在当前路径上有没有被用过

public List<List<Integer>> permuteUnique(int[] nums) {
    Arrays.sort(nums); // 先排序,让相同的元素靠在一起
    used = new boolean[nums.length];
    backtrack(nums);
    return res;
}

void backtrack(int[] nums) {
    if (track.size() == nums.length) { // 只保留叶子
        res.add(new LinkedList(track));
        return;
    }

    for (int i = 0; i < nums.length; i++) {
        if (used[i]) { 
            continue;
        }
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { // 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
            // 如果前面的相邻相等(nums[i] == nums[i - 1])元素没有用过(!used[i - 1]),则跳过
            continue;
        }
        track.add(nums[i]);
        used[i] = true;
        backtrack(nums);
        track.removeLast();
        used[i] = false;
    }
}

形式三、元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次

/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    for (int i = start; i < nums.length; i++) {       
        track.addLast(nums[i]);// 做选择
        backtrack(nums, i);
        track.removeLast(); // 撤销选择
    }
}

/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        track.addLast(nums[i]);// 做选择
        backtrack(nums);
        track.removeLast();// 撤销选择
    }
}

39. 组合总和 - 力扣(LeetCode)

List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>(); // 记录回溯算法的递归路径
int sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    backtrack(candidates, 0, target);
    return res;
}

void backtrack(int[] nums, int start, int target) {
    if(sum + nums[start] == target){
    	res.add(new LinkedList<>(track)); // 前序位置,每个节点的值都是一个子集
        return;
    }

    for (int i = start; i < nums.length; i++) { // 从start位置行进到nums数组的末尾
        track.addLast(nums[i]); // 做选择
        sum += nums[i];
        backtrack(nums, i, target);  // 通过 start 参数控制树枝的遍历,避免产生重复的子集
        track.removeLast(); // 撤销选择
        sum -= nums[i];
    }
}
Previous
Next