当我谈递归与回溯时我在谈些什么

本文深入探讨了递归和回溯算法,通过电话号码的字母组合、全排列、组合问题等实例,阐述了如何利用树形结构分析问题,并提供了相关Java代码实现。文章还涉及了FloodFill算法及其在岛屿数量和被围绕区域问题中的应用,以及N皇后和解数独问题的解决方案,展示了回溯法在解决组合和排列问题中的核心思想。
摘要由CSDN通过智能技术生成

递归算法能够解决的一个典型问题,是具有树形结构的问题,当我们发现一个问题与一个更小的问题之间存在递推的关系时,递归关系呈现出来的是一个树形结构

树形问题:分析问题时,发现解决问题的思路本质是一棵树的形状

  • 回溯算法是在树形图上的深度优先遍历
  • 回溯的产生是由于深度优先遍历的回退,造成了状态重置或撤销选项

解决回溯问题的第一步是画出树形图

回溯的本质是DFS,回溯只是DFS产生的现象

  • 本质上是遍历的算法,全程使用一份状态变量去搜索状态空间里的所有状态
  • 具有DFS的自叶子到根的特点
    在这里插入图片描述
    17. 电话号码的字母组合
    在这里插入图片描述
    通过分析,'23’可分解为以下树形
    在这里插入图片描述
    得到以下推断过程:
    得到以下分析:

设,digits代表数字字符串,s(digits)是digits所能代表的字母字符串,,letter( digits[i] ) 表示第 i 个数字能表示的所有字母

则有:

  • s( digits[0…n-1] )

    = letter( digits[0] ) + s( digits[1…n-1] )

    = letter( digits[0] ) + letter( digits[1] ) + s( digits[2…n-1] )

    = …

因此,不难得出如下思路步骤

思路步骤:

  1. 每次从digits中取出一个数字字符,找到其对应的字母字符串并循环

  2. 以当前的字母字符串为基础,从digits中取出下一个数字字符,找出字母字符串,递归取出所有数字字符并将当前字符与之前的字符串进行拼接 (s( digits[0…n-1] ) = letter( digits[0] ) + s( digits[1…n-1] ))

  3. 直到构成的解 s 长度与digits长度相同 (因为有多少数字组成的每一个字母字符串就有多少位,因此只要s的长度等于digits的长度),就说明完成了一个字母组合,存入list

  4. 递归上述步骤,直到所有第一个数字对应的字母字符串的所有字母组合完毕
    Java代码实现如下:

/**
 * @Author: antigenMHC
 * @Date: 2020/3/27 11:47
 * @Version: 1.0
 **/
public class test {

    public static void main(String[] args) {

        List<String> strings = new Solution().letterCombinations("23");
        for (String s : strings) {
            System.out.println(s);
        }
    }

}


class Solution {

    private String[] letterTable = new String[]{
            "",      //0
            "",      //1
            "abc",   //2
            "def",   //3
            "ghi",   //4
            "jkl",   //5
            "mno",   //6
            "pqrs",  //7
            "tuv",   //8
            "wxyz",  //9
    };

    private List<String> lists = new LinkedList<>();
    //返回值为digits所能表示的所有字母字符串组成的list
    public List<String> letterCombinations(String digits) {
        find(digits, 0, "");

        return lists;
    }
    //index表示每次处理的数字在数字字符串中的位置
    //该函数每次处理一位数字!
    //s表示index之前已经转换了的数字,即 s ( digits[0, index-1] )
    //寻找和digits[index]匹配的字母字符串,获得 letter( digits[index] )
    //s ( digits[0, index-1] ) + letter( digits[index] ) = s ( digits[0, index] ) 即得到了一个新解
    private void find(String digits, int index, String s){

        //到达digits末尾,生成一个字母字符串
        if(s.length() == digits.length()) {
            lists.add(s);
            return;
        }

        char nowDigits = digits.charAt(index);   //获取对应数字字符
        assert (nowDigits >= '2' && nowDigits <= '9');

        String letters = letterTable[nowDigits-'0'];  //提取数字对应的字母字符串

        //遍历digits对应的字符串,每次得到一个字母,然后继续向下搜索,找出其后后面所有数字的组合
        for(int i=0; i<letters.length(); i++){

            //s+letters.charAt(i), 基于当前字母组合继续向下寻找
            //digits有多少数字,最终的每一个字母字符串就有多少位,因此只需要将数字对应的字母字符串中的每一个字母取出来,与前面的拼接,然后继续对下一个数字
            //进行字母字符的求解即可
            find(digits, index+1, s+letters.charAt(i));

        }

        return;
    }
}

上述代码画出图来可以很好地理解

通过递归调用来寻找答案的过程也称为回溯,回溯法是暴利解法的一个主要实现手段

从树的形状来看和结果来看,结果的个数大致为 3n (n为树深度),所以时间复杂度为 O(2n)

接下来看几个回溯法能够处理的问题

1.排列问题

46. 全排列

在这里插入图片描述
继续先分析出树形结构,如下
在这里插入图片描述
可以发现思路如下:
排列 ( nums[0…n-1] ) = {取出一个数字} + 排列 ( nums[0…n-1 - 这个数字] )

排列问题中,元素之间是相互冲突的,因此需要判断和回溯(使用了一个元素并存入list中后,去掉这个元素,选择下一个元素)

Java代码实现如下:

class Solution {

    private List<List<Integer>> reList = new ArrayList<>();

    //一个List<Integer>为一个排列
    public List<List<Integer>> permute(int[] nums) {

        if(nums.length==0) return reList;

        find(nums, 0, new ArrayList<>());

        return reList;
    }

    //index表示第index个元素,l表示保存了index个元素的排列
    //当传进index+1时,l就获得一个index+1个元素的排列
    private void find(int[] nums, int index, List<Integer> l){
        //至多组成nums.length的长度的一个排列,当index==nums.length时说明生成了一个排列到l中
        //保存l到reList
        if(index == nums.length){
            //保存当前生成的这个排列
            reList.add(new ArrayList<>(l));
            return;
        }
        //遍历每一个数字,找到需要添加的第index+1个数字
        for(int i=0; i<nums.length; i++){
            //l中不存在这个数字,则添加
            if(!l.contains(nums[i])){
                l.add(nums[i]);
                find(nums, index+1, l);
                //在排列问题中,元素之间是相互冲突的,因此在回溯时需要对l所存的内容也进行回溯
                //回退一个元素,不然l中就始终存在nums[i]了,nums[i]会被重复使用
                //回溯状态和变量!
                l.remove(l.size()-1);
            }
        }
        return;
    }
}

排列问题中:一个数字在不同排列中可以出现多次,因此每次递归 i 从0开始查找所有可能

2.组合问题

77. 组合

在这里插入图片描述
同样,先分析出其树形结构
在这里插入图片描述
在组合问题中,前一次被取用元素在下一次中不再取用
因此,只需要对排列问题稍加修改即可
Java代码实现如下:

class Solution {

    private List<List<Integer>> reList = new ArrayList<>();
    private List<Integer> tmp = new ArrayList<>();
    private List<Boolean> used = new ArrayList<Boolean>();
    private int pre = -1;

    public List<List<Integer>> combine(int n, int k) {

        if(k==0 || n==0) return reList;

        List<Integer> list = new ArrayList<>();
        for(int i=0; i<=n; i++){
            list.add(i);
            used.add(false);
        }

        find(list, k, 1);
        return reList;
    }

    private void find(List<Integer> n, int k, int index){

        if(tmp.size() == k){
            reList.add(new ArrayList<>(tmp));
            return;
        }

        //组合问题基本模板
        for(int i=index; i<n.size(); i++){
            tmp.add(n.get(i));
            //前一个分支把第i个数及以前的数字的所有情况都考虑了进去,因此从i+1个元素开始考虑
            find(n, k, i+1);
            //回溯,探寻弹出的元素可以组成的其他组合
            //回溯算法中基本上都要
            pre = tmp.remove(tmp.size()-1);
        }
    }
}

其实,通过上面的树形图,可以很容易发现,分支4是不需要的,假如我们的递归深度很深的话,那最后一个分支会占用大量的时空复杂度,因此我们可以将其剪去,这就是经典的剪枝思想
在这里插入图片描述
对于n=4,k=2的情况而言,根本不需要去考虑取4,即最后一个元素的情况,因此可以尝试将取4的情况 “剪掉”

对于分析发现:

  • 每次循环,储存组合的list中还有 k-list.size()个空位
  • 所以 [i … n] 中至少要有 k-list.size() 个元素来填补list的空位,于是 i 如何求呢?
    • k-l.size() = 1时, i(最大) = n = n-1+1, [n…n]就有1个元素刚好填补
    • k-l.size() = 2时, i(最大) = n-1= n-2+1, [n-1…n] 就有2个元素刚好填补
    • k-l.size() = 3时, i(最大) = n-2 = n-3+1, [n-2…n] 3个元素刚好填补
  • 于是推导得 i <= n-(k-l.size())+1 <= n,i 不能大于 n-(k-l.size())+1 ,会导致需要填补的元素变多,空位不足

由此,获得剪枝后的Java代码如下:

class Solution {

    private List<List<Integer>> reList = new LinkedList<>();

    public List<List<Integer>> combine(int n, int k) {

        if(k==0 || n==0 || k>n) return reList;

        find(n, new ArrayList<>(), k, 1);
        return reList;
    }

    private void find(int n, List<Integer> l, int k, int start){

        if(l.size() == k){
            reList.add(new ArrayList<>(l));
            return;
        }
        // i<=n-(k-l.size())+1 剪枝条件
        // list中还有k-l.size()个空位
        // 所以 [i .... n] 中至少要有 k-l.size() 个元素填补,因此问题为 i 取多少时[i .... n]有k-l.size() 个元素?
        // k-l.size() == 1时, i(最大)==n==n-1+1, [n...n]就有1个元素刚好填补
        // k-l.size() == 2时, i(最大)==n-1==n-2+1, [n-1...n] 就有2个元素刚好填补
        // k-l.size() == 3时, i(最大)==n-2==n-3+1, [n-2...n] 3个元素刚好填补
        //因此 i <= n-(k-l.size())+1 <= n
        //i最大等于n-(k-l.size())+1,因为如果i比n-(k-l.size())+1还大的话,list中就没有足够的空位了
        for(int i=start; i<=n-(k-l.size())+1; i++){
            l.add(i);
            //前一个分支把第i个数及以前的数字的所有情况都考虑了进去,因此从i+1个元素开始考虑
            find(n, l, k, i+1);
            l.remove(l.size()-1);
        }
    }
}

39. 组合总和
在这里插入图片描述

class Solution {

    private Set<List<Integer>> set = new HashSet<>();
    private List<List<Integer>> reList = new LinkedList<>();
    private List<Integer> tmp = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {

        if(candidates.length == 0) return reList;

        find(candidates, target, 0, 0);
        reList.addAll(set);

        return reList;
    }

    private void find(int[] candidates, int tartget, int index, int sum){
        if(sum > tartget) return;
        if(sum==tartget || index>candidates.length){
            set.add(new ArrayList<>(tmp));
            return;
        }
        //从index开始,每一位都重复添加,看是否与target相等
        for(int i=index; i<candidates.length; i++){
            tmp.add(candidates[i]);
            //传入i,重复相加
            find(candidates, tartget, i, sum+candidates[i]);
            tmp.remove(tmp.size()-1);
        }
    }
}

40. 组合总和 II
在这里插入图片描述

class Solution {

    private Set<List<Integer>> set = new HashSet<>();
    private List<List<Integer>> reList = new LinkedList<>();
    private List<Integer> tmp = new ArrayList<>();
    private Set<Integer> canSet = new HashSet<>();
    private int pre = -1;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {

        Arrays.sort(candidates);   //排序,相邻大小数挨在一起

        find(candidates, target, 0, 0);
        reList.addAll(set);

        return reList;
    }
    //每个数只能选择一次, 不允许同级出现同样大小的元素,但不同层可以
    private void find(int[] candidates, int tartget, int index, int sum){

        if(sum > tartget) return;

        if(sum==tartget){
            set.add(new ArrayList<>(tmp));
            return;
        }

        for(int i=index; i<candidates.length; i++){
            //用pre记录删除了的元素,表示已经使用过,不再使用,剪枝
            if(pre == candidates[i]) continue;

            tmp.add(candidates[i]);
            //一个数只能用一次,i+1
            find(candidates, tartget, i+1, sum+candidates[i]);
            pre = tmp.remove(tmp.size()-1);
        }

    }
}

216. 组合总和 III
在这里插入图片描述

class Solution {

    private Set<List<Integer>> reSet = new HashSet<>();
    private List<Integer> tmp = new ArrayList<>();
    private int[] arr = new int[]{1,2,3,4,5,6,7,8,9};
    private boolean[] used = new boolean[]{false,false,false,false,false,false,false,false,false};

    public List<List<Integer>> combinationSum3(int k, int n) {

        find(n,k,0, 0);

        List<List<Integer>> reList = new ArrayList<>(reSet);
        return reList;
    }

    private void find(int target, int k, int index, int sum){

        if(sum > target || tmp.size() > k) return;
        if(sum == target && tmp.size()==k){
            reSet.add(new ArrayList<>(tmp));
            return;
        }

        for(int i=index; i<9; i++){
            if(!used[i]){
                tmp.add(arr[i]);
                used[i] = true;
                find(target, k, i, sum+arr[i]);
                used[i] = false;
                tmp.remove(tmp.size()-1);
            }
        }
    }
}

组合题规律总结

当题干包含:

  • 在这里插入图片描述时,使用Set集合去重

  • 在这里插入图片描述使用辅助数字判断哪个数字已经被使用过,在生成一个组合后将数字和存储组合的列表回溯 (如216,面试题08)

  • 在这里插入图片描述循环时 i 以传入第index位基位,递归时重复传入当前 i 表示可以重复使用 (如39题)

  • 在这里插入图片描述循环时 i 以传入第index位基位,递归时传入 i+1,表示前 i 个数字已经存在当前组合中 (这种情况下可以考虑剪枝提高效率) (如40题)

二维平面上的回溯法

比如最常见的迷宫问题
79. 单词搜索
在这里插入图片描述
简单分析树形如下:
在这里插入图片描述
基于传入的坐标,对当前坐标的四周进行试探,当满足条件是进入坐标,继续试探,被试探过的坐标应该标记为visited,当该坐标四周试探完成不满足条件后,应该将vitised回溯为未访问状态,以便让其相邻位置进行试探

class Solution {

    //设置偏移量数组的技巧在二维数组问题中经常使用
    //4个方位,每个一维数组中的第一个元素代表startX的移动,第二个元素代表startY的移动
    private int map[][] = new int[][]{  {-1,0},   //上
                                        {0,1},    //右
                                        {1,0},    //下
                                        {0,-1}};  //左
    private Boolean[][] visited;

    public boolean exist(char[][] board, String word) {

        if(board.length==0 || word==null) return false;

        visited  = new Boolean[board.length][board[0].length];

        for(int i=0; i<board.length; i++){
            for(int j=0; j<board[i].length; j++){
                visited[i][j] = false;
            }
        }

        for(int i=0; i<board.length; i++){
            for(int j=0; j<board[i].length; j++){
                //每次从[i][j]出发,寻找相应的字符串
                if (searchWord(board, word, 0, i, j)) {
                    return true;
                }
            }
        }

        return false;
    }
    //每次查找word的第index个元素
    //从board[startX][startY]开始,寻找word[index...word.length()]
    private boolean searchWord(char[][]board, String word, int index, int startX, int startY){

        //当前所处的board位置是否与字符串最后一位相同
        if(index == word.length()-1){
            return board[startX][startY] == word.charAt(index);
        }
        //元素相等时,继续搜索
        if(board[startX][startY] == word.charAt(index)){
            //设置为已经访问过该元素
            visited[startX][startY] = true;
            //从startX,startY开始朝四个方向出发进行搜索
            for(int i=0; i<4; i++){
                int newX = startX + map[i][0];
                int newY = startY + map[i][1];
                //newX,newY不能越界 && board[newX][newY]没有被访问过
                if((newX>=0 && newX<board.length && newY>=0 && newY<board[0].length) &&
                        !visited[newX][newY]){
                    if (searchWord(board, word, index+1, newX, newY)) {
                        return true;
                    }
                }
            }
            //基于[startX][startY]找遍4个方向都找不到的话,放弃[startX][startY]位置,置位false
            visited[startX][startY] = false;
        }
        return false;
    }
}

FloodFill算法(洪泛填充算法)

FloodFill的过程其实就是 DFS 的过程

200. 岛屿数量
在这里插入图片描述
我们需要找到与一个陆地直接相邻,即同属于一个岛屿的其它陆地(递归寻找),进行标记,这样在寻找下一个岛屿的时候才不会重复。

从某个位置开始向外扩散,直到没有相连的陆地,这个过程就是FloodFill的过程

这里的 FloodFill算法 不需要进行状态回溯,因为当递归未结束时找到的所有陆地一定属于了一个岛屿,如果回溯了的话就说明不属于这个岛屿了

class Solution {

    private boolean[][] visited;
    private int[][] map = new int[][]{{-1,0},{0,1},{1,0},{0,-1}};

    private int islands;
    public int numIslands(char[][] grid) {

        if(grid.length==0) return 0;

        visited = new boolean[grid.length][grid[0].length];
        for (int i = 0; i < grid.length; i++) {
            for(int j = 0; j < grid[i].length; j++){
                visited[i][j] = false;
            }
        }

        for (int i = 0; i < grid.length; i++) {
            for(int j = 0; j < grid[i].length; j++){
                //FloodFill :本质就是一次DFS
                //递归终止条件隐含在这,只有陆地才会归入岛屿
                //再次循环时,对于同属于这块岛屿的其它陆地就不会再进行查找
                if(grid[i][j] == '1' && !visited[i][j]){
                    islands++;
                    findIslands(grid, i, j);
                }
            }
        }

        return islands;
    }

    private void findIslands(char[][] grid, int startX, int startY){

        for(int i=0; i<4 ;i++) {
            //不仅标识是否被访问,同时也标识已经属于一个岛屿了
            visited[startX][startY] = true;
            int newX = startX+map[i][0];
            int newY = startY+map[i][1];
            if(newX>=0 && newX<grid.length && newY>=0 && newY<grid[0].length && !visited[newX][newY]){
                if(grid[newX][newY] == '1'){
                    findIslands(grid, newX, newY);
                }
            }
            //这里不需要状态回溯,因为当递归未结束时找到的所有陆地一定属于了一个岛屿,如果回溯了的话就说明不属于这个岛屿了
        }
    }
}

这里有另外一种更简洁的解法

//一次dfs找到一个岛屿
class Solution {
    public static void dfs(char[][] grid, int i, int j) {
		if(i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1') return;
		else grid[i][j] = '2';			// 将找到的 '1' 标注为 2,防止便利时再被找到
		dfs(grid, i, j+1);				// 搜索上下左右的 '1'
		dfs(grid, i-1, j);
		dfs(grid, i+1, j);
		dfs(grid, i, j-1);
	}

	public static int numIslands(char[][] grid) {
		int count = 0;
		for (int i = 0; i < grid.length; i++) {
			for (int j = 0; j < grid[0].length; j++) {
				if(grid[i][j] == '1') {
					++count;
					dfs(grid, i, j);
				}
			}
		}
		return count;
    }
}

130. 被围绕的区域
在这里插入图片描述
只要与边界上的O相邻的所有O都不会被反转成X,因此可以认为是一个反向的FloodFill,从边界上的每一个O开始递归,将递归到的所有为O的位置标记为true (在对grid遍历时对于X的位置就可以标记为true)。最后将不为true的位置(即不与边界相连)上的所有元素转换成X

class Solution {

    int[][] map = new int[][]{{-1,0},{0,1},{1,0},{0,-1}};
    boolean[][] visited;

    public void solve(char[][] board) {

        if(board.length == 0) return;

        visited = new boolean[board.length][board[0].length];
        for (int i = 0; i < board.length; i++) {
            for(int j=0; j<board[i].length; j++){
                visited[i][j] = false;
            }
        }

        //找出与边界相连的所有O,以及所有X,标记为true
        for (int i = 0; i < board.length; i++) {
            for(int j=0; j<board[i].length; j++){
                if(board[i][j]=='X') visited[i][j] = true;
                //从边界开始进行dfs
                if(board[i][j]=='O' && (i==0 || j==0 || i==board.length-1 || j==board[i].length-1)){
                    dfs(board, i, j);
                }
            }
        }
        //没有被标记的就是没有与边界相邻的O了
        for (int i = 0; i < visited.length; i++) {
            for(int j=0; j<visited[i].length; j++){
                if(!visited[i][j]){
                    board[i][j] = 'X';
                }
            }
        }

    }

    public void dfs(char[][] board, int startX, int startY){
    
        for(int i=0; i<4; i++){
            visited[startX][startY] = true;
            int newX = startX+map[i][0];
            int newY = startY+map[i][1];
            
            if(newX>0 && newY>0 && newX<board.length && newY<board[0].length && !visited[newX][newY]){
                if(board[newX][newY] == 'O'){
                    dfs(board, newX, newY);
                }
            }
        }
    }
}

51. N皇后

在这里插入图片描述
在这里插入图片描述
对于N皇后问题,n个皇后摆放在nn的棋盘格中,使得横,竖和两个对角线方向均不会同时出现两个皇后
以4皇后为例,4
4的范围,每一行都应该有一个皇后,不然其中一行没有皇后,则说明另外一行至少有两个皇后,即违背题意。
思路:
每一次在一行中摆放一个皇后,看是否能摆放,如果不能摆放则回到上一行,重新摆放上一行皇后的位置,直到在四行中每一行都摆放了皇后
在这里插入图片描述
于是问题就变成了如何快速判断皇后位置是否合法

  • 竖向:col[i] 表示第 i 列已经被占用
  • 对角线1:dia1[i] 表示对角线1中第 i 个元素被占用
  • 对角线2:dia2[i] 表示对角线2中第 i 个元素被占用

对对角线1而言
在这里插入图片描述
发现对角线上的横纵坐标相加是相同的,于是用dia1[x+y] 表示第 i 个对角线被占用很合适

对对角线2而言
在这里插入图片描述
对角线上的横纵坐标相减的值是一样的,但是由于是从-3开始的,于是使用 i-j+n-1使其从下标0开始,即 dia2[x-y+n-1]

Java代码实现如下:

class Solution {

    //存放每一行皇后摆放在第几列的位置
    //row[0] 存放 第一行中皇后所在的位置
    //row[1] 存放 第二行中皇后所在的位置
    private List<Integer> row = new ArrayList<>();
    //纵方向是否冲突
    private List<Boolean> col = new ArrayList<>();
    //两个对角线方向是否冲突
    private List<Boolean> dia1 = new ArrayList<>();
    private List<Boolean> dia2 = new ArrayList<>();

    private List<List<String>> reList = new ArrayList<>();
    //n皇后的一个解是一个字符串组成的大小为n的列表
    public List<List<String>> solveNQueens(int n) {

        for(int i=0; i<n; i++){
            col.add(false);
        }

        for(int i=0; i<2*n-1; i++){
            dia1.add(false);
            dia2.add(false);
        }

        find(n, 0);

        return reList;
    }
    //index: 第几行
    //在n皇后问题中,摆放第index行的皇后所处位置
    private void find(int n, int index){

        //index == n,说明得到了一个n皇后问题的解
        if(index == n){
            //增加一个n皇后问题的解,getOne生成一个.和Q组成的字符串列表
            reList.add(getOne(n));
            return;
        }

        for(int i=0; i<n; i++){
            //尝试将第index行的皇后摆放在第i列
            //如果在纵,以及两个对角线方向都不冲突的话,进行摆放
            if(!col.get(i) && !dia1.get(index+i) && !dia2.get(index-i+n-1) ){
                row.add(i);
                //将对应的列,及两个对角线置位true
                col.set(i, true);
                dia1.set(index+i, true);
                dia2.set(index-i+n-1, true);
                //尝试下一行
                find(n, index+1);
                //回溯
                col.set(i, false);
                dia1.set(index+i, false);
                dia2.set(index-i+n-1, false);
                row.remove(row.size()-1);
            }

        }
    }
    //将每一行转换为.,并在每一行的相应位置改为Q
    private List<String> getOne(int n){
        List<String> re = new ArrayList<>();
        //n*n的大小
        char[][] arr = new char[n][n];
        //每个位置初始化为.
        for(int i=0; i<n; i++){
            for(int j=0; j<n; j++){
                arr[i][j] = '.';
            }
        }
        //在每一行的皇后位置置为Q
        for(int i=0; i<n; i++){
            //row代表每一行中的皇后应该存在的列位置
            arr[i][row.get(i)] = 'Q';
            //每一行形成的String存入re中
            re.add(String.valueOf(arr[i]));
        }
        return re;
    }
}

37. 解数独
在这里插入图片描述
这题和N皇后问题有异曲同工之妙,数独问题的特殊点在于其33的方格,因此我们定义一个[3][3][10]的三维数组,来表示在一个33的方格中,1~9这9个数字是否被使用过 (定义为10仅仅是为了方便)

详细思路见如下Java代码:

class Solution {

    //[i][j]表示第[i]行的已经使用了数字[j]
   private boolean[][] rows = new boolean[9][10];
   private boolean[][] cols = new boolean[9][10];
   //表示在一个3*3的方块,数字[j]已经被使用过
   private boolean[][][] blocks = new boolean[3][3][10];

    public void solveSudoku(char[][] board) {

        //初始化不可使用范围
        for(int i=0; i<board.length; i++){
            for(int j=0; j<board[i].length; j++){
                //转数字
                int num = board[i][j] - '0';
                if(num<=9 && num>=1){
                    //在行上已经使用
                    rows[i][num] = true;
                    //在列上已经使用
                    cols[j][num] = true;
                    //3*3方格的对应位置上已经有了num这个数字
                    blocks[i/3][j/3][num] = true;
                }
            }
        }

        find(board, 0, 0);

    }

    private boolean find(char[][] board, int row, int col){
        //这个边界条件很重要!搜寻完一行以后row++进行下一行的搜寻,最后row到达最后一行才搜寻结束,返回true就不会进行回溯
        if(col == board[0].length){
            col=0;
            row++;
            if(row==board.length)
                return true;
        }
        //是空则尝试填充, 否则跳过,继续尝试下一个位置
        if(board[row][col] == '.'){
            for(int num=1; num<10; num++){
                //是否可以使用
                boolean used = !(rows[row][num] || cols[col][num] || blocks[row/3][col/3][num]);

                if(used){
                    rows[row][num] = true;
                    cols[col][num] = true;
                    blocks[row/3][col/3][num] = true;
                    //存入
                    board[row][col] = (char)('0'+num);
                    //完全替换完成后返回true,会一直向上返回,但不再回溯,避免回溯成原来的亚子
                    if(find(board, row, col+1)) return true;
                    //回溯
                    rows[row][num] = false;
                    cols[col][num] = false;
                    blocks[row/3][col/3][num] = false;
                
                    board[row][col] = '.';
                }
            }
        }else
            return find(board, row, col+1);

        return false;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值