最近偶尔有玩数独,有的题太复杂了不好解,刚好看到LeetCode上有这样的题,就尝试写了个Java的解法。

1. 数独介绍

数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。

左边是数独的题目,右边是完成后的结果。

2. 求解思路

2.1 方法定义与初始化

传入的是一个char[9][9]的数组,其中空白的部分用点号’.’代替,算出的解直接填进去即可。

public void solveSudoku(char[][] board)

我定义了一个Map<Integer, List<Character>> unsolve,它的key是当前未得到解的格子下标(从左上角开始到右下角分别是0-80),它的value是该格子可能的数值。

定义了一个初始化方法,用于初始化上面的那个map,即将每个未得到解的格子的可能数字填入1-9这9个数字。

private Map<Integer, List<Character>> initUnsolveMap(char[][] board) {Map<Integer, List<Character>> unsolve = new TreeMap<>();final List<Character> initChars = Arrays.asList('1', '2', '3', '4', '5', '6', '7', '8', '9');for (int y = 0; y < board.length; y++) {char[] chars = board[y];for (int x = 0; x < chars.length; x++) {char aChar = chars[x];if (aChar == '.') {unsolve.put(y * 9 + x, new ArrayList<>(initChars));}}}return unsolve;
}

当这个map的某个key的值只有一个元素时,那这个格子就确定了,我会将它从map中删除,并更新board。

数独最基本的解法是唯一余数法和摒除法。我采取的做法是用程序模拟人工去求解的办法。
下面开始正式求解过程。

2.1 唯一余数法

  1. 唯一余数法:用格位去找唯一可填数字,称为余数法,格位唯一可填数字称为唯余解。比如一行9个格子其中8个都填满了,那剩下的一个毫无疑问已经确定了。
  2. 我这里的做法是,依次遍历map的每个key,也就是每个未获得解的格子。
  3. 检查它所在的行、列、小方格,将不可能的数字剔除(见下面的代码)。例如所在行已经有2、5、7了,那这个格子肯定不会是这3个数字中的一个。这个步骤可以剔除不少数字。
  4. 可以看到代码中我定义了一个变量叫change,这个用于记录本轮执行过程中,map是否有发生变化。这个用途后面会说到。
  5. 对那些剔除后,只剩下一种可能数字的格子,进行移除操作,并更新board。
// 检查未解出的格子的每行、每列、每个方块,剔除已经出现的数字
for (Integer integer : unsolve.keySet()) {boolean thisChange = false;List<Character> resX = checkX(board, unsolve, integer);if (resX.size() > 0) {thisChange = true;}List<Character> resY = checkY(board, unsolve, integer);if (resY.size() > 0) {thisChange = true;}List<Character> resC = checkCube(board, unsolve, integer);if (resC.size() > 0) {thisChange = true;}if (thisChange) {change = true;}
}
// 对那些剔除后,只剩下一种可能数字的格子,进行移除操作,并更新board
List<Integer> needRemove = new ArrayList<>();
for (Integer integer : unsolve.keySet()) {List<Character> integers = unsolve.get(integer);if (integers.size() == 1) {board[integer / 9][integer % 9] = integers.get(0);needRemove.add(integer);}if (integers.size() == 0) {return FAIL;}
}
for (Integer integer : needRemove) {unsolve.remove(integer);
}

2.2 摒除法

  1. 摒除法:用数字去找单元内唯一可填空格,称为摒除法。例如9这个数字,在第一行和第二行都出现过了,那第三行的9一定不会在前2行出现过的宫格(9个格子的小方块)中,所以只剩下一个宫格的第三行会出现9,那如果那个宫格的那一行只有一个空位,那9就确定了。
  2. 与唯一余数法不同的是,这个时候可能该行、该列、该宫格都不止一个空位。这2种方法其实是使用不同的维度求解,一个从格子的维度,一个是从数字的维度,是互相补充的。一种方法无法继续求解后,可尝试第二种方法,就有可能求解了。
  3. 具体做法是,遍历1-9这9个数字,例如我们选择1这个数字进行说明
  4. 对1这个数字,遍历每一行的每个格子,如果该行没有1的话,那我们会检查剩下的格子的可能数字。
  5. 如果剩下的格子中,只有一个格子可能是1,那1就确认了。确认后进行删除map中的key并更新board。
//进行摒除法
for (char i = '1'; i <= '9'; i++) {boolean thisChange = checkNumX(board, unsolve, i);if (thisChange) change = true;
}

checkNumX方法:

private boolean checkNumX(char[][] board, Map<Integer, List<Character>> unsolve, char c) {boolean change = false;for (int y = 0; y < 9; y++) {boolean found = false;List<Integer> poss = new ArrayList<>();for (int x = 0; x < 9; x++) {if (board[y][x] == c) {found = true;break;}if (board[y][x] == '.') {int num = y * 9 + x;List<Character> characters = unsolve.get(num);if (characters.contains(c)) {poss.add(num);}}}if (!found) {if (poss.size() == 1) {int integer = poss.get(0);board[integer / 9][integer % 9] = c;unsolve.remove(integer);change = true;}}}return change;
}

2.3 递归调用

  • 当使用上述方法求解后,可能board和unsolve这个map已经发生了变化(例如某个格子已经解出来了),那这个时候,重新进行上述两种方法,将可以排除新的数字。这也是我们前2个方法中有记录change变量的用处。
  • 在下面的代码中,我们判断change是否为true,也就是本轮是否发生了变化。如果发生了变化,我们进行新一轮求解。
  • 如果没有发生变化,有3种可能:
    • 已经全部解出来了,这个时候我们标记为SUCCESS
    • 前2种方法已经无法找出解了,这个时候我标记为UNDONE
    • 求解失败,当前无解,标记为FAIL
  • 这里我们用到了一个方法isValidSudoku,用来校验当前board的有效性,比较简单,就是检查一下每行、每列、每宫格有没有重复的数字,这里就不说了。
  • 当本轮change为true时,将重新递归调用本方法进行求解,直至某一轮未改变停止,将状态归为上面3种中的一种。
if (change) {return solve(board, unsolve);
} else {if (unsolve.size() > 0) {return UNDONE;} else {boolean valid = isValidSudoku(board);if (valid) {return SUCCESS;} else {return FAIL;}}
}

2.4 假设法

  1. 经过上面的递归尝试数独的2种基本求解方法,我发现这种方式,可以应对软件中的难度为简单和中等的数独题目,但是针对难度为困难的题目,就会卡住无法继续求解了(也就是状态为UNDONE)。
  2. 数独还有一些其他的复杂解法,例如区块摒除法)、数组、二链列、唯一矩形、全双值格致死解法、同数链、异数链及其他数链的高级技巧等等,这些解法代码层面实现起来较为复杂,故放弃。
  3. 这里我选择了比较适合代码编写的方式,也就是假设法,或者叫暴力求解法。也就是在上述方法得到的结果的基础上,找出那些可能数字比较少的一个格子。
  4. 针对这个格子,我分别设它为可能的每一个数字,并各自启动一个新的实例去求解。例如某个格子可能为3、5这2个数字,那我创建2个新的board,分别填入3、5这2个数字,并重新调用上面的求解办法。
  5. 按上面的方式继续求解,那会得到一个结果列表,里面可能有的是UNDONE,有的是FAIL和有的是SUCCESS,如果有SUCCESS,那我们的求解过程就结束了,将成功的board返回即可。而其中如果出现了FAIL,说明这个实例不可能存在,那就排除掉。
  6. 比较复杂的是出现UNDONE了,说明这一轮的假设仍然求不出解。那我们可以再继续假设。例如第一轮假设我们假设了3、5。然后返回了2个UNDONE,那我们还要继续在3的结果里面,继续假设,例如还是有2个可能性。这样我们可能就产生了4个假设的结果,再进行上面的判断。
  7. 说起来可能蛮复杂,但是代码实现起来挺简单的,也就是再次应用了递归的方法。如下所示:
private int recursion(char[][] rawBoard, Map<Integer, List<Character>> unsolve) {int status = solve(rawBoard, unsolve);//进行常规的求解过程,并获得当前求解的状态if (status == UNDONE) {//只有UNDONE的状态需要继续假设,SUCCESS和FAIL都不需要List<char[][]> boards = findPossBoard(rawBoard, unsolve);//根据当前结果进行假设,获得假设的board列表for (char[][] board : boards) {int newStatus = recursion(board, initUnsolveMap(board));//递归调用本方法,展开新的求解与假设过程if (newStatus == SUCCESS) {for (int i = 0; i < 9; i++) {System.arraycopy(board[i], 0, rawBoard[i], 0, 9);//成功时,将成功的board赋值给原始的board数组。如果这不是最外层的递归,那将一层一层往上传递,直至传给最原始的board数组。}return SUCCESS;}}return FAIL;} else {return status;}
}

3. 总结

经过上面的4种方法共同求解,目前只要有解的数独都可以解出来。完整的代码见我的github:https://github.com/lnho/LeetCode_Java/blob/master/src/main/java/com/lnho/leetcode/solution/Solution037Simple.java ,里面还有一些测试的用例。

数独的Java版解法相关推荐

  1. 数独游戏java版(一)--核心算法

    之前学习javascript时用javascript写过一个数独游戏,最近看了一点java的内容,于是就心血来潮想搞一个java版的数独游戏. 现在将全部代码分享出来和大家学习交流,当然代码中有着各种 ...

  2. 程序员的算法趣题:Q13 有多少种满足字母算式的解法(Java版)

    题目说明 所谓字母算式,就是用字母表示的算式, 规则是相同字母对应相同数字,不同字母对应不同数字, 并且第一位字母的对应数字不能是 0. 譬如给定算式 We * love = CodeIQ,则可以对应 ...

  3. java 数独算法_java版数独游戏核心算法(一)

    之前学习javascript时用javascript写过一个数独游戏,最近看了一点java的内容,于是就心血来潮想搞一个java版的数独游戏. 现在将全部代码分享出来和大家学习交流,当然代码中有着各种 ...

  4. 2021年 第12届 蓝桥杯 第4次模拟赛真题详解及小结【Java版】

    蓝桥杯 Java B组 省赛决赛 真题详解及小结汇总[2013年(第4届)~2021年(第12届)] 第11届 蓝桥杯-第1.2次模拟(软件类)真题-(2020年3月.4月)-官方讲解视频 说明:大部 ...

  5. 2021年 第12届 蓝桥杯 第3次模拟赛真题详解及小结【Java版】

    蓝桥杯 Java B组 省赛决赛 真题详解及小结汇总[2013年(第4届)~2021年(第12届)] 第11届 蓝桥杯-第1.2次模拟(软件类)真题-(2020年3月.4月)-官方讲解视频 说明:大部 ...

  6. 2020年 第11届 蓝桥杯 C/C++ B组 省赛真题详解及小结【第1场省赛2020.7.5】【Java版】

    蓝桥杯 Java B组 省赛真题详解及小结汇总[2013年(第4届)~2020年(第11届)] 注意:部分代码及程序 源自 蓝桥杯 官网视频(历年真题解析) 郑未老师. 2013年 第04届 蓝桥杯 ...

  7. 2020年 第11届 蓝桥杯 第2次模拟赛真题详解及小结【Java版】

    蓝桥杯 Java B组 省赛真题详解及小结汇总[2013年(第4届)~2020年(第11届)] 注意:部分代码及程序 源自 蓝桥杯 官网视频(历年真题解析) 郑未老师. 2013年 第04届 蓝桥杯 ...

  8. leetcode 235. 二叉搜索树的最近公共祖先(Java版,树形dp套路)

    题目 原题地址:leetcode 235. 二叉搜索树的最近公共祖先 说明: 所有节点的值都是唯一的. p.q 为不同节点且均存在于给定的二叉搜索树中. 题解 关于 树形dp 套路,可以参考我的另一篇 ...

  9. 常见数据结构和算法实现(排序/查找/数组/链表/栈/队列/树/递归/海量数据处理/图/位图/Java版数据结构)

    常见数据结构和算法实现(排序/查找/数组/链表/栈/队列/树/递归/海量数据处理/图/位图/Java版数据结构) 数据结构和算法作为程序员的基本功,一定得稳扎稳打的学习,我们常见的框架底层就是各类数据 ...

最新文章

  1. RedHat 5.4 RHCE DHCP学习笔记
  2. linux 批量kill java进程
  3. mysql命令分类(DML、DDL、DCL)
  4. 关于真正免费的嵌入式GUI
  5. [SNOI2017]遗失的答案 (FWT)
  6. java -jar 内存溢出_JAVA系统启动栈内存溢出-StackOverflowError
  7. 为什么不能使用 BigDecimal 的 equals 方法做等值比较
  8. Mysql删除重复数据并解决You can't specify target table 'xx' for update in FROM clause 报错与 query interrupted报错
  9. 力扣题目——143. 重排链表
  10. matlab第四章答案,matlab第四章课后
  11. adb shell /system/bin/screencap screenrecord
  12. 2003年高考语文全国最高分_2003年参加高考的同学们?你们考了多少分啊?再议2003年高考数学...
  13. 【那些年踩过的坑】服务器配环境:Ubuntu 16.04 + Titan Xp + CUDA 9.0 + cuDNN 7.1 + Tensorflow + Pytorch + MXNet
  14. 大文件传输软件的优势你了解吗?
  15. Java工程师考试题
  16. 如何通过QA质量管理提高软件质量?
  17. AcWing蓝桥杯AB组辅导课07、贪心
  18. 2013年3月TIOBE编程语言排行榜,Ruby超越Perl
  19. Go语言环境安装与试运行
  20. 【C#基础】数据结构

热门文章

  1. 前端技术 | dva,美貌与智慧并存
  2. 低频理疗按摩仪8种常用基本波形
  3. 云班课python答案_云班课测试题答案
  4. 看日漫学日语:日漫里常看到的日语100句(建议收藏)
  5. 使用scp传输文件给linux服务器,出现Permission denied(publickey) 的解决办法
  6. 射击类项目(数据的持久化保存)整理四
  7. cad渐开线齿轮轮廓绘制_CAD渐开线齿形怎么绘制
  8. OpenCV入门到进阶:实战三大典型项目(更新至12) IT自学视频教程
  9. Android开发实例-Android平台手机新闻客户端
  10. SourceTree 对比工具配置