leetcode-回溯总结

回溯算法能解决如下问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 棋盘问题:N皇后,解数独等等

如何理解回溯法

回溯法解决的问题都可以抽象为树形结构,是的,所有回溯法的问题都可以抽象为树形结构!

因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度

递归就要有终止条件,所以必然是一颗高度有限的树(N叉树)。

回溯法模板

回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。

如图:

回溯算法理论基础

从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。

回溯算法模板框架如下:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

求组合问题

组合是不强调元素顺序的,排列是强调元素顺序

例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了。

剪枝优化

在一些题目中是结果是对个数有要求的,比如:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。在这种情况下:如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

优化之后的for循环是:

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置

还有一些题目对元素总和有限制,比如:找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。在这种情况下:已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉

在求和问题中,排序之后加剪枝是常见的套路!

对于组合问题,什么时候需要startIndex呢?

去重

拿40.组合总和2举例:

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

题解代码如下:

    private List<List<Integer> > res;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        res = new LinkedList<>();
        Arrays.sort(candidates);
        LinkedList<Integer> track = new LinkedList<>();
        // 这里的used数组used[i]表示在已经选择的一条路径里(纵向的路径),candidates[i]是否被选择了
        boolean[] used = new boolean[candidates.length];
        backTrack(candidates,target,0,0,track,used);
        return res;
    }

    private void backTrack(int[] candidates,int target,int sum,int startIndex,LinkedList<Integer> track,boolean[] used) {
        if (sum == target) {
            res.add(new LinkedList<>(track));
            return;
        }

        for (int i = startIndex;i<candidates.length;i++) {
            // 假设现在排序后的candidates为[1,1,2,5,6,7,10]
            // 出现重复节点,同层的第一个节点已经被访问过,所以直接跳过,这里的used[i-1]为false说明在上一层里的选择节点(纵向的路径)里没有选择candidates[i-1]
            // 但是在上一层肯定已经对candidates[i-1]做了选择了(第一个1),然后又遍历了i+1之后的节点(2)作为下一层的选择节点,(1,2),所以此时used[i-1]为false,如果再选择candidates[i](第二个1)
            // 就会重复在后边继续选择下一层可选择的节点(2),这样就会出现重复(1,2)
            // 而此时如果used[i-1]==true时,说明上一层节点选择了第一个1,此时是可以选择第二个1的,这样就会有[1,1,6]这样的选择结果
            if (i > 0 && candidates[i] == candidates[i-1] && used[i-1] == false) {
                continue;
            }
            if (sum + candidates[i] > target) {
                break;
            }
            track.add(candidates[i]);
            // 置used[i]为true,表示在已选择的路径下包含了candidates[i]节点
            used[i] = true;
            backTrack(candidates,target,sum+candidates[i],i+1,track,used);
            used[i] = false;
            track.removeLast();
        }
    }

如代码注释那样:

假设现在排序后的candidates为[1,1,2,5,6,7,10]
这里的used[i-1]为false说明在上一层里的选择节点(纵向的路径)里没有选择candidates[i-1]
但是在上一层肯定已经对candidates[i-1]做了选择了(第一个1),然后又遍历了i+1之后的节点(2)作为下一层的选择节点,(1,2),所以此时used[i-1]为false,如果再选择candidates[i] (第二个1),就会重复在后边继续选择下一层可选择的节点(2),这样就会出现重复(1,2),而此时如果used[i-1]==true时,说明上一层节点选择了第一个1,此时是可以选择第二个1的,这样就会有[1,1,6]这样的选择结果,这样是符合的

切割问题

比如:分割回文子串:

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

代码题解如下:

   List<List<String> > res;
    public List<List<String>> partition(String s) {
        res = new LinkedList<>();
        LinkedList<String> track = new LinkedList<>();
        backTrack(s,0,track);
        return res;
    }

    // 这里的startIndex代表着分割位置,1...3代表着索引位置从1到3区间的字符串,这里的不停往后分割也就类似于往下一层的遍历树,每一层的节点就是一个分割区间,所以可以用回溯 
    private void backTrack(String s,int startIndex,LinkedList<String> track) {
        //如果起始位置大于s的大小,说明找到了一组分割方案
        if (startIndex >= s.length()) {
            res.add(new LinkedList<>(track));
            return;
        }

        for (int i = startIndex;i < s.length();i++) {
            // 这里要先判断分割的字符串满不满足回文串,如果不满足就不能继续下一层遍历,满足才可以进行后边的分割也就是下一层遍历
            if (isPalindrome(s,startIndex,i)) {
                track.add(s.substring(startIndex,i+1));
                backTrack(s,i+1,track);
                track.removeLast();
            }
        }
    }

    // 利用双指针判断是否是回文串
    private boolean isPalindrome(String s,int start,int end) {
        for (int i = start,j = end;i<j;i++,j--) {
            if (s.charAt(i) != s.charAt(j)) {
                return false;
            }
        }
        return true;
    }

如代码注释那样:

startIndex代表着分割位置,1...3代表着索引位置从1到3区间的字符串,这里的不停往后分割也就类似于往下一层的遍历树,每一层的节点就是一个分割区间,所以可以用回溯

在每一层的遍历里,要先判断分割的字符串满不满足回文串,如果不满足就不能继续下一层遍历,满足才可以进行后边的分割也就是下一层遍历,判断回文子串利用双指针来判断

求子集问题

在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果

最基础的就是每一次递归入口,都把track放入到res列表里

比较难得是491-求递增子序列:

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

这里的去重方法和之前的不一样,这里是要判断同一层是否重复访问了已访问的节点,需要在每一次递归的时候重新初始化一个used数组(因为数字范围是-100到100,所以数组长度初始化为201),访问之后把对应的used位置上的值置为1,

代码题解如下:

    List<List<Integer> > res;
    public List<List<Integer>> findSubsequences(int[] nums) {
        res = new LinkedList<>();
        // 这里获取原数组的递增子序列,所以不能排序改变结构
        LinkedList<Integer> track = new LinkedList<>();
        backTrack(nums,0,track);
        return res;
    }

    private void backTrack(int[] nums,int startIndex,LinkedList<Integer> track) {
        if (track.size() >= 2) {
            res.add(new LinkedList<>(track));
        }
        // 记录同层之间的节点是否已访问过,若已访问过就不能将该节点添加到下一层遍历中去;题目说数值范围[-100, 100]
        int[] used = new int[201];
        for (int i = startIndex;i<nums.length;i++) {
            // 如果同层之间nums[i] 已经访问过了,就不能再添加该节点了,因为是同层之间的判断,所以每一次递归都要初始化该数组
            if (used[nums[i]+100] == 1) {
                continue;
            }
            // 这里要保证track里的序列是有序递增的
            if (track.size() == 0 || (track.size() > 0 && track.get(track.size()-1) <= nums[i])) {
                track.add(nums[i]);
                used[nums[i]+100] = 1;
                backTrack(nums,i+1,track);
                track.removeLast();
            }
            
        }
    }

排列问题

排列问题的不同:

  • 每层都是从0开始搜索而不是startIndex
  • 需要used数组记录path里都放了哪些元素了
posted @ 2021-11-21 16:45  RealGang  阅读(90)  评论(0编辑  收藏  举报