一文通数据结构与算法之——回溯算法+常见题型与解题策略+Leetcode经典题

回溯算法

1 基本内容

1.1 回溯算法的框架

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

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

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

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

result = []
public List<Integer> backtrack(路径, 选择列表){
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择
}

1.2 回溯核心思想

1、每一次的backtrack,是在回溯深度,如二叉树的深度遍历,所以我们要知道深度在这道题中的意义

  • 比如对于N皇后问题,深度就是二维数组的行,所以每一次是backtrack(row+1)
  • 比如分割字符串问题,深度就是字符串的长度,所以每一次是backtrack(start+1)
  • 比如全排列问题,深度就是字符串的长度,所以每一次是backtrack(i+1)
back(s,start+i,path);
trackBack(nums,target-nums[i],i,path);

2、在每一个backTrace中的for循环代表什么意思,就是在当前状态下你的所有选择

  • 比如恢复IP中,你的选择是 用1还是2 还是 3作为这一段的长度
  • 对于N皇后问题,你的选择就是这一层的那一个列的位置 col作为皇后的位置
for (int i = start; i < num.length ; i++) {//长度的选择
    ...
}
for (int i = 1; i <=3 ; i++) {//切分的选择
    ...
}

3、剪枝

  • 不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。

  • 把没有必要的枝叶剪去的操作就是剪枝,在代码中一般通过 break 或者 continereturn (表示递归终止)实现

if(i>0 && nums[i]==nums[i-1] && !isVisit[i-1]){
    continue;
}
if(list.contains(num[i])){
    continue;
}
if(i==3 && temp.compareTo("255")>0){
    return ;
}

1.3 回溯法解决的问题

回溯法,一般可以解决如下几种问题:

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

注意组合和排列的区别:

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

{1, 2} 和 {2, 1} 在组合上,就是同一个,而要是排列的话,{1, 2} 和 {2, 1} 就是两个排列

1.4 题目列表

全排列

子集

组合

分割

棋盘

1.4 常见问题分析

明明设置了起始位置,但是结果中还是加入了起始位置前的元素——回溯起始位置问题的是i但是填成了start

输入:nums = [1,2,3]
输出:[[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3],[3,2]]//[3,2]这种情况

输出的集合中的元素,远超过限制的元素,忘记在回溯的时候撤销选择了

path.removeLast()

输出的组合比答案少——是否剪枝的时候没有排序?

if(target-nums[i]<0){
    break;
}

输出下面这种情况,是因为char[] 没有初始化

[[".Q\u0000\u0000","\u0000\u0000.Q","Q.\u0000\u0000",
"\u0000\u0000Q\u0000"],["..Q\u0000","Q\u0000..",
"..\u0000Q","\u0000Q.\u0000"]]

String知识

//String.compareTo()方法
//如果第一个字符和参数的第一个字符不等,结束比较,返回第一个字符的ASCII码差值。
//如果第一个字符和参数的第一个字符相等,则以第二个字符和参数的第二个字符做比较,以此类推,直至不等为止,返回该字符的ASCII码差值。 //如果两个字符串不一样长,可对应字符又完全一样,则返回两个字符串的长度差值。
	@Test
    public void test(){
        //输出 5,第一个字符相同,返回第二个字符差值的ASCII码值 5
        System.out.println("15".compareTo("10"));
        //输出 1,第一个字符不同,返回第一个字符差值的ASCII码值 1
        System.out.println("35".compareTo("255"));
        // 输出 4,第一个字符不同,返回第一个字符差值的ASCII码值 4
        System.out.println("5".compareTo("10"));
        // 输出 -1,第一个字符不同,返回第一个字符差值的ASCII码值 -1
        System.out.println("15".compareTo("25"));
    }

//将二维字符数组转化成String,用到的String.copyValueOf(char[])的
	public List<String> char2List(char[][] path){
        LinkedList<String> list = new LinkedList<>();
        for (char[] ch : path) {
            list.add(String.copyValueOf(ch));
        }
        return list;
    }

2 经典力扣题

2.1 全排列问题

图片
2.1.1 没有重复元素的全排列

剑指 Offer II 083. 没有重复元素集合的全排列==46. 全排列

给定一个不含重复数字的整数数组 nums ,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
	List<List<Integer>> res;
    public List<List<Integer>> permute(int[] nums) {
//        1.创建存放结果集的List
        res = new LinkedList<>();
        LinkedList<Integer> track = new LinkedList<>();
        trackBack(nums,track);
        return res;
    }

    public void trackBack(int[] num, LinkedList<Integer> list){
//        1.结束条件,全排列,全部元素都在里面
        if(list.size()==num.length){
            //为什么要 new LinkedList<>(list),因为list是一个引用,不创建新的话,还是会
            res.add(new LinkedList<>(list));
            return;
        }
//        2.确定遍历的集合时全部元素
        for (int i = 0; i < num.length; i++) {
//            3.首先需要将已经在列表中的元素排除
            if(list.contains(num[i])){
                continue;
            }
//            4.做出选择
            list.add(num[i]);
//            5.继续递归下去
            trackBack(num,list);
//            6.撤销选择
            list.removeLast();
        }
    }
2.1.2 含重复元素的递归全排列

47. 全排列 II

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

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

思路:这里所给元素是重复的,所以如何去处理重复元素

① 进行排序,那么相同的元素就会排在一起

	if(i>0 && nums[i]==nums[i-1] && !isVisit[i-1]){
         continue;
     }
  • 保证相同的元素只会在根节点回溯时只回溯一次nums[i]==nums[i-1],如图①
  • 为了避免[1,1,2]这种情况被剪枝,再加一个限制条件!isVisit[i-1],既前一个元素不在遍历的路径上

② 对使用过的元素进行标记

image-20211004094021327

	List<List<Integer>> ans;
    boolean[] isVisit;
    public List<List<Integer>> permuteUnique(int[] nums) {
//        1.前期处理
        if(nums==null || nums.length==0){
            return null;
        }
        ans = new LinkedList<>();
        LinkedList<Integer> path = new LinkedList<>();
//        2.排序
        Arrays.sort(nums);
//        3.回溯穷举
        trackBack2(nums,path);
        return ans;
    }

    public void trackBack(int[] nums,LinkedList<Integer> path){
//        1.结束条件,找到一组合格的解
        if(path.size()==nums.length){
            ans.add(new LinkedList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
//            1.首先需要将已经在列表中的元素排除
            if(isVisit[i]){
                continue;
            }
//            2.重复数字只会在第一次出现时填入一次,[1 1 2]这种情况还必须加上前一个元素没有被选上的条件
            if(i>0 && nums[i]==nums[i-1] && !isVisit[i-1]){
                continue;
            }
//            3.做出选择
            path.add(nums[i]);
            isVisit[i] = true;
//            4.回溯
            trackBack(nums,path);
//            5.撤销选择
            path.removeLast();
            isVisit[i] = false;
        }
    }

2.2 子集问题

2.2.1 不含重复元素的子集

78. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
	List<List<Integer>> res;
    public List<List<Integer>> subsets(int[] nums) {
        res = new LinkedList<>();
        if(nums==null || nums.length==0){
            res.add(new LinkedList<>());
            return res;
        }
        LinkedList<Integer> set = new LinkedList<>();
        trackBack(nums,0,set);
        return res;
    }

    public void trackBack(int[] num, int start, LinkedList<Integer> set){
//        1.因为是子集,所以结束条件不是长度,而是直接入结果集
        res.add(new LinkedList<>(set));
//        2.起始位置保证了不会有重复元素
        for (int i = start; i < num.length ; i++) {
            set.add(num[i]);
//            3.注意!!!这里是 i+1 而不是 start+1,找了半天的错误找不到
            trackBack(num,i+1,set);
            set.removeLast();
        }
    }
2.2.2 含重复元素的子集个数

90. 子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集

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

思路:

① 排序,使相同的元素在一堆

② 同全排列的思考,标记,当前一个相同的元素没选中,并且当前元素等于前一个元素,那么剪枝就可以去重了

	List<List<Integer>> ans;
    boolean[] isVisit;
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        ans = new LinkedList<>();
        isVisit = new boolean[nums.length];
        if(nums==null || nums.length==0){
            ans.add(new LinkedList<>());
            return ans;
        }
        Arrays.sort(nums);
        LinkedList<Integer> path = new LinkedList<>();
        backTrack(nums,0,path);
        return ans;
    }

    public void backTrack(int[] nums,int start,LinkedList<Integer> path){
        ans.add(new LinkedList<>(path));
        for (int i = start; i < nums.length; i++) {
//          1.思路同全排列,剪枝去重
            if(i>0 && nums[i]==nums[i-1] && !isVisit[i-1]){
                continue;
            }
            path.add(nums[i]);
            isVisit[i] = true;
            backTrack(nums,i+1,path);
            isVisit[i] = false;
            path.removeLast();
        }
    }
2.2.3 递增子序列

491. 递增子序列

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

**注意:**子序列是不能对它进行排序的会破坏原来的相对位置

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

思路:

这里使用到HashSet就非常好的印证了,在每一个for循环中,是表示同一层的元素,而回溯的代码中显示的则是深度,所以在此时使用的HashSet可以记录这一层已经遍历了哪些元素比如[1,3,3,7]——这一层 HashSet [3],表示已经遍历了3,就不会出现[1,3,7],[1,3,7]这种重复的现象

	public List<List<Integer>> findSubsequences(int[] nums) {
        res = new LinkedList<>();
        if(nums==null || nums.length==0){
            return res;
        }
        LinkedList<Integer> path = new LinkedList<>();
        back(nums,0,path);
        return res;
    }

    public void back(int[] nums,int start,LinkedList<Integer> path){
        if(path.size()>1){
            res.add(new LinkedList<>(path));
        }
        // 利用哈希表去重,同一层里面不能有两个相同的元素,相同既代表已经遍历过了
        HashSet<Integer> set = new HashSet<>();
        for (int i = start; i < nums.length; i++) {
            if(set.contains(nums[i])){
                continue;
            }
            set.add(nums[i]);
            if(path.size()==0 || nums[i]>=path.getLast()){
                path.add(nums[i]);
                back(nums,i+1,path);
                path.removeLast();
            }
        }
    }

2.3 组合问题

77. 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合

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

思路:

① 为了防止[1,2][2,1]这种重复,也是通过起始位置 start进行限制

② 回溯的终止条件是长度==输入要求的长度

List<List<Integer>> res;
    public List<List<Integer>> combine(int n, int k) {
        res = new LinkedList<>();
        if(k<=0 || n<k){
            return res;
        }
        int[] nums = new int[n];
        for (int i = 1; i <= n; i++) {
            nums[i-1] = i;
        }
        LinkedList<Integer> path = new LinkedList<>();
        backTrack(nums,k,0,path);
        return res;
    }

    public void backTrack(int[] num,int k,int start,LinkedList<Integer> path){
        if(path.size()==k){
            res.add(new LinkedList<>(path));
            return;
        }
        for (int i = start; i <num.length ; i++) {
            path.add(num[i]);
            backTrack(num,k,i+1,path);
            path.removeLast();
        }
    }
39. 组合总和

给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。candidates 中的数字可以无限制重复被选取。

输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]

方法一:回溯,不断的在元素中循环进行回溯,每一次的起始位置都不变

这里尤其需要主要排序+target-nums[i]<0剪枝部分的优化,不排序剪枝会少结果,剪枝效率高些

image-20211002142919420
	List<List<Integer>> ans;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if(candidates==null || candidates.length==0){
            return null;
        }
        ans = new LinkedList<>();
        //排序,因为后面用到了 sum + candidates[i]>target就结束本轮for循环的遍历。如果不排序的话回导致结果少
        //而这样会做到剪枝的效果,所以还是这样
        Arrays.sort(candidates);
        LinkedList<Integer> path = new LinkedList<>();
        trackBack(candidates,target,0,path);
        return ans;

    }

    public void trackBack(int[] nums,int target,int start,LinkedList<Integer> path){
        //结束回溯的条件
        if(target==0){
            ans.add(new LinkedList<>(path));
            return;
        }
        for (int i = start; i < nums.length; i++) {
            //剪枝
            if(target-nums[i]<0) {
                break;
            }
            //做出选择
            path.add(nums[i]);
            //回溯
            trackBack(nums,target-nums[i],i,path);
            //撤销选择
            path.removeLast();
        }
    }

方法二:递归的形式,如下图,有点类似于上台阶,两种情况加起来,选或者不选

image-20211002104345233

	List<List<Integer>> ans;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if(candidates==null || candidates.length==0){
            return null;
        }
        ans = new LinkedList<>();
        LinkedList<Integer> path = new LinkedList<>();
        Arrays.sort(candidates);
        trackBack(candidates,target,0,path);
        return ans;
    }

    public void dfs(int[] nums,int target,int start,LinkedList<Integer> path){
        if(start>=nums.length){
            return;
        }
        if(target==0){
            ans.add(new LinkedList<>(path));
            return;
        }
        //不将当前的元素加入
        dfs(nums,target,start+1,path);
        //正常加入
        if(target-nums[start]<0) {
            return;
        }
        path.add(nums[start]);
        //因为元素是不限数量的,下次还是从start位置开始
        dfs(nums,target-nums[start],start,path);
        path.removeLast();
    }
40. 组合总和 II

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

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:[[1,1,6],[1,2,5],[1,7],[2,6]]

思路:

① 输入有重复的元素,需要去重,使用boolean[]数组进行标记

② 选择过的元素不能再选择,需要start去控制起始遍历位置

	List<List<Integer>> res;
	boolean[] isVisited;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        res = new LinkedList<>();
        if(candidates==null || candidates.length==0){
            return null;
        }
        isVisited = new boolean[candidates.length];
        LinkedList<Integer> path = new LinkedList<>();
        Arrays.sort(candidates);
        track(candidates,target,0,path);
        return res;
    }

    public void track(int[] nums,int target,int start,LinkedList<Integer> path){
        if(target==0){
            res.add(new LinkedList<>(path));
            return ;
        }
        for (int i = start; i < nums.length; i++) {
            if(target-nums[i]<0){
                break;
            }
            if(i>0 && nums[i]==nums[i-1]&& !isVisited[i-1]){
                continue;
            }
            path.add(nums[i]);
            isVisited[i] = true;
            track(nums,target-nums[i],i+1,path);
            path.removeLast();
            isVisited[i] = false;
        }
    }
216. 组合总和 III

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

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
	public List<List<Integer>> combinationSum3(int k, int n) {
        res = new LinkedList<>();
        if(k==0 || n==0){
            return res;
        }
        LinkedList<Integer> path = new LinkedList<>();
        int[] nums = {1,2,3,4,5,6,7,8,9};
        backTrack(nums,0,k,n,path);
        return res;
    }

    public void backTrack(int[] nums,int start,int k,int target,LinkedList<Integer> path){
//      回溯结束的条件,和与长度
        if(path.size()==k){
            if(target==0){
                res.add(new LinkedList<>(path));
            }
            return;
        }
        
        for (int i = start; i <nums.length ; i++) {
            if(target-nums[i]<0){
                break;
            }
            path.add(nums[i]);
            backTrack(nums,i+1,k,target-nums[i],path);
            path.removeLast();
        }
    }
17. 电话号码的字母组合

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

img
List<String> ans;
    public List<String> letterCombinations(String digits) {
        ans = new LinkedList<>();
        if(digits==null || digits.length()==0){
            return ans;
        }
        HashMap<Character,String> map = new HashMap<>();
        map.put('2',"abc");
        map.put('3',"def");
        map.put('4',"ghi");
        map.put('5',"jkl");
        map.put('6',"mno");
        map.put('7',"pqrs");
        map.put('8',"tuv");
        map.put('9',"wxyz");
        StringBuffer path = new StringBuffer();
        traceBack(map,digits,0,path);
        return ans;
    }

    public void traceBack(Map<Character,String> map, String digits,int index, StringBuffer path){
//        1.结束的条件
        if(index== digits.length()){
            ans.add(path.toString());
            return ;
        }
        char digit = digits.charAt(index);
        String s = map.get(digit);
//        1.遍历所有数字
        for (int i = 0; i < s.length(); i++) {
//            选择当前数字的一位
            path.append(s.charAt(i));
//            再去选择下一个数字的元素
            traceBack(map,digits,index+1,path);
            path.deleteCharAt(index);
        }
    }

2.4 分割问题

131. 分割回文串

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

	List<List<String>> res;
    public List<List<String>> partition(String s) {
        res = new LinkedList<>();
        LinkedList<String> path = new LinkedList<>();
        backTrack(s,0,path);
        return res;
    }
    public void backTrack(String s,int index,LinkedList<String> path){
//        1.回溯结束的条件,这一部分需要注意,我想不到是 >= 的关系
        if(index>=s.length()){
            res.add(new LinkedList<>(path));
            return ;
        }
//        遍历,判断什么时候成回文
        for (int i = index; i < s.length(); i++) {
//          2.当前切分的区间是回文时才进入
            if(isCyc(s.substring(index,i+1))){
//                注意subString是左闭右开的区间
                path.add(s.substring(index,i+1));
                backTrack(s,i+1,path);
                path.removeLast();
            }
        }
    }
    public boolean isCyc(String s){
        int end = s.length()-1;
        int begin = 0;
        while(begin<end){
            if(s.charAt(begin)!=s.charAt(end)){
                return false;
            }
            begin++;
            end--;
        }
        return true;
    }
93. 复原 IP 地址

给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

思路:

① 一个字符有三种切法:2 25 255,所以回溯中就按1-3的循环来

② 回溯结束的条件,字段长为4,并且刚刚用完所有字符

③ 有关剪枝

  • 第一种情况是 前导为0的情况
  • 第二种是长度为3的切片>255的范围
  • 超出长度
	List<String> ans;
    final int COUNT = 4;
    public List<String> restoreIpAddresses(String s) {
        ans = new LinkedList<>();
        LinkedList<String> path = new LinkedList<>();
//        特殊情况排除
        if(s.length()<4 || s.length()>12){
            return ans;
        }
        back(s,0,path);
        return ans;
    }

    public void back(String s,int start,LinkedList<String> path){
//        1.回溯结束的条件,找到四段,或者索引超长
        if(start>=s.length()){
            if(path.size() == COUNT){
                StringBuilder str = new StringBuilder();
                for (int i = 0; i < path.size(); i++) {
                    if(i<COUNT-1){
                        str.append(path.get(i)).append(".");
                    }else{
                        str.append(path.get(i));
                    }
                }
                ans.add(str.toString());
            }
            return ;
        }
//        2.枚举出选择,三种切割长度
        for (int i = 1; i <COUNT ; i++) {
            //不判断会导致 indexOutOfBound 错误
            if(start+i>s.length()){
                return;
            }
//            3.不能有前导 0,不能切出'0x'、'0xx'
            if(i!=1 && s.charAt(start)=='0'){
                return;
            }
//            4.要在0-255范围内
            String temp = s.substring(start,i+start);
//            注意我原来的思路是,temp.compareTo("0")>=0 &&temp.compareTo("255")<=0
//            这样会导致不必要的剪枝,比如 "35".compareTo("255")=1,就被剪枝了
            if(i==3 && temp.compareTo("255")>0){
                return ;
            }
            path.add(temp);
            back(s,start+i,path);
            path.removeLast();
        }
    }

2.5 棋盘问题

51. N 皇后

n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上

该方案中 'Q''.' 分别代表了皇后和空位。

image-20211003111551513
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
	List<List<String>> res;
    public List<List<String>> solveNQueens(int n) {
        res = new LinkedList<>();
        if(n==0){
            return res;
        }
        char[][] path = new char[n][n];
        for (char[] ch : path) {
            Arrays.fill(ch,'.');
        }
        backTrace(0,path);
        return res;
    }

    public void backTrace(int row,char[][] path){
//        二维矩阵中矩阵的高就是这颗树的高度,回溯的是树的高度
//        1.回溯结束的条件:所有深度都填上了皇后
        if(path.length==row){
            res.add(char2List(path));
            return ;
        }
//      矩阵的宽就是树形结构中每一个节点的宽度,在每一次回溯里面遍历的是数的宽度
        int n = path[row].length;
        for (int col = 0; col < n; col++) {
//            2.判断当前位置是否合法
            if(!isValid(path,row,col)){
                continue;
            }
            path[row][col] = 'Q';
            backTrace(row+1,path);
            path[row][col] = '.';
        }
    }
    public boolean isValid(char[][] path,int row,int col){
//        因为是深度回溯,所以当前行不可能有其他元素,不需要判断
//        1.遍历所有列是否冲突
        for (int i = 0; i < row; i++) {
            if(path[i][col]=='Q'){
                return false;
            }
        }
//        2.遍历45°斜线是否有元素
        for (int i = row-1,j=col-1; i >=0 && j>=0; i--,j--) {
            if(path[i][j]=='Q'){
                return false;
            }
        }
//        3.遍历135°斜线是否有元素
        for (int i = row-1,j=col+1; i >=0 && j<path.length; i--,j++) {
            if(path[i][j]=='Q'){
                return false;
            }
        }
        return true;
    }

    public List<String> char2List(char[][] path){
        LinkedList<String> list = new LinkedList<>();
        for (char[] ch : path) {
            list.add(String.copyValueOf(ch));
        }
        return list;
    }
37. 解数独

编写一个程序,通过填充空格来解决数独问题。数独部分空格内已填入了数字,空白格用 ‘.’ 表示。数独的解法需 遵循如下规则:

  • 数字 1-9 在每一行只能出现一次。
  • 数字 1-9 在每一列只能出现一次。
  • 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

img img

输入:board = 
    [["5","3",".",".","7",".",".",".","."],
     ["6",".",".","1","9","5",".",".","."],
     [".","9","8",".",".",".",".","6","."],
     ["8",".",".",".","6",".",".",".","3"],
     ["4",".",".","8",".","3",".",".","1"],
     ["7",".",".",".","2",".",".",".","6"],
     [".","6",".",".",".",".","2","8","."],
     [".",".",".","4","1","9",".",".","5"],
     [".",".",".",".","8",".",".","7","9"]]
输出:
    [["5","3","4","6","7","8","9","1","2"],
     ["6","7","2","1","9","5","3","4","8"],
     ["1","9","8","3","4","2","5","6","7"],
     ["8","5","9","7","6","1","4","2","3"],
     ["4","2","6","8","5","3","7","9","1"],
     ["7","1","3","9","2","4","8","5","6"],
     ["9","6","1","5","3","7","2","8","4"],
     ["2","8","7","4","1","9","6","3","5"],
     ["3","4","5","2","8","6","1","7","9"]]

思路:

  • 先每一行进行填充,行填充玩了,再进入下一行填充,范围都是0-9
  • 如果当前位置已经有元素了,直接跳过
  • 在填充元素时需要进行判断填充的元素是否合法
    • 行是否有重复元素
    • 列是否有重复元素
    • 小三角块是否有重复
// 对于3*3 小块的索引技巧,如下表示(4,5)位置处的小块
// i/3在0-9的范围是(0 0 0,1 1 1,2 2 2),所以出现(3 3 3,4 4 4,5 5 5),符合行规律
// i%3在0-9的范围是(0 1 2,0 1 2,0 1 2),所以出现(3 4 5,3 4 5,3 4 5),符合列规律
for (int i = 0; i < COUNT; i++) {
    System.out.print("行:"+((4/3)*3+i/3)+" ");
    System.out.println("列:"+((5/3)*3+i%3));
}
输出:
    行:3 列:3
    行:3 列:4
    行:3 列:5
    行:4 列:3
    行:4 列:4
    行:4 列:5
    行:5 列:3
    行:5 列:4
    行:5 列:5
	/**
     * 解数独问题
     * @param board
     */
    static final int COUNT=9;
    public void solveSudoku(char[][] board) {
        if(board==null || board.length==0){
            System.out.println(Arrays.deepToString(board));
        }
        back(board,0,0);
        System.out.println(Arrays.deepToString(board));
    }
    public boolean back(char[][] board,int row,int col){
//        1.如果列达到限制了,从下一行重新开始
        if(col==COUNT){
            return back(board,row+1,0);
        }
//        2.如何此时行也到达了最后一行,那么找到一组解
        if(row==COUNT){
            return true;
        }
//        3.当前位置已经有数字了,直接跳过
        if(board[row][col]!='.'){
            return back(board,row,col+1);
        }
//        4.万事具备,开填数字
        for(char i='1';i<='9';i++){
            if(!isVal(board,row,col,i)){
                continue;
            }
            board[row][col] = i;
//            5.只要找到一个,可以直接返回
            if(back(board,row,col+1)){
                return true;
            }
            board[row][col] = '.';
        }
        return false;
    }

    public boolean isVal(char[][] board,int row,int col,char ch){
        for (int i = 0; i < COUNT; i++) {
//        1.行是否有重复
            if(board[row][i]==ch){
                return false;
            }
//        2.列是否有重复
            if(board[i][col]==ch){
                return false;
            }
//        3.所属小三角块是否重复,(row/3)*3 == 小块的行
            if(board[(row/3)*3+i/3][(col/3)*3+i%3]==ch){
                return false;
            }
        }
        return true;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值