【LeetCode】回溯 N皇后(DFS、子集、组合问题)

总结

技巧:

  • 子集和组合问题经常需要一个start来标记开始选择的起始位置来达到去重的目的,排列问题不用 【46.全排列】

  • 排列问题常需要一个used数组标记已经选择的元素(也可以同时用于去重)

C++ 总结了回溯问题类型 带你搞懂回溯算法(大量例题)

  • ①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
  • ②根据题意,确立结束条件
  • ③找准选择列表(与函数参数相关),与第一步紧密关联※
  • ④判断是否需要剪枝
  • ⑤作出选择,递归调用,进入下一层
  • ⑥撤销选择

【参考:回溯算法解题套路框架 :: labuladong的算法小抄

46、51题

  • 模版
result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

for 选择 in 选择列表:
    # 做选择
    将该选择再加入选择列表
    路径.add(选择)
    
    backtrack(路径, 选择列表)
    
    # 撤销选择
    路径.remove(选择)
    将该选择从选择列表移除

在这里插入图片描述

  • 切割问题类似于组合问题(131题、93题)
  • 子集和组合问题一般for循环从start开始
  • 排列问题一般for循环从0开始
    • 比如{1,2}求排列:{1},{2},{1,2},{2,1}
  • 去重先得对集合排序

C++ 总结了回溯问题类型 带你搞懂回溯算法(排列篇)

【参考:回溯算法入门级详解 + 练习(持续更新) - 全排列 - 力扣(LeetCode)巨详细

去重——用set去重
【参考:代码随想录# 回溯算法去重问题的另一种写法

中等

78.子集

【参考:78. 子集 - 力扣(LeetCode)

class Solution {

    List<List<Integer>> res=new ArrayList<>();

    public List<List<Integer>> subsets(int[] nums) {
        List<Integer> track=new ArrayList<>(); //走过的路径
        backtarck(nums,0,track);
        return res;
    }

    public void backtarck(int[] nums,int start,List<Integer> track){
    	if (start > nums.length) {
            return;
        }
 
        res.add(new ArrayList<>(track));
        
        for(int i=start;i<nums.length;i++){
            // 做选择(添加这个数)
            track.add(nums[i]);
            // 递归回溯(从i+1开始)
            backtarck(nums,i+1,track);
            // 撤销选择
            track.remove(track.size() - 1);
        }
    }
}

90. 子集 II

【参考:90. 子集 II - 力扣(LeetCode)

需要去重

class Solution {
    List<List<Integer>> res=new ArrayList<>();

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<Integer> track=new ArrayList<>();
        int[] used=new int[nums.length];
        Arrays.sort(nums);
        backtarck(nums,0,used,track);
        return res;
    }

    public void backtarck(int[] nums,int start,int[] used,List<Integer> track){
    	if (start > nums.length) {
            return;
        }
 
        res.add(new ArrayList<>(track));
        
        for(int i=start;i<nums.length;i++){
            // 去重
            if(i>0 && nums[i]==nums[i-1] && used[i-1]==0){
                continue;
            }
            // 做选择(添加这个数)
            track.add(nums[i]);
            used[i]=1;
            // 递归回溯(从i+1开始)
            backtarck(nums,i+1,used,track);
            // 撤销选择
            used[i]=0;
            track.remove(track.size() - 1);
        }
    }
}

77. 组合 ***

【参考:77. 组合 - 力扣(LeetCode)

【参考:代码随想录# 第77题. 组合

【参考:回溯算法 + 剪枝(Java) - 组合 - 力扣(LeetCode)这个写的巨详细

class Solution {
    List<List<Integer>> res=new ArrayList<>();

    public List<List<Integer>> combine(int n, int k) {
        if(k<=0 || n<=0) 
            return res;
        List<Integer> track=new ArrayList<>(); //走过的路径
        backtarck(n,k,1,track);// start从1开始
        return res;
    }
    // 范围 [1, n] 中所有可能的 k 个数的组合,start:从几开始
    public void backtarck(int n,int k,int start,List<Integer> track){
        if(k==track.size()){
            res.add(new ArrayList<>(track));
            return;
        }
        for(int i=start;i<=n;i++){
            // 做选择(添加这个数)
            track.add(i);
            System.out.println("递归之前 => " + track);
            // 递归回溯(从i+1开始)
            backtarck(n,k,i+1,track);
            // 撤销选择
            track.remove(track.size() - 1);
            System.out.println("递归之后 => " + track);
        }
    }

	public static void main(String[] args) {
        Solution solution = new Solution();
        int n = 5;
        int k = 3;
        List<List<Integer>> res = solution.combine(n, k);
        System.out.println(res);
    }
}

递归之前 => [1]
递归之前 => [1, 2]
递归之前 => [1, 2, 3]
递归之后 => [1, 2]
递归之前 => [1, 2, 4]
递归之后 => [1, 2]
递归之前 => [1, 2, 5]
递归之后 => [1, 2]
递归之后 => [1]
递归之前 => [1, 3]
递归之前 => [1, 3, 4]
递归之后 => [1, 3]
递归之前 => [1, 3, 5]
递归之后 => [1, 3]
递归之后 => [1]
递归之前 => [1, 4]
递归之前 => [1, 4, 5]
递归之后 => [1, 4]
递归之后 => [1]
递归之前 => [1, 5]
递归之后 => [1]
递归之后 => []
递归之前 => [2]
递归之前 => [2, 3]
递归之前 => [2, 3, 4]
递归之后 => [2, 3]
递归之前 => [2, 3, 5]
递归之后 => [2, 3]
递归之后 => [2]
递归之前 => [2, 4]
递归之前 => [2, 4, 5]
递归之后 => [2, 4]
递归之后 => [2]
递归之前 => [2, 5]
递归之后 => [2]
递归之后 => []
递归之前 => [3]
递归之前 => [3, 4]
递归之前 => [3, 4, 5]
递归之后 => [3, 4]
递归之后 => [3]
递归之前 => [3, 5]
递归之后 => [3]
递归之后 => []
递归之前 => [4]
递归之前 => [4, 5]
递归之后 => [4]
递归之后 => []
递归之前 => [5]
递归之后 => []
[[1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 3, 4], [1, 3, 5], [1, 4, 5], [2, 3, 4], [2, 3, 5], [2, 4, 5], [3, 4, 5]]

作者:liweiwei1419
链接:https://leetcode-cn.com/problems/combinations/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-ma-/

剪枝优化

【参考:代码随想录# 第77题. 组合-剪枝优化

  • 已经选择的元素个数:path.size();
  • 还需要的元素个数为: k - path.size();
  • 在集合n中起始位置i最大为 : n - (k - path.size()) + 1

为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。

举个例子,n = 4,k = 2, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 2 - 0) + 1 = 3。i最大从3开始搜索 (组合[3, 4])。也就是说 i ∈ [ 1 , 3 ] i\in[1,3] i[1,3]

这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。

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

216. 组合总和 III

【参考:216. 组合总和 III - 力扣(LeetCode)

class Solution {
    List<List<Integer>> res=new ArrayList<>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        List<Integer> track=new ArrayList<>(); //走过的路径
        backtarck(k,n,1,0,track);
        return res;
    }

    // sum 为已经收集的元素之和
    public void backtarck(int k, int n,int start,int sum,List<Integer> track){
        if(sum==n&&track.size()==k){
            res.add(new ArrayList<>(track));
            return;
        }               
        for(int i=start;i<10;i++){
            // 做选择(添加这个数)
            track.add(i);
            sum += i;
            // 递归回溯
            backtarck(k,n,i+1,sum,track); // 注意下一步start=i+1
            // 撤销选择
            sum -= i;
            track.remove(track.size() - 1);
        }
    }
}

39. 组合总和

【参考:39. 组合总和 - 力扣(LeetCode)

class Solution {

    List<List<Integer>> res=new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<Integer> track=new ArrayList<>(); //走过的路径
        backtarck(candidates,target,0,0,track);
        return res;
    }

    public void backtarck(int[] candidates,int target,int sum,int start,List<Integer> track){
        if (sum > target) {
            return;
        }
        if(sum==target){
            res.add(new ArrayList<>(track));
            return;
        }
                
        for(int i=start;i<candidates.length;i++){
            // 做选择
            track.add(candidates[i]);
            sum+=candidates[i];
            
            // 注意可以重复读取当前的数,所以下一次start=i
            backtarck(candidates,target,sum,i,track);// 递归回溯
            
            // 撤销选择
            sum-=candidates[i];
            track.remove(track.size() - 1);
        }
    }
}

分析

如果不需要记录选择的路径,只需返回结果总数

sum+=candidates[i];  // 做选择
backtarck(candidates,target,sum,i,track);// 递归回溯
sum-=candidates[i];// 撤销选择

或者将sum+candidates[i]直接写在函数里面
backtarck(candidates,target,sum+candidates[i],i,track);// 递归回溯
  • 剪枝
去掉
		if (sum > target) {
            return;
        }
        
修改
for(int i=start;
	i<candidates.length && sum+candidates[i]<= target;
	i++)

40.组合总和 II ***

【参考:40. 组合总和 II - 力扣(LeetCode)

使用used[]去重(具有普适性)

【参考:代码随想录# 40.组合总和II
在这里插入图片描述
我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:

  • used[i - 1] == 1,说明同一树枝candidates[i - 1]使用过
  • used[i - 1] == 0,说明同一树层candidates[i - 1]使用过(前一个树枝使用过)

前提:为了将重复的数字都放到一起,要先进行排序

boolean[] used = new boolean[n];// 默认为false

class Solution {

    List<List<Integer>> res=new ArrayList<>();
    List<Integer> track=new ArrayList<>(); //走过的路径

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);// 先排序
        //加标志数组,用来辅助判断同层节点是否已经遍历
        int[] used = new int[candidates.length];// 默认为0
        backtarck(candidates,target,0,0,used);
        return res;
    }

    public void backtarck(int[] candidates,int target,int sum,int start,int[] used){
        if (sum > target) {
            return;
        }
        if(sum==target){
            res.add(new ArrayList<>(track));
            return;
        }
                
        for(int i=start;i<candidates.length;i++){

            if (i > 0 && candidates[i] == candidates[i - 1] && used[i-1] == 0) {
                continue;
            }
            // 做选择
            track.add(candidates[i]);
            sum+=candidates[i];
            used[i]=1;
            // 递归回溯
            backtarck(candidates,target,sum,i+1,used);// 下一次start=i+1
            // 撤销选择
            used[i]=0;            
            sum-=candidates[i];
            track.remove(track.size() - 1);
        }
    }
}

直接使用start去重

【参考:回溯算法 + 剪枝(Java、Python) - 组合总和 II - 力扣(LeetCode)

解释语句: if cur > begin and candidates[cur-1] == candidates[cur] 是如何避免重复的。
【参考:回溯算法 + 剪枝(Java、Python) - 组合总和 II 高赞评论 - 力扣(LeetCode)

class Solution {

    List<List<Integer>> res=new ArrayList<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<Integer> track=new ArrayList<>(); //走过的路径
        Arrays.sort(candidates);
        backtarck(candidates,target,0,0,track);
        return res;
    }

    public void backtarck(int[] candidates,int target,int sum,int start,List<Integer> track){
        if (sum > target) {
            return;
        }
        if(sum==target){
            res.add(new ArrayList<>(track));
            return;
        }
                
        for(int i=start;i<candidates.length;i++){
        	// 这里去重是关键
            if (i > start && candidates[i] == candidates[i - 1]) {
                continue;
            }
            // 做选择
            track.add(candidates[i]);
            sum+=candidates[i];
            // 递归回溯
            backtarck(candidates,target,sum,i+1,track);// 下一次start=i+1
            // 撤销选择
            sum-=candidates[i];
            track.remove(track.size() - 1);
        }
    }
}

17. 电话号码的字母组合

【参考:17. 电话号码的字母组合 - 力扣(LeetCode)

class Solution {
    //初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
    String letterMap[] = {
            " ",    //0
            "",     //1
            "abc",  //2
            "def",  //3
            "ghi",  //4
            "jkl",  //5
            "mno",  //6
            "pqrs", //7
            "tuv",  //8
            "wxyz"  //9
    };
    List<String> reslut=new ArrayList<>();
    StringBuilder sb=new StringBuilder();
    public List<String> letterCombinations(String digits) {
        if(digits.length()==0){
            return reslut;
        }
        backTracking(digits,0);
        return reslut;
    }
    // 下标index表示digits中第index+1个字符
    public void backTracking(String digits,int index){
        if(index==digits.length()){
            reslut.add(sb.toString());// 添加
            return;
        }

        int num=digits.charAt(index)- '0';// 比如 '2'-'0'= 2
        String letter=letterMap[num];// 数字对于的字符串

        for(char c: letter.toCharArray()){
            sb.append(c);
            backTracking(digits,index+1);// 递归
            sb.deleteCharAt(sb.length()-1);// 回溯
        }

    }
}

131. 分割回文串

【参考:131. 分割回文串 - 力扣(LeetCode)

【参考:代码随想录# 131.分割回文串

class Solution {

    List<List<String>> result = new ArrayList<>();
    List<String> path = new ArrayList<>(); 

    public List<List<String>> partition(String s) {
        
        backtarck(s, 0);
        return result;
    }

    public void backtarck(String s, int start) {
        if (start > s.length()) {
            return;
        }
        if (start == s.length()) {
            result.add(new ArrayList<>(path));
            return;
        }

        for (int i = start; i < s.length(); i++) {
            // 不是回文则跳过
            if (!isPalindrome(s, start, i)) {
                continue;
            }
            // 做选择
            path.add(s.substring(start, i + 1));// +1 是因为substring函数是[)
            // 递归回溯
            backtarck(s, i + 1);
            // 撤销选择
            path.remove(path.size() - 1);
        }
    }

    public boolean isPalindrome(String s, int left, int right) {
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}

93. 复原 IP 地址(难)

【参考:93. 复原 IP 地址 - 力扣(LeetCode)

【参考:代码随想录# 93.复原IP地址

class Solution {
    List<String> result = new ArrayList<>();
    List<String> path = new ArrayList<>(); //走过的路径

    public List<String> restoreIpAddresses(String s) {
        // 如果长度不符合
        int len = s.length();
        if (len < 4 || len > 12) {
            return result;
        }
        backtarck(s, 0, 0);
        return result;
    }

    public void backtarck(String s, int start, int pointNum) {

        if (pointNum == 3) {// 逗点数量为3时,分隔结束
            // 判断第四段⼦字符串是否合法,如果合法就放进result中
            if (isValid(s, start, s.length() - 1)) {
                result.add(s);
            }
            return;
        }
        for (int i = start; i < s.length(); i++) {
            if (!isValid(s, start, i)) {
                break;
            } else {
                // substring:1.[beginIndex, endIndex) 2.beginIndex至末尾
                s = s.substring(0, i + 1) + "." + s.substring(i + 1); //在str的后⾯插⼊⼀个逗点
                pointNum++;// 点的数量加一
                backtarck(s, i + 2, pointNum);// 插⼊点之后下⼀个⼦串的起始位置为i+2
                pointNum--;// 回溯
                s = s.substring(0, i + 1) + s.substring(i + 2);// 回溯删掉点
            }
        }

    }

    // 判断字符串s在左闭⼜闭区间[start, end]所组成的数字是否合法
    private Boolean isValid(String s, int start, int end) {
        if (start > end) {
            return false;
        }
        if (s.charAt(start) == '0' && start != end) { // 0开头的数字不合法
            return false;
        }
        int num = 0;
        for (int i = start; i <= end; i++) {
            if (s.charAt(i) > '9' || s.charAt(i) < '0') { // 遇到⾮数字字符不合法
                return false;
            }
            num = num * 10 + (s.charAt(i) - '0');
            if (num > 255) { // 如果⼤于255了不合法
                return false;
            }
        }
        return true;
    }
}

491. 递增子序列(难)

【参考:491. 递增子序列 - 力扣(LeetCode)

【参考:代码随想录# 491.递增子序列

需要去重,但不能重新排序

set去重那块不太好理解,待回顾
【参考:代码随想录# 回溯算法去重问题的另一种写法

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    

    public List<List<Integer>> findSubsequences(int[] nums) {
        backtrack(nums, 0);
        return result;
    }

    public void backtrack(int[] nums, int start) {
        if (path.size() > 1) {
            result.add(new ArrayList<>(path));
            // 这里不要return
        }
        Set<Integer> set = new HashSet<>();// 仅对本层元素进行去重
        for (int i = start; i < nums.length; i++) {

            if ((!path.isEmpty() && nums[i] < path.get(path.size() - 1))
                    || set.contains(nums[i])) {
                continue;
            }

            set.add(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
            path.add(nums[i]);

            backtrack(nums, i + 1);

            path.remove(path.size() - 1);
            // set.remove(nums[i]);// 不需要该行,每次backtrack都会重置set
        }

    }
}

46. 全排列 ***

【参考:46. 全排列 - 力扣(LeetCode)

【参考:回溯算法解题套路框架 :: labuladong的算法小抄

// labuladong
List<List<Integer>> res = new LinkedList<>();

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 记录「路径」
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}

// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
    // 触发结束条件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (track.contains(nums[i])) {
            continue;
        }
        // 做选择
        track.add(nums[i]);
        // 进入下一层决策树
        backtrack(nums, track);
        // 取消选择
        track.removeLast();
    }
}

【参考:代码随想录# 46.全排列

class Solution {

    List<List<Integer>> result=new ArrayList<>();
    List<Integer> path=new ArrayList<>(); 

    public List<List<Integer>> permute(int[] nums) {
        //加标志数组,用来辅助判断
        int[] used = new int[nums.length];// 默认为0
        backtarck(nums,used);
        return result;
    }
	
    public void backtarck(int[] nums,int[] used){
        if(path.size()==nums.length){
            result.add(new ArrayList<>(path));
            return;
        }
                
        for(int i=0;i<nums.length;i++){
            if (used[i] == 1) {// path中已经收录的元素,直接跳过
                continue;
            }
            // 做选择
            path.add(nums[i]);
            used[i]=1;
            
            backtarck(nums,used);// 递归回溯
            // 撤销选择
            used[i]=0;            
            path.remove(path.size() - 1);
        }
    }
}

47. 全排列 II

【参考:47. 全排列 II - 力扣(LeetCode)

需要去重

class Solution {
   
    List<List<Integer>> result=new ArrayList<>();
    List<Integer> path=new ArrayList<>(); 

    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);// 先排序
        //加标志数组,用来辅助判断
        int[] used = new int[nums.length];// 默认为0
        backtarck(nums,used);
        return result;
    }

    public void backtarck(int[] nums,int[] used){
        if(path.size()==nums.length){
            result.add(new ArrayList<>(path));
            return;
        }
                
        for(int i=0;i<nums.length;i++){
            // 去重
            if (i > 0 && nums[i] == nums[i - 1] && used[i-1] == 0) {
                continue;
            }

            if (used[i] == 1) {// path中已经收录的元素,直接跳过
                continue;
            }
            // 做选择
            path.add(nums[i]);
            used[i]=1;
            
            backtarck(nums,used);// 递归回溯
            // 撤销选择
            used[i]=0;            
            path.remove(path.size() - 1);
        }
    }
}

困难

51. N 皇后 ***

【参考:51. N 皇后 - 力扣(LeetCode)

【参考:回溯算法解题套路框架 :: labuladong的算法小抄

这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。

因为 C++ 代码对字符串的操作方便一些,所以这道题我用 C++ 来写解法,直接套用回溯算法框架:

vector<vector<string>> res;

/* 输入棋盘边长 n,返回所有合法的放置 */
vector<vector<string>> solveNQueens(int n) {
    // '.' 表示空,'Q' 表示皇后,初始化空棋盘。
    vector<string> board(n, string(n, '.'));
    backtrack(board, 0);
    return res;
}

// 路径:board 中小于 row 的那些行都已经成功放置了皇后
// 选择列表:第 row 行的所有列都是放置皇后的选择
// 结束条件:row 超过 board 的最后一行
void backtrack(vector<string>& board, int row) {
    // 触发结束条件
    if (row == board.size()) {
        res.push_back(board);
        return;
    }
    
    int n = board[row].size();
    for (int col = 0; col < n; col++) {
        // 排除不合法选择
        if (!isValid(board, row, col)) {
            continue;
        }
        // 做选择
        board[row][col] = 'Q';
        // 进入下一行决策
        backtrack(board, row + 1);
        // 撤销选择
        board[row][col] = '.';
    }
}

/* 是否可以在 board[row][col] 放置皇后? */
bool isValid(vector<string>& board, int row, int col) {
    int n = board.size();
    // 检查列是否有皇后互相冲突
    for (int i = 0; i < n; i++) {
        if (board[i][col] == 'Q')
            return false;
    }
    // 检查右上方是否有皇后互相冲突
    for (int i = row - 1, j = col + 1; 
            i >= 0 && j < n; i--, j++) {
        if (board[i][j] == 'Q')
            return false;
    }
    // 检查左上方是否有皇后互相冲突
    for (int i = row - 1, j = col - 1;
            i >= 0 && j >= 0; i--, j--) {
        if (board[i][j] == 'Q')
            return false;
    }
    return true;
}

只想要一个答案,怎么办呢?
只要找到一个答案,for 循环的后续递归穷举都会被阻断。

// 函数找到一个答案后就返回 true
bool backtrack(vector<string>& board, int row) {
    // 触发结束条件
    if (row == board.size()) {
        res.push_back(board);
        return true;
    }
    ...
    for (int col = 0; col < n; col++) {
        ...
        board[row][col] = 'Q';

        if (backtrack(board, row + 1))
            return true;
        
        board[row][col] = '.';
    }

    return false;
}

【参考:代码随想录# 第51题. N皇后

class Solution {
    List<List<String>> result = new ArrayList<>();
    List<String> chessboard = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        // "." 表示空,"Q"表示皇后,初始化棋盘
        char[][] board = new char[n][n];
        for (char[] c : board) {
            Arrays.fill(c, '.');
        }
        backtrack(n, 0, board);
        return result;
    }
    // row 记录当前遍历到棋盘的第row+1层
    public void backtrack(int n, int row, char[][] board) {
        // 准备开始遍历到n+1行时,表明此时棋盘已铺满(row从0开始)
        if (row == n) {
            result.add(charToList(board));
        }

        // 按行遍历
        for (int col = 0; col < n; col++) {
            if (isVaild(row, col, n, board)) {
                board[row][col] = 'Q'; // 放置皇后
                backtrack(n, row + 1, board);// 递归下一行
                board[row][col] = '.'; // 回溯 撤销皇后
            }
        }
    }

    public boolean isVaild(int row, int col, int n, char[][] board) {
        // 检查列(正上方)
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 'Q')
                return false;
        }

        // 检查左上方
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 'Q')
                return false;
        }

        // 检查右上方
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 'Q')
                return false;
        }

        return true;
    }

    public List<String> charToList(char[][] board) {
        List<String> list = new ArrayList<>();

        for (char[] c : board) {
        	// System.out.println(String.valueOf(c));
            list.add(String.valueOf(c));
        }
        // System.out.println();
        return list;
    }
}
.Q..
...Q
Q...
..Q.

..Q.
Q...
...Q
.Q..

37. 解数独

【参考:37. 解数独 - 力扣(LeetCode)

【参考:回溯算法最佳实践:解数独 :: labuladong的算法小抄

【参考:代码随想录# 37. 解数独

有点没看懂 待回顾

class Solution {
    public void solveSudoku(char[][] board) {
        solveSudokuHelper(board);
    }

    private boolean solveSudokuHelper(char[][] board){
        //「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
        // 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
        for (int i = 0; i < 9; i++){ // 遍历行
            for (int j = 0; j < 9; j++){ // 遍历列
                if (board[i][j] != '.'){ // 跳过原始数字
                    continue;
                }
                for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适
                    if (isValidSudoku(i, j, k, board)){
                        board[i][j] = k;
                        if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回
                            return true;
                        }
                        board[i][j] = '.';
                    }
                }
                // 9个数都试完了,都不行,那么就返回false
                return false;
                // 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
                // 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
            }
        }
        // 遍历完没有返回false,说明找到了合适棋盘位置了
        return true;
    }

    /**
     * 判断棋盘是否合法有如下三个维度:
     *     同行是否重复
     *     同列是否重复
     *     9宫格里是否重复
     */
    private boolean isValidSudoku(int row, int col, char val, char[][] board){
        // 同行是否重复
        for (int i = 0; i < 9; i++){
            if (board[row][i] == val){
                return false;
            }
        }
        // 同列是否重复
        for (int j = 0; j < 9; j++){
            if (board[j][col] == val){
                return false;
            }
        }
        // 9宫格里是否重复
        int startRow = (row / 3) * 3;
        int startCol = (col / 3) * 3;
        for (int i = startRow; i < startRow + 3; i++){
            for (int j = startCol; j < startCol + 3; j++){
                if (board[i][j] == val){
                    return false;
                }
            }
        }
        return true;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值