[java] 回溯算法+剪枝

题型一:排列、组合、子集相关问题

这部分练习可以帮助我们熟悉「回溯算法」的一些概念和通用的解题思路。解题的步骤是:先画图,再编码。去思考可以剪枝的条件, 为什么有的时候用 used 数组,有的时候设置搜索起点 start 变量

组合问题用start变量,排列问题用used数组

初级-39. 组合总和

题目

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

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:

输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
  [7],
  [2,2,3]
]

示例 2:

输入:candidates = [2,3,5], target = 8,
所求解集为:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

提示:

  • 1 <= candidates.length <= 30
  • 1 <= candidates[i] <= 200
  • candidate 中的每个元素都是独一无二的。
  • 1 <= target <= 500

思路

偷一张图来表示一下构造的树,然后去剪枝,用start变量
[java] 回溯算法+剪枝_第1张图片
代码

class Solution {
    private int len=0;
    private List<List<Integer>> res = new ArrayList<List<Integer>>();
    private List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        len=candidates.length;
        if (candidates==null || len==0){
            return res;
        }
        Arrays.sort(candidates);
        search(target,candidates,0);
        return res;

    }
    public void search(int target,int[] candidates,int start){
        if(target<candidates[start] && target!=0) return;
        if(target==0) res.add(new ArrayList<>(path));
        else{
            for(int i=start;i<len && target>=candidates[i];i++){
                path.add(candidates[i]);
                search(target-candidates[i],candidates,i);
                path.remove(path.size()-1);
            }
        }
        
    }
}

初级-78. 子集

题目

给你一个整数数组 nums ,返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10

代码

class Solution {
    private List<List<Integer>> res = new ArrayList<List<Integer>>();
    private List<Integer> path = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        if (nums.length==0) return res;
        backtracking(nums,0);
        return res;

    }
    public void backtracking(int[] nums,int start){
        res.add(new ArrayList<>(path));
        for(int i=start;i<nums.length;i++){
            path.add(nums[i]);
            backtracking(nums,i+1);
            path.remove(path.size()-1);
        }
        
    }
}

初级-46. 全排列

题目

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

思路

再偷一张图,用used数组
[java] 回溯算法+剪枝_第2张图片

代码

class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    List<Integer> path=new ArrayList<Integer>();
    public List<List<Integer>> permute(int[] nums) {
        if(nums.length==0) return res;
        boolean[] used=new boolean[nums.length];
        backtracking(nums,used);
        return res;
    }
    public void backtracking(int[] nums,boolean[] used){
        if(path.size()==nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=0;i<nums.length;i++){
            if(!used[i]){
                used[i]=true;
                path.add(nums[i]);
                backtracking(nums,used);
                path.remove(path.size()-1);
                used[i]=false;
            }
        }
    }
}

高级-47. 全排列 II

题目

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

  • 1 <= nums.length <= 8
  • -10 <= nums[i] <= 10

思路

和46题的变化就是给数组排了个序,然后增加了一句

if(i>0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;

关于这一句的理解不是很容易,可以参考这个题解 liweiwei1419

代码

class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    List<Integer> path=new ArrayList<Integer>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        if(nums.length==0) return res;
        boolean[] used=new boolean[nums.length];
        Arrays.sort(nums);
        backtracking(nums,used);
        return res;
    }
    public void backtracking(int[] nums,boolean[] used){
        if(path.size()==nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        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;
            path.add(nums[i]);
            backtracking(nums,used);
            path.remove(path.size()-1);
            used[i]=false;
        }
    }
}

高级-40. 组合总和 II

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

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

说明:

  • 所有数字(包括目标数)都是正整数。
  • 解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
  [1,2,2],
  [5]
]

代码

// 前面的代码和上一题一样
    public void search(int target,List<Integer> list,int[] candidates,int start){
        if(target<0) return;
        if(target==0){
            res.add(new ArrayList<>(list));
        }
        else{
            for(int i=start;i<len && target>=candidates[i];i++){
                if(i>start && candidates[i]==candidates[i-1] ) continue;
                list.add(candidates[i]);
                search(target-candidates[i],list,candidates,i+1);
                list.remove(list.size()-1);
            }      
        }
        
    }

高级-90. 子集 II

题目
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: [1,2,2]
输出:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

思路

和46题的变化就是给数组排了个序,然后增加了一句

if(i>start && nums[i]==nums[i-1]) continue;

代码

class Solution {
    private List<List<Integer>> res = new ArrayList<List<Integer>>();
    private List<Integer> path = new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        if (nums.length==0) return res;
        Arrays.sort(nums);
        backtracking(nums,0);
        return res;

    }
    public void backtracking(int[] nums,int start){
        res.add(new ArrayList<>(path));
        for(int i=start;i<nums.length;i++){
            if(i>start && nums[i]==nums[i-1]) continue;
            path.add(nums[i]);
            backtracking(nums,i+1);
            path.remove(path.size()-1);
        }
        
    }
}

题型二:Flood Fill

提示:Flood 是「洪水」的意思,Flood Fill 直译是「泛洪填充」的意思,体现了洪水能够从一点开始,迅速填满当前位置附近的地势低的区域。类似的应用还有:PS 软件中的「点一下把这一片区域的颜色都替换掉」,扫雷游戏「点一下打开一大片没有雷的区域」。

下面这几个问题,思想不难,但是初学的时候代码很不容易写对,并且也很难调试。我们的建议是多写几遍,忘记了就再写一次,参考规范的编写实现(设置 visited 数组,设置方向数组,抽取私有方法),把代码写对。

  1. 图像渲染(Flood Fill,中等)
  2. 岛屿数量(中等)
  3. 被围绕的区域(中等)
  4. 单词搜索(中等)
    说明:以上问题都不建议修改输入数据,设置 visited 数组是标准的做法。可能会遇到参数很多,是不是都可以写成成员变量的问题,面试中拿不准的记得问一下面试官

题型三:字符串中的回溯问题

提示:字符串的问题的特殊之处在于,字符串的拼接生成新对象,因此在这一类问题上没有显示「回溯」的过程,但是如果使用 StringBuilder 拼接字符串就另当别论。
在这里把它们单独作为一个题型,是希望朋友们能够注意到这个非常细节的地方。

  1. 电话号码的字母组合(中等),题解;
  2. 字母大小写全排列(中等);
  3. 括号生成(中等) :这道题广度优先遍历也很好写,可以通过这个问题理解一下为什么回溯算法都是深度优先遍历,并且都用递归来写。

题型四:游戏问题

回溯算法是早期简单的人工智能,有些教程把回溯叫做暴力搜索,但回溯没有那么暴力,回溯是有方向地搜索。「力扣」上有一些简单的游戏类问题,解决它们有一定的难度,大家可以尝试一下。

  1. N 皇后(困难):其实就是全排列问题,注意设计清楚状态变量,在遍历的时候需要记住一些信息,空间换时间;
  2. 解数独(困难):思路同「N 皇后问题」;
  3. 祖玛游戏(困难)
  4. 扫雷游戏(困难)

你可能感兴趣的:(数据结构,剪枝)