37. 解数独

描述

编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法需遵循如下规则:

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
原题地址

胡思乱想

回溯肯定要的,但具体怎么写不知道;答案肯定是要看的,借此文章来加深印象。

思路解法:(回溯法)

  1. 约束编程:
    基本的意思是在放置每个数字时都设置约束。在数独上放置一个数字后立即
    排除当前行,列和子方块对该数字的使用
    。这会传播约束条件并有利于减少需要考虑组合的个数。
  2. 回溯:
    让我们想象一下已经成功放置了几个数字。但是该组合不是最优的并且不能继续放置数字了。该怎么办?回溯。意思是回退,来改变之前放置的数字并且继续尝试。如果还是不行,再次回溯。

生产代码:

对于代码的编写,我们有以下几点需要注意:

  1. 如何设置约束,使用何种数据结构来保证能够迅速的判断约束情况;
  2. 回溯函数的编写,其具体思路为何;
  3. 如何计算方块索引,即根据当前列行的数值获取当前所在方块索引的数值;

我们分别解决上述问题:

  1. 我们假设在一个空位置上放上数d, 那么我们要判断三个约束,即在当前行是否出现该数,当前列是否出现该数,当前方块是否出现该数。能够立刻想到的是在每次判断的时候都扫描当前行列或方块,是否出现该数,然而这是一种低效的方法,因为很多行、列或者方块会被重复扫描,造成了时间上的浪费。我们使用初始化的技巧,即在一开始将数独扫描一遍,将结果保存,之后每添加一个数或者删除一个数,我们维护这个结果。 可以使用二维数组来达成这样的结果。设置3个二维数组,类型为boolean,其中数组的第一个下标表示当前的位置(一个数组表示行,一个表示列,一个表示方块),第二个下标则表示1~9的数,数组的值表示当前位置有无该数。例如rows[2][8] == true表示在数独的第2行中存在数字8。
  2. 约束的问题解决了,下面是回溯函数的问题,这涉及到整个方法的核心。
    假定定义函数为void backtrack(int row, int col),进行过一次该函数之后,在第row行第col列的位置上会有一个正确的数出现。函数的思路如下:
    若该位置为空,从1到9迭代数字d,若某个d不存在约束,将该数放入该位置,并寻找下一个位置;若放置的该数不能导致数独的解决,将此d移除,并尝试下一个d。
    若该位置不为空,则直接寻找一个位置。
  3. 方块索引值 = (行 / 3) * 3 + 列 / 3 (经验获得)

实际上将代码分成以下步骤来编写思路会很清晰:

  1. 定义约束数组,数独面板二维数组,结束终止标记;
  2. 编写backtrack函数,先不实现其中要用的部分功能函数;
  3. 将backtrack中所需要的部分功能函数完成;
  4. 完成初始化约束数组函数;

具体代码:

class Solution {
    //定义数独版
    char[][] board = new char[9][9];
    //定义约束数组
    boolean [][] rows = new boolean[9][10];
    boolean [][] cols = new boolean[9][10];
    boolean [][] boxs = new boolean[9][10];
    //定义是否数独是否完成
    boolean isFinished = false;

    public void initControls(char [][] board){//初始化约束数组
        this.board = board;
        for(int i=0; i<9; i++){
            for(int j=0; j<9; j++){
                if(this.board[i][j]!='.'){
                    placeNumber(this.board[i][j]-'0',i,j);
                }
            }
        }
    }

    public boolean couldPlaceNumber(int d, int row, int col){//判断当前位置能否放入该数字
        int boxId = (row/3)*3+col/3;
        return !rows[row][d]&&!cols[col][d]&&!boxs[boxId][d];
    }

    public void placeNumber(int d, int row, int col){//放入该数字,同时维护约束数组
        int boxId = (row/3)*3+col/3;
        rows[row][d] = true;
        cols[col][d] = true;
        boxs[boxId][d] = true;
        board[row][col] =  (char)(d + '0');
    }

    public void removeNumber(int d, int row, int col){//移除该数字,同时维护约束数组
        int boxId = (row/3)*3+col/3;
        rows[row][d] = false;
        cols[col][d] = false;
        boxs[boxId][d] = false;
        board[row][col] = '.';
    }

    public void nextPlaceNumber(int row, int col){//寻找下一个位置,同时判断是否完成数独
        if(row==8&&col==8)  isFinished = true;
        else{
            if(col+1<9) backtrack(row,col+1);
            else   backtrack(row+1,0);
        }
    }

    public void backtrack(int row, int col){//核心思路,回溯法
        if(board[row][col]=='.'){
            for(int i=1; i<=9; i++){
                if(couldPlaceNumber(i,row,col)){	//如果当前位置能够放入该数字
                    placeNumber(i,row,col);	//放入该数字
                    nextPlaceNumber(row,col);	//移动到下一个可以到达的位置
                    if(!isFinished) removeNumber(i,row,col);	//若该位置不能够完成数独,移除该数字
                }
            }
        }else{
            nextPlaceNumber(row,col);	//若当前位置不为空,转移到下一个位置
        }
    }

    public void solveSudoku(char[][] board){
        initControls(board); //初始化约束数组
        backtrack(0,0);
    }
}

心得:

没玩过数独,只知道其中的概念。在考虑很难找到一个通用公式的情况下,应该能用的方法只有回溯了。官方题解的代码非常漂亮,无论是思路还是函数的封装都堪称完美,要学习的远不止其中的核心思路,还有其代码风格。只有把每个函数要做的事情清清楚楚地实现,不多不少,思路清晰,那么就算是递归,也没有什么好怕的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值