LeetCode刷题——回溯算法

回溯算法

前言

「回溯是递归的副产品,只要有递归就会有回溯」,所以回溯法也经常和二叉树遍历,深度优先遍历( d f s dfs dfs)混在一起,因为这两种方式都是用了递归。

回溯法就是暴力搜索,优化回溯算法只有「剪枝」一种方法。

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

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

解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:

1、路径:也就是已经做出的选择。

2、选择列表:也就是你当前可以做的选择。

3、结束条件:也就是到达决策树底层,无法再做选择的条件。

代码的框架:

result = [];//结果集
void backtracking(路径,选择列表) {
    if (结束条件) {
        result.add(路径)//存放结果
        return;
    }

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

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」。

使用两个变量: res 和 path,res 表示最终的结果,path 保存已走过的路径。当满足结束条件,即到达了决策树的底层,就把 path 放到 res 中。

此总结参考于 labuladong的算法小抄 和 代码随想录。

组合问题

77,组合,medium

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

示例:

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

题解

n 相当于 树的宽度, k 相当于 树的高度。

Image

由上面框架,选择一个数就填入路径集path,结束条件:路径集path大小 = k。

在每层递归如何选择数呢?需要变量 index 记录下一层递归的起始位置,index + 1 ~ n即为下层递归的选择列表。

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        backtrack(n, k, 1, new ArrayList<>());
        return res;
    }
    public void backtrack(int n, int k, int index, List<Integer> path){
        if(path.size() == k){
            res.add(new ArrayList<>(path));
            return;
        }
        //横向遍历
        for(int i = index; i <= n ; i++){
            path.add(i);
            backtrack(n, k, i + 1, path);
            path.remove(path.size() - 1);
        }
    }
}

这里还可以进行优化,会将效率提高不少。若 n = 5, k = 4,现在 path.size() = 1,那还需 k - path.size() = 3 个数。若 index = 4,则只能选取 5,不满足,故 i 有限制条件。i <= n - (k - path.size()) + 1,即在集合n中至多要从该起始位置 : n - (k - path.size()) + 1开始遍历。

组合总和
216,组合总和Ⅲ,medium

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

说明:

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

输入: k = 3, n = 7
输出: [[1,2,4]]

示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]

题解

借鉴上一题的思路,组合中的数字为 1~9,则从1 开始分层遍历,结束条件即为 和为 n 且 path.size() = k。选择操作:将 i 添入路径,并加入和sum 中,撤销操作反之。

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum3(int k, int n) {
        backtracking(n, k, new ArrayList<>(), 1);
        return res;
    }
    public void backtracking(int n, int k, List<Integer> path, int index){
        if(sum == n && path.size() == k){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = index; i <= 9 ; i++){
            path.add(i);
            sum += i;
            backtracking(n, k, path, i + 1);
            sum -= i;
            path.remove(path.size() - 1);
        }
    }
}
39,组合总和,medium

给定一个无重复元素的数组 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

题解

题意:在无重复元素的数组中可重复选取元素使其和为target,结果集中的数组不重复。

此题的难点在于不产生重复组合。

错误:

for(int i = 0; i < candidates.length; i++){
    ...
    backtracking(i,...);
    ...
}

如果不使用 index,最后结果会有重复数组,如 [[2,2,3],[2,3,2],[3,2,2],[7]] 。

解决:仍需要 index,以使下一次选择的起点在当前选择的基础上,这样就不会选到本次选择同层左边的数。

在这里插入图片描述

正确:

for(int i = index; i < candidates.length; i++){
    ...
    backtracking(index,...);
    ...
}

规律:若只在一个集合中选取组合,需要开始索引 index,如此题和上面两题。

如果是多个集合取组合,各个集合之间相互不影响,那么就不用 index,如 17。

结束条件: sum == target 时填入路径,sum > target 时舍弃。

Image

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtracking(candidates, target, 0, new ArrayList<>());
        return res;
    }
    public void backtracking(int[] candidates, int target, int index, List<Integer> path){
        if(sum > target) return;
        if(sum == target){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = index; i < candidates.length; i++){
            path.add(candidates[i]);
            sum += candidates[i];
            backtracking(candidates, target, i, path);
            sum -= candidates[i];
            path.remove(path.size() - 1);
        }
    }
}
40,组合总和Ⅱ,medium

给定一个数组 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]
]

题解

与39题比较:

39题 数组元素无重复,可重复选取,解集无重复

40题 数组元素有重复,不可重复选取,解集无重复

关键在于「去重」,对此题构成的树,从上而下的同一树枝可以有重复元素,同一树层之间不可以有重复元素。如数组[1,1,2](为方便理解已排序),target = 3 时构成的树如图

Image

那么怎么区分树层的重复元素和树枝的重复元素呢?

使用boolean 数组 used,初始化为false,当选取元素改为true。

首先对数组进行排序,若相邻元素相等且前一元素已被同一树层使用过,跳过。代码表示为:

if(i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false)
    continue;  

Image

在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:

  • used[i - 1] == true,说明同一树支candidates[i - 1]使用过
  • used[i - 1] == false,说明同一树层candidates[i - 1]使用过

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {             boolean[] used = new boolean[candidates.length];
        Arrays.fill(used, false);
        //先排序使相同元素处于相邻位置
        Arrays.sort(candidates);
        backtracking(candidates, target, 0, new ArrayList<>(), used);
        return res;
    }
    public void backtracking(int[] candidates, int target, int index, List<Integer> path, boolean[] used){
        if(sum > target) return;
        if(sum == target){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = index; i < candidates.length ; i++){
            //同一树层不可以重复
            if(i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false)
                continue;   
            
            path.add(candidates[i]);
            sum += candidates[i]; 
            used[i] = true;
            backtracking(candidates, target, i + 1, path, used);
            used[i] = false;
            sum -= candidates[i];
            path.remove(path.size() - 1);
        }
    }
}
多个集合求组合
17,电话号码的字母组合,medium

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

在这里插入图片描述

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

输入:digits = ""
输出:[]

示例 3:

输入:digits = "2"
输出:["a","b","c"]

题解

与上面题目不同,本题是在多个集合中找组合,不需要开始索引 index。

此题需要注意的地方有很多:

  • 数字与字母的映射——可以用数组或Map

  • 结束条件怎么表示?用 idx 表示遍历到digits 的数字索引(即树的深度),当 idx == digits.length() 时,结束。

    Image

  • 怎么选择?对 每个数字digit 对应的字母集letter 进行遍历,选择 letter 中的字母。

代码

class Solution {
    List<String> res = new ArrayList<>();
    String[] str = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};//0-9对应字母集
    public List<String> letterCombinations(String digits) {
        if(digits.length() == 0) return res;
        backtracking(digits, new StringBuffer(), 0);
        return res;
    }
    public void backtracking(String digits, StringBuffer path, int idx){
        if(idx == digits.length()){
            res.add(path.toString());
            return;
        }
        int digit = digits.charAt(index) - '0';     // 将index指向的'数字'转为int
        String letter = str[digit]; 				// 第 index 个数字对应的字母集
        for(int i = 0; i < letter.length(); i++){
            path.append(letter.charAt(i));			//选择
            backtracking(digits, path, idx + 1);  //对下一层处理
            path.deleteCharAt(path.length() - 1);   //撤销选择
        }
    }
}

细节

  • StringBuffer 与 String 加入字母的区别:

因为StringBuffer传入的都是同一个对象,所以在递归完成之后必须撤回上一次的操作,需要删除上一次添加的字符。而String每次改变之后传入的都是不同的对象。故无需撤销操作。

for(int i = 0 ; i < letters.length() ; i ++){
	backtracking(digits, index + 1, s + letters.charAt(i));
}
  • int 与 char 类型的转换

    char 不能直接转为int,得到是‘3’的Ascii,如

    public static void main(String[] args) {
    	char numChar = '3';
        int intNum = (int)numChar;
        System.out.println(numChar + ": " + intNum);
    }
    

    输出为:

    3:51
    

    char 转为 int 的正确方法是:

    public static void main(String[] args) {
    	char numChar = '3';
        int intNum = numChar -0;
        System.out.println(numChar + ": " + intNum);
    }
    

    输出为:

    3:3
    

    而 int 可以强转为 char

    int a = 9;
    char b = (char)a; //b = '9'
    

子集问题

78,子集,medium

给你一个整数数组 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
nums 中的所有元素 互不相同

题解

与上面组合问题不同在于**「子集」**是这棵树的所有节点,而不是只有叶子节点。

解中不含重复子集,则取过的元素不会重复取,for 循环的开始索引 index,而不是 0。

Image

那结束条件是什么呢?可以不需要加终止条件,因为index >= nums.size(),本层for循环本来也结束了。

「求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树」

根据上面的模板有

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        backtracking(nums, 0, new ArrayList<>());
        return res;
    }
    public void backtracking(int[] nums, int index, List<Integer> path){
        //不需结束条件
        res.add(new ArrayList<>(path));
        for(int i = index; i < nums.length; i++){
            path.add(nums[i]);
            backtracking(nums, i + 1, path);
            path.remove(path.size() - 1);
        }
    }
}
90,子集Ⅱ,medium

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

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

示例:

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

题解

与 78 题的区别:

  • 78题 —— 数组不含重复元素,解不含重复子集。
  • 90题 —— 数组含重复元素,解不含重复子集。

此题与 40题 类似,解题思路也一致。

同一树层不能取相同元素(否则解中的子集会重复),而同一树枝可以有相同元素。

使用boolean数组 used ,初始化为false,当选取元素改为true。

首先对数组进行排序,若相邻元素相等且前一元素已被同一树层使用过,跳过。

Image

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        boolean[] used = new boolean[nums.length];
        Arrays.sort(nums);
        backtracking(nums, 0, used, new ArrayList<>());
        return res;
    }
    public void backtracking(int[] nums, int index, boolean[] used, List<Integer> path){
        res.add(new ArrayList<>(path));
        for(int i = index; i < nums.length; i++){
            if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)
                continue;
            path.add(nums[i]);
            used[i] = true;
            backtracking(nums, i + 1, used, path);
            used[i] = false;
            path.remove(path.size() - 1);
        }
    }
}
491,递增子序列,medium

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。

示例:

输入: [4, 6, 7, 7]
输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

题解

首先判断 此题的**「去重」**与 40、90题不同,上面的做法是将数组先排序再去重,防止同一层的相同元素重复使用,使解中出现重复子集。但此题要求递增子序列,不可打乱顺序。

采用 HashSet 去重,记录同层使用过的元素。

如果当前元素在 set 中有重复元素,则跳过。

那怎么保证递增呢?

Image

如果当前元素 小于 上一个选取的元素,则跳过。

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        backtracking(nums, 0, new ArrayList<>());
        return res;
    }
    public void backtracking(int[] nums, int idx, List<Integer> path){
        //结束条件
        if(path.size() > 1){
            res.add(new ArrayList<>(path));
            //注意此处没有return
        }
        Set<Integer> set = new HashSet<>();
        for(int i = idx; i < nums.length; i++){
            //当同层有相同元素已经在set中,或要加入的nums[i] < 上一个加入的元素时
            if(!path.isEmpty() && nums[i] < path.get(path.size() - 1) || set.contains(nums[i]))
            continue;
            
            set.add(nums[i]);
            path.add(nums[i]);
            backtracking(nums, i + 1, path);
            path.remove(path.size() - 1);
        } 
    }
}

细节

set 定义在递归函数上面,为了确保本层不选取重复元素。新的一层 set 都会重新定义(清空),所以要知道 set 只负责本层!

因为递增序列中至少两个元素,所以 path.size() > 1 才添加到 res 中,注意不能写 return,因为要记录树的所有节点。

添加 return 返回的结果为:

[[4,7],[4,6],[7,7],[6,7]]
小总结

如果给定数组中包含重复元素 / 组合和子集问题中要求解中不含重复结果 / 在一个集合中找组合,就需要开始索引 idx 对同层元素去重。(77,39,216,40)

子集问题不需要剪枝,因为要返回所有可能集合。不需要return。

切割问题

131,分割回文串,medium

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

示例:

输入: "aab"
输出:
[
  ["aa","b"],
  ["a","a","b"]
]

题解

不能重复截取,所以仍需要 idx

  • 结束条件:分割线到字符串末尾,将path填入 res 中。
  • 选择:如果当前形成的字符串[idx,i] 不是回文串,跳过。是则进行递归。
    在这里插入图片描述

代码

class Solution {
    List<List<String>> res = new ArrayList<>();
    public List<List<String>> partition(String s) {
        backtracking(new ArrayList<String>(), s, 0);
        return res;
    }
    public void backtracking(List<String> path, String s, int idx){
        //遍历完成
        if(idx == s.length()){
            res.add(new ArrayList<String>(path));
            return;
        }
       
        for(int i = idx; i < s.length(); i++){
             //子串 [idx,i]
            String str = s.substring(idx, i + 1);
            //如果不是回文,跳过
            if(!isPalindrome(str)){
                continue;
            }         
            path.add(str);
            backtracking(path, s, i + 1);
            path.remove(path.size() - 1);
        }
        
    }
    //判断是不是回文字符串
    public boolean isPalindrome(String str){
        int left = 0;
        int right = str.length() - 1;
        while(left < right){
            if(str.charAt(left) != str.charAt(right))
                return false;
            left++;
            right--;
        }
        return true;
    }
}

排列问题

46,全排列,medium

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

示例:

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

题解

  • 不需要idx:解中的组合是有序的,例如:在同一树层已经选择了 1,下一次选择还可以选 1,即 [2,1] ≠[1,2]。所以不需要 idx

  • 使用boolean 数组 used:全排列,组合中没有重复数字,同一树枝上不能重复选择。用used 数组记录当前元素是否已被选择。

  • 结束条件:当递归到树的叶子节点结束,path 添加到res 中。

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        boolean[] used = new boolean[nums.length];
        backtracking(nums, used, new ArrayList<>());
        return res;
    }
    public void backtracking(int[] nums, boolean[] used, List<Integer> path){
        int n = nums.length;
        //结束条件
        if(path.size() == n){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = 0; i < n; i++){
            //如果一个树枝上的元素已被选择(path已有nums[i]),跳过
            if(used[i] == true) continue;

            path.add(nums[i]);
            used[i] = true;
            backtracking(nums, used, path);
            used[i] = false;
            path.remove(path.size() - 1);
        }
    }
}

排列问题的不同:

  • 因为解中的数组是有序的,每层都是从0开始搜索而不是startIndex
  • 需要used数组记录path里都放了哪些元素了
47,全排列Ⅱ,medium

给定一个可包含重复数字的序列 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]]

题解

与46题的区别

此题有 重复数字,则在同一层不能选取相同数字,否则会出现重复排列。类似 40题的思路来去重。

在同一树枝中,同一个数字不能被重复选,需要通过 used[i] 判断是否已被选取过。

在这里插入图片描述

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        boolean[] used = new boolean[nums.length];
        //先排序
        Arrays.sort(nums);
        backtracking(nums, new ArrayList<>(), used);
        return res;
    }
    public void backtracking(int[] nums, List<Integer> path, boolean[] used){
        if(path.size() == nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = 0; i < nums.length; i++){
            //同一层相同元素已被选取,跳过
            if(i > 0 && used[i - 1] == false && nums[i] == nums[i - 1]) continue;
            //同一枝同一元素已被选取,跳过
            if(used[i] == true) continue;

            path.add(nums[i]);
            used[i] = true;
            backtracking(nums, path, used);
            used[i] = false;
            path.remove(path.size() - 1);
        }
    }
}

总结

注意解中有序和无序的区别:在 组合、子集、切割问题 中,一个集合的问题,需要开始索引 idx。排列问题从 0 开始遍历。

注意同一层的重复元素和同一树枝的重复元素的区别:常借用 boolean数组 记录被选择的元素,进行 去重

注意结束条件:树的节点,所有节点 或 叶子节点 或满足题意的节点。

其他

22,括号生成,medium

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]

提示:

1 <= n <= 8

题解

选择:将左括号 或 右括号 填入path。

结束条件:当左括号 和 右括号都用完了,或 path.length() == 2 * n 就结束。

遍历过程:

  • 剪枝:当选择的右括号数量 > 左括号。
  • 选择:当选择的左括号数量 < n,填入左括号;右括号同。
    在这里插入图片描述

代码

class Solution {
    List<String> res = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        backtracking(n, 0, 0, new StringBuilder());
        return res;
    }
    //l 表示已填入path 的左括号数量,r 表示已填入path 的右括号数量
    public void backtracking(int n, int l, int r, StringBuilder path){
        //结束条件
        // if(path.length() == 2 * n){
        if(l == n && r == n){
            res.add(path.toString());
            return;
        }
        //已选择的左括号数量 < 右括号,剪枝
        if(l < r) return;
        if(l < n){
            path.append("(");
            backtracking(n, l + 1, r, path);//向下递归
            path.deleteCharAt(path.length() - 1);
        }
        if(r < n){
            path.append(")");
            backtracking(n, l, r + 1, path);
            path.deleteCharAt(path.length() - 1);
        }
    } 
}
51,N皇后,hard

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

在这里插入图片描述

示例 1:

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

说明:皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。

题解
将棋盘的行当作树的层,使用 row 记录遍历到棋盘的行数。
结束条件:row == n 时,递归到棋盘最后一行,结束。

棋盘的宽度(列数)就是for循环的长度。横向搜索每一列,如果合法,选择当前位置放 ‘Q’,继续递归下一行,再撤回选择。

在这里插入图片描述

对于合法的判断;
当前行 row,需要检查同列的元素没有 Q、左上角对角线上没有 Q、右上角对角线上没有 Q。具体见代码。

代码

class Solution {
    List<List<String>> res = new ArrayList<>();
    public List<List<String>> solveNQueens(int n) {
        char[][] board = new char[n][n];
        //初始化二维数组都为'.'
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                board[i][j] = '.';
            }
        }
        backtracking(board, 0, n);
        return res;
    }
    public void backtracking(char[][] board, int row, int n){
        if(row == n){
            res.add(construct(board));
            return;
        }
        //横向遍历每列元素
        for(int i = 0; i < n; i++){
            //不合法就不能放皇后,跳过
            if(!isValid(board, row, i)) continue;
                
            board[row][i] = 'Q';
            backtracking(board, row + 1, n);//递归下一行
            board[row][i] = '.';
        }
    }
    /*
    row:当前行
    col:当前列
    */
    public boolean isValid(char[][] board, int row, int col){
        //检查同列元素
        for(int i = 0; i < row; i++){
            if(board[i][col] == 'Q')
                return false;
        }
        //检查左上角对角线元素是否为 Q
        for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--){
            if(board[i][j] == 'Q')
                return false;
        }
        //检查右上角对角线元素是否为 Q
        for(int i = row - 1, j = col + 1; i >= 0 && j < board[0].length; i--, j++){
            if(board[i][j] == 'Q')
                return false;
        }
        return true;
    }
    /*
    将二维数组board 转为 List<String>
    */
    public List<String> construct(char[][] board){
        List<String> path = new ArrayList<>();
        for(int i = 0; i < board.length; i++){
            path.add(new String(board[i]));
        }
        return path;
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值