leetcode解题笔记:backtracking类型解题思路

基本概念:

backtracking(回溯算法)也叫试探法,它是一种系统地搜索问题的解的方法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。

回溯算法说白了就是穷举法。不过回溯算法使用剪枝函数,剪去一些不可能到达最终状态(即答案状态)的节点,从而减少状态空间树节点的生成。
回溯法是一个既带有系统性又带有跳跃性的的搜索算法。它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。

  • 回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。
  • 而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。

这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。

解题思路:

在leetcode中比较经典的backtracking问题有以下几个:
51. N-Queens
52. N-Queens II
39. Combination Sum
40. Combination Sum II
216. Combination Sum III
46. Permutations
47. Permutations II
78. Subsets
90. Subsets II
131. Palindrome Partitioning

permutations

首先看 permutations 这个问题,是求一个数组的全排列,思路就是将数组当做一个池子,第一次取出一个数,然后在池子里剩下的数中再任意取一个数此时组成两个数,然后再在池子中剩下的数里取数,直到无数可取,即取完一次,形成一个排列。

public class Solution {
    private List<List<Integer>> result = new ArrayList<List<Integer>>();
    private int length;
    public List<List<Integer>> permute(int[] nums) {
        length = nums.length;
        List<Integer> select = new ArrayList<Integer>();
        helper(select,nums,0);
        return result;
    }
    //s代表已取出的数,nums则是有所有数的池子,pos代表要取第几个位置的数
    public void helper(List<Integer> s,int[] nums,int pos){
        //跳出条件是已取了池子里所有的数,完成一次排列
        if(pos == length){
            result.add(new ArrayList<Integer>(s));
            return;
        }
        for(int i=0;i<nums.length;i++){
            int num = nums[i];
            //取过的数不再取 
            if(s.contains(num)){
                continue;
            }
            s.add(num);
            helper(s,nums,pos+1);
            //重要!!遍历过此节点后,要回溯到上一步,因此要把加入到结果中的此点去除掉!
            s.remove(s.size()-1);
        }
    }
}

我总结其中最重要的有几点:

  • 递归函数的开头写好跳出条件,满足条件才将当前结果加入总结果中
  • 已经拿过的数不再拿 if(s.contains(num)){continue;}
  • 遍历过当前节点后,为了回溯到上一步,要去掉已经加入到结果list中的当前节点。

因此我们可以总结出一个递归函数的模板:

//list s是已取出的数,nums是原始数组,pos是当前取第几个位置的数
public void helper(List<Integer> s,int[] nums,int pos){
        //跳出条件
        if(……){
            ……
            return;
        }
        //遍历池子中的数
        for(int i=0;i<nums.length;i++){
            int num = nums[i];
            //取过的数不再取 
            if(s.contains(num)){
                continue;
            }
            //取出一个数
            s.add(num);
            //进行下一个位置的取数,pos+1
            helper(s,nums,pos+1);
            //重要!!遍历过此节点后,要回溯到上一步,因此要把加入到结果中的此点去除掉!
            s.remove(s.size()-1);
        }
    }
}

这是最原始的模板,根据题要求的不同会增加参数或各种判断条件,但都离不开这个框架。

subsets

其次再来看Subsets问题,是取一个数组的组合而不是全排列,基本代码结构都很相似,不同的有:

  • 不是在结果长度等于数组长度时才将结果加入总结果中,而是在每次递归中都将当前组合加入结果中,因为求的是子集而不是全排列。
  • 每次递归不是在池子中随便取一个数加入当前结果,因为此题要求的是子集,[1,3]和[3,1]是相同的,要求的是[1,3],因此每次在取数时,都要从其位置开始取后面的数,防止取到[3,1]这样的结果。
public class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<List<Integer>>();
        List<Integer> s = new ArrayList<Integer>();
        helper(result,s,nums,0,0);
        return result;
    }

    public void helper(List<List<Integer>> result,List<Integer> s,int[] nums,int pos,int iter){
  //不是在结果长度等于数组长度时才将结果加入总结果中,而是在每次helper中都将当前组合加入结果中,因为求的是子集而不是全排列。
        result.add(new ArrayList<Integer>(s));

        if(pos == nums.length){
            return;
        }
        //增加iter参数,用来代表当前数的位置,只能取此数后面的数
        for(int i=iter;i<nums.length;i++){
            if(s.contains(nums[i])){
                continue;
            }
            s.add(nums[i]);
            helper(result,s,nums,pos+1,i);
            s.remove(s.size()-1);
        }
    }
}

从这两道题可以有许多变种,但都可以归为permutations问题或subsets问题。求

follow-up

像上面这种permutations和subsets问题的II问题一般都是说数组中有重复的数
如Permutations II,[1,1,2] have the following unique permutations:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
这之中有两个重复的1,一次第一次选取了第一个1作为全排列的第一个数,得到112和121。但若选择第二个1作为全排列的第一个数,依旧能得到112和121,此时就会有重复的全排列出现,因此,在每一次选择同一位置的数时(如选择全排列第一个位置的数),只选择重复的数中的第一个。
为了保证重复的数在一起,一般这种问题都要对数组进行一个排序处理:

Arrays.sort(nums);

然后我再另外用一个数组来记录每个数在当前位置处有没有被取过:

int[] visited = new int[nums.length];

没有取过的话默认为0,取了则设为1;

因此比如当我们在全排列的第一个位置取了第二个1,而此时发现第一个1并没有被取过,这就说明第一个1还没有被作为全排列的第一个位置,因此此时就应该放弃取第二个1作为全排列的第一个位置。

因此递归函数应为:

public void helper(List<Integer> s,int[] nums,int[] visited,int pos){
        if(pos == length){
            List<Integer> list = new ArrayList<Integer>(s);
            result.add(list);
            return;
        }
        for(int i=0;i<nums.length;i++){
//如果此数取过了则不再取||此数和前面的数相同而前面的数没有取,则此数也不能取
            if(visited[i]==1 || (i>0 && nums[i] == nums[i-1]&& visited[i-1] == 0)){
                continue;
            }
            s.add(nums[i]);
            visited[i] = 1;
            helper(s,nums,visited,pos+1);
            //注意要回溯到上一步的话也要把visited当前位置还原
            s.remove(s.size()-1);
            visited[i] = 0;
        }
    }

subsets问题在处理重复数时同理。

N-Queens

最为经典的n皇后问题,其实也是求满足一定条件下的permutations问题,再加上判断皇后位置是否有效和画棋盘。但基本回溯思路和permutations是一致的:

public void helper(int n,int row,int[][] pos){
        //放置好最后一个皇后时完成一次排列
        if(row == n){
            draw(pos);
            return;
        }
        for(int i=0;i<n;i++){
            if(isValid(row,i,pos)){
                pos[row][i] = 1;
                helper(n,row+1,pos);
                //为回溯到上一步做准备
                pos[row][i] = 0;
            }
        }
    }

Combination Sum

Combination Sum其实求的是一个subsets问题,在给定的数组中任意取几个数和为最后的target,这其中一个数可以被重复取。
因此大致思路和subsets问题一致:

public void helper(List<List<Integer>> result,List<Integer> list,int[] candidates,int target,int sum,int pos){
        if(pos == candidates.length && sum!=target){
            return;
        }
        //结果可行的条件,需维护一个当前所有数的sum
        if(sum == target){
            result.add(new ArrayList<Integer>(list));
            return;
        }
        //和subsets一样,从当前位置向后取
        for(int i=pos;i<candidates.length;i++){
            //当前的数加上当前取出的数的和小于等于target时才继续向下
            if(sum+candidates[i] <= target){
                list.add(candidates[i]);

//每个数能取多次,因此传递的pos就是当前数的位置,这样下次可能再取
                    helper(result,list,candidates,target,sum+candidates[i],i);
                list.remove(list.size()-1);
            }
        }
    }

而Combination Sum II则是每个数只能取一次,这就和最原始的题一样,那么pos递归传递的就是pos+1;第二个不同是数组中可能有重复的数,那就参照上面的follow-up将原始数组排序并维护一个visited辅助数组。

Combination Sum III则是1-9中任取k个数和为n,是原始的subsets问题加上一些限制条件而已。

总结

backtracking类型题其实思路都是一致的,连解决follow-up都是一致的,只要掌握了思路,做同一类型的题都是手到擒来。

    原文作者:八皇后问题
    原文地址: https://blog.csdn.net/crystal6918/article/details/51924665
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞