回溯法专题总结

261 阅读3分钟

回溯法模板

image.png

void backtracking(参数) {
    // 子集问题直接存放结果,不需要终止条件判断
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点; 
        // 这里一般两种处理方法,
        // 一是直接收集结果,
        // 二是先进行if判断,这个判断多种多样,有时是判断是否符合某条件,有时是去重,
        // 根据判断,收集结果或者continue跳过。
        backtracking(路径,选择列表); // 递归,上图黑色箭头
        回溯,撤销处理结果  // 上图红色箭头
    }
}

回溯法去重

这里解释一下去重的意思。要知道一个元素在一个path中只能取一次,去重是在有重复元素(两个元素值相同)的情况下,为了防止出现两个重复的path,进行的去重操作。 所以,要去重的是同一树层上的“使用过”,同一树枝上的都是一个path集合里的元素,不用去重。

树层去重,需要对数组先排序!特殊情况除外(目前只遇到递增子序列问题不能排序)

去重方法

  1. 排序 + 使用set

在每个树层新建一个set(也就是每个for循环开始前定义set),处理节点前先判断set中是否包含该节点值。如果已包含,说明树层重复,continue;不包含则把该值加入到set,继续处理。

  1. 排序 + 使用used数组

推荐在排列问题使用,used数组可以一举两用。

            // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            // 要对同一树层使用过的元素进行跳过
            if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
                continue;
            }

排列问题下使用used数组去重示例 力扣47.全排列Ⅱ

image.png

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    boolean[] used;
    public List<List<Integer>> permuteUnique(int[] nums) {
        used = new boolean[nums.length];
        Arrays.sort(nums);
        back(nums);
        return res;

    }
    void back(int[] nums){
        if(path.size() == nums.length){
            res.add(new ArrayList(path));
            return;
        }
        for(int i = 0; i < nums.length; i++){
            if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false){   // 这里树层去重
                continue;
            }
            if(used[i] == false){  // 排列问题,用used数组保证一个path里一个元素只能使用一次
                used[i] = true;
                path.add(nums[i]);
                back(nums);
                used[i] = false;
                path.remove(path.size() - 1);               
            }
        }
    }
}

小tips: used数组定义为全局变量boolean[] used;,再在主要函数中初始化used = new boolean[nums.length];,这样在回溯函数中可以不传入used数组参数。

  1. 排序 + 使用startIndex
    推荐在组合或子集这种需要使用startIndex的问题使用。
  if (i > startIndex && candidates[i] == candidates[i-1]) continue;  

排列问题

  • 排列问题和组合问题、切割问题、子集问题最大的不同就是for循环里不用startIndex了,因为排列问题,每次都要从头开始搜索(for循环i从0开始)。
  • 排列问题需要记录path里都放了哪些元素了(因为每次for循环i从0开始,需要防止一个元素在一个path中取多次),可以使用used数组,在java中也可以使用if(path.contains(nums[i])) continue;做判断的方式。

组合问题,什么时候需要startIndex