【算法刷题】解数独
作者:互联网
本文为个人解题思路整理,水平有限,有问题欢迎交流
概览
本题已数独问题为背景,要求计算出唯一解,表面是一个暴力深搜和回溯的问题,然而实际上如何优化才是精华所在
难度:中等
核心知识点:DFS(回溯)、状态压缩、位运算
题目来源
力扣:https://leetcode-cn.com/problems/sudoku-solver
题目内容
编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。
本题满足以下设定:
- 给定的数独序列只包含数字
1-9
和字符'.'
。 - 你可以假设给定的数独只有唯一解。
- 给定数独永远是
9x9
形式的。
样例
源数据
结果
解题思路
-
基本思路:显然可以使用递归
DFS
暴力搜索每个可能的结果- 若最终所有结果被排除则搜索失败
- 若所有空白被填充即搜索成功
-
剪枝:只需要搜索空白点就可以了,那么可以将这些空白点整理出来
-
剪枝:玩过数独的都知道先从选择少的点下手,这里同理,可以减少搜索的可能路径数量
-
优化:搜索的时候需要确定当前点可能的数字,而这些数字要求不允许与同行、同列、同块(3*3)相同,那么可以提前将每行、每列、每块已出现的数字存储起来,只需要检查某个点所在的行列块就能知道可选数字,第一想法是存在数组里,
-
优化:行列快均只允许
1-9
数字,那么可以用二进制数字表示,第i
位为1
则代表数字i
出现过,为0
则代表没出现过,注意二级制高位在右边,比如第一行的二进制是001010100
,这样处理带来下面几个好处-
每行仅需要一个10位二进制数字表示即可,最大也就2047,总共只需要三个
int[9]
即可分别存放行、列、块的状态 -
使用位运算进行数据变更或者检查都极其方便(性能消耗也小)
如某个目标状态为
states
,进行以下操作- 将第
i
位设为1
:state |= 1 << i
- 将第
i
位设为0
:state ^= 1 << i
- 检查第
i
位是否为0:(state >> i) % 2 == 0
- 将第
-
-
优化:因为只需要获得唯一解,那么放置一个全部变量用于标记是否找到答案即可。当标记为
true
的时候即结束所有搜索,不再搜索;若检查了所有可能性,该标记仍未false
则证明没有答案当然,如果答案不止一个就不能这么处理了
解题思路确定,开始整理解题方案
解题方案
-
遍历整个棋盘的每个点,以计算行、列、块的状态,并获取
- 若该点为
.
,证明为空白,将这个点存储在列表中 - 若该点不位
.
,证明为数字,将相关的行、列、块中标记已出现过这个数字
- 若该点为
-
开始深度搜索
-
获取选择可能性最小的点
position
,并计算其可能的所有数字 -
检查
position
是否存在,不存在则证明所有空点已填充,修改标记为搜索成功,并结束递归 -
用数字
i
枚举1-9
-
检查是否是否允许数字
i
,不允许则跳过 -
修改数据
-
将当前搜索的点修改为数字
i
-
修正与当前相关的行、列、块的状态
-
将当前点标记为已填充
-
-
进行下一层搜索
-
检查搜索成功的标记,若搜索成功则结束递归
-
撤回修改数据
-
将当前搜索的点修改为空白
.
-
修正与当前相关的行、列、块的状态
-
将当前点标记为未填充
-
-
-
完整代码
class DemoBasicApplicationTests {
@Test
void test() {
char[][] board = {
{'5', '3', '.', '.', '7', '.', '.', '.', '.'},
{'6', '.', '.', '1', '9', '5', '.', '.', '.'},
{'.', '9', '8', '.', '.', '.', '.', '6', '.'},
{'8', '.', '.', '.', '6', '.', '.', '.', '3'},
{'4', '.', '.', '8', '.', '.', '.', '.', '1'},
{'7', '.', '.', '.', '2', '.', '.', '.', '6'},
{'.', '6', '.', '.', '.', '.', '2', '8', '.'},
{'.', '.', '.', '4', '1', '9', '.', '.', '5'},
{'.', '.', '.', '.', '8', '.', '.', '7', '9'}
};
solveSudoku(board);
}
public int[] col = new int[9];//行
public int[] row = new int[9];//列
public int[][] block = new int[3][3];//块
List<Integer> list = new ArrayList<>();//空白位列表
boolean flag = false;//标记,用于识别搜索是否成功
/**
* 解决方案
*/
public void solveSudoku(char[][] board) {
//初始化
init(board);
//执行dfs搜索
dfs(board);
//打印结果
// System.out.println(flag);
out(board);
}
/**
* 打印board
* 调试用
*
* @param board 目标board
*/
public void out(char[][] board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
System.out.println();
}
/**
* 初始化,填充行、列、块以及空白位列表的值
*
* @param board 目标board
*/
public void init(char[][] board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
//不为空的时候,更新行、列、块的值
update(i, j, board);
} else {
//为空,则将其添加到空白位列表
list.add(i * 9 + j);
}
}
}
}
/**
* 执行搜索
* 在排除所有可能后结束搜索,flag标记搜索成功或失败
*
* @param board 目标board
*/
private void dfs(char[][] board) {
//查询到list中下一个位置
int nextPosition = getNextPosition();
//找不到下一个尝试位置,即所有点均填充完成,则结束搜索,并判断为搜索成功
if (nextPosition < 0) {
flag = true;
return;
}
//下一个位置的棋盘中的位置
int next = list.get(nextPosition);
int state = getState(next);
//从1开始检索,检索到9
int i = 0;
while (++i < 10) {
//开始尝试
if ((state >> i) % 2 == 0) {//第i位为0,则证明该位置可能为i
//更新行列
board[next / 9][next % 9] = (char) (i + '0');
list.set(nextPosition, -1);
update(next / 9, next % 9, board);
// out();
// System.out.println("" + next / 9 + " " + next % 9 + " " + i);
//开始搜索下一个位置
dfs(board);
//找到答案,结束搜索
if (flag) {
return;
}
//未找到答案,撤回修改,继续尝试
board[next / 9][next % 9] = '.';
list.set(nextPosition, next);
update(next / 9, next % 9, i, board);
}
}
}
/**
* 获取下一个位置
*
* @return 下一个位置在list中的位置,若结果为-1则证明没有需要查询的结果
*/
private int getNextPosition() {
int position = -1;
int minNum = -1;
for (int i = 0; i < list.size(); i++) {
//忽略被标记为-1的位置
if (list.get(i) >= 0) {
//找到可能性最少的位置
int possibleNum = getPossibleNum(list.get(i));
if (position < 0 || possibleNum < minNum) {
minNum = possibleNum;
position = i;
}
}
}
return position;
}
/**
* 计算可能数字的数量
*/
private int getPossibleNum(int position) {
int state = getState(position);
int num = 0;
//遍历每个二进制位,若为1则计数器加1
while (state > 0) {
num += state & 1;
state >>= 1;
}
return 9 - num;
}
/**
* 查询某个位置的状态
*/
private int getState(int position) {
int x = position / 9;
int y = position % 9;
int state = col[x] | row[y] | block[x / 3][y / 3];
return state;
}
/**
* 更新某个位置的数据
*
* @param x 横坐标
* @param y 纵坐标
* @param board 棋盘
*/
private void update(int x, int y, char[][] board) {
int num = 1 << (board[x][y] - '0');
col[x] |= num;
row[y] |= num;
block[x / 3][y / 3] |= num;
}
/**
* 更新某个位置的数据到指定数字
*
* @param x 横坐标
* @param y 纵坐标
* @param target 目标数字
* @param board 棋盘
*/
private void update(int x, int y, int target, char[][] board) {
int num = 1 << target;
col[x] ^= num;
row[y] ^= num;
block[x / 3][y / 3] ^= num;
}
}
执行结果:
性能:
记得关闭掉打印,否则会影响执行时间
后记
表面深搜,实则优化,提出解决方案并不难,提出优质的解决方案才是我们该追求的
作者:Echo_Ye
WX:Echo_YeZ
Email :echo_yezi@qq.com
个人站点:在搭了在搭了。。。(右键 - 新建文件夹)
标签:int,state,next,算法,搜索,board,position,解数,刷题 来源: https://www.cnblogs.com/silent-bug/p/13680052.html