说明
- 先根据规则解数独,
- 规则1: 如果备选数字只有一个, 那么就填入这个数字
- 规则2: 如果在3*3单元格中, 或者一行, 或者一列中, 某个备选数字在所有的备选数字中只出现了一次, 那么就填入这个数字.
- 再暴力破解数独, 依次填入备选数字, 如果不能解开, 换下一个备选数字, 直到数独完全解开.
代码
package com.example.springboot01;import org.junit.Test;
import org.junit.platform.commons.util.StringUtils;import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;public class Test0805 {/*** 解数独* @param board 待解数独*/public void solveSudoku(char[][] board) {// 存储备选数字,全部都是null时,表示数独完全解析出来了String[][] newBoard = new String[9][9];// 统计备选数字solveSudoku1(board, newBoard);// 使用规则1和规则2解数独boolean isContinue = true;while (isContinue) {isContinue = solveSudoku2(board, newBoard) || solveSoduko3(board, newBoard);}// 暴力破解数独, 破解速度和备选数字的先后排序有很大关联int result = checkSodukoState(newBoard);if (2 == result) {solveSudoku4(0, 0, board, newBoard);}printStringArr(newBoard);}/*** 统计备选数字* @param board 待解数独* @param newBoard 剩余备选数字*/public void solveSudoku1(char[][] board, String[][] newBoard) {// 初始化备选数字for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {if ('.' == board[i][j]) {newBoard[i][j] = "123456789";}}}// 从备选数字中排除掉确定数组for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {if ('.' != board[i][j]) {cleaNum(i, j, board[i][j]+"", newBoard);}}}}/*** 使用待选数字排除法,直到待选数字只剩一个* @param board 待解数独* @param newBoard 剩余备选数字*/public boolean solveSudoku2(char[][] board, String[][] newBoard) {boolean isContinue = false;// 使用待选数字排除法,直到待选数字只剩一个for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {if ('.' == board[i][j] && newBoard[i][j].length() == 1) {char item = newBoard[i][j].charAt(0);board[i][j] = item;newBoard[i][j] = null;isContinue = true;// 清除备选数字cleaNum(i, j, item + "", newBoard);}}}return isContinue;}/*** 清除行,列,3*3单元格内的指定备选数字* @param i 行* @param j 列* @param item 待清除数字* @param newBoard 备选数字*/private void cleaNum(int i, int j, String item, String[][] newBoard) {// 清除备选数字// 3*3单元格起始坐标(左上角)int blockRow = (i / 3) * 3;int blockCol = (j / 3) * 3;for (int m = 0; m < 9; m++) {// 清除掉行里面的备选数字itemif (newBoard[i][m] != null) {newBoard[i][m] = newBoard[i][m].replace(item, "");}// 清除掉列里面的备选数字itemif (newBoard[m][j] != null) {newBoard[m][j] = newBoard[m][j].replace(item, "");}// 清除掉3*3单元格里面的备选数字itemint x = blockRow + (m/3);int y = blockCol + (m%3);if (newBoard[x][y] != null) {newBoard[x][y] = newBoard[x][y].replace(item, "");}}}/*** 一个3*3单元格,一行或者一列的所有备选数字中,如果某个数只出现了一次,那么这个数就是最终数字* @param board 待解数独* @param newBoard 剩余备选数字* @return 是否成功解开一个数字*/private boolean solveSoduko3(char[][] board, String[][] newBoard) {boolean isContinue = false;// 行for (int i = 0; i < 9; i++) {Map<Character, Integer> numToRegion = new HashMap<>();Map<Character, Integer> map = new HashMap<>();for (int j = 0; j < 9; j++) {if (newBoard[i][j] != null) {char[] chars = newBoard[i][j].toCharArray();for (char item : chars) {numToRegion.put(item, j);if (map.containsKey(item)) {map.put(item, map.get(item) + 1);} else {map.put(item, 1);}}}}for (char item : map.keySet()) {if (map.get(item) == 1) {isContinue = true;int j = numToRegion.get(item);board[i][j] = item;newBoard[i][j] = null;// 清除备选数字cleaNum(i, j, item+"", newBoard);}}}// 列for (int j = 0; j < 9; j++) {Map<Character, Integer> numToRegion = new HashMap<>();Map<Character, Integer> map = new HashMap<>();for (int i = 0; i < 9; i++) {if (newBoard[i][j] != null) {char[] chars = newBoard[i][j].toCharArray();for (char item : chars) {numToRegion.put(item, i);if (map.containsKey(item)) {map.put(item, map.get(item) + 1);} else {map.put(item, 1);}}}}for (char item : map.keySet()) {if (map.get(item) == 1) {isContinue = true;int i = numToRegion.get(item);board[i][j] = item;newBoard[i][j] = null;// 清除备选数字cleaNum(i, j, item+"", newBoard);}}}// 3*3单元格,一共9个for (int x = 0; x < 9; x++) {int row = (x/3)*3;int col = (x%3)*3;// 存储每个备选数字对应的坐标Map<Character, String> numToRegion = new HashMap<>();// 存储备选数字的个数Map<Character, Integer> map = new HashMap<>();for (int i = row; i < row+3; i++) {for (int j = col; j < col+3; j++) {if (newBoard[i][j] != null) {char[] chars = newBoard[i][j].toCharArray();for (char item : chars) {numToRegion.put(item, i + "," + j);if (map.containsKey(item)) {map.put(item, map.get(item) + 1);} else {map.put(item, 1);}}}}}for (char item : map.keySet()) {// 如果备选个数为1,那么就填入这个数字if (map.get(item) == 1) {String[] location = numToRegion.get(item).split(",");int i = Integer.parseInt(location[0]);int j = Integer.parseInt(location[1]);board[i][j] = item;newBoard[i][j] = null;isContinue = true;// 清除备选数字cleaNum(i, j, item+"", newBoard);}}}return isContinue;}/*** 暴力拆除法,依次尝试备选数字* @param board1 待解数独* @param newBoard1 剩余备选数字* @return 处理结果,返回 1 表示解数独完成,返回 2 表示没有解, 返回 3 表示备份数字错误,需要继续尝试下一个*/private int solveSudoku4(int startRow, int startCol, char[][] board1, String[][] newBoard1) {// 复制char[][] board = new char[9][9];String[][] newBoard = new String[9][9];for (int m = 0; m < 9; m++) {for (int n = 0; n < 9; n++) {board[m][n] = board1[m][n];newBoard[m][n] = newBoard1[m][n];}}// 随机选择一个备选数字for (int i = startRow; i < 9; i++) {for (int j = startCol; j < 9; j++) {if (StringUtils.isNotBlank(newBoard[i][j])) {int length = newBoard[i][j].length();for (int k = 0; k < length; k++) {char item = newBoard[i][j].charAt(k);board[i][j] = item;newBoard[i][j] = null;cleaNum(i, j, item+"", newBoard);boolean isContinue2 = true;while (isContinue2) {isContinue2 = solveSudoku2(board, newBoard) || solveSoduko3(board, newBoard);}int result = checkSodukoState(newBoard);if (1 == result) {// 复制最终结果for (int m = 0; m < 9; m++) {for (int n = 0; n < 9; n++) {board1[m][n] = board[m][n];newBoard1[m][n] = newBoard[m][n];}}return 1;} else if (2 == result) {int result2 = solveSudoku4(i, j, board, newBoard);if (1 == result2) {for (int m = 0; m < 9; m++) {for (int n = 0; n < 9; n++) {board1[m][n] = board[m][n];newBoard1[m][n] = newBoard[m][n];}}return 1;}}// 当最后一个备份数字也不符合时,需要修改前面一位备份数字if (k == length-1 && result == 3) {return 3;}// 回退到上一步,尝试下一个字符for (int m = 0; m < 9; m++) {for (int n = 0; n < 9; n++) {board[m][n] = board1[m][n];newBoard[m][n] = newBoard1[m][n];}}}}}}return 3;}/*** 判断数独状态* 1.newBoard值全为null, 表示全部解开* 2.newBoard里面有数字, 表示未完全解开* 3.newBoard里面没有数字,存在空字符串,表示数独不存在唯一解* @param newBoard 备选数字* @return 数独状态*/private int checkSodukoState(String[][] newBoard) {boolean isAllNull = true;for (int i=0; i<9; i++) {for (int j=0; j<9; j++) {if (newBoard[i][j] != null) {isAllNull = false;if ("".equals(newBoard[i][j])) {return 3;}}}}return isAllNull ? 1 : 2;}private final String[] ten = new String[]{"1","2","3","4","5","6","7","8","9"};private String getRestNum(String str) {String retStr = "";for (String item : ten) {if (!str.contains(item)) {retStr += item;}}return retStr;}private void printArr(char[][] chars) {System.out.println("===============================================================================");for (int i=0; i<9; i++) {for (int j=0; j<9; j++) {System.out.print(chars[i][j] + "\t");}System.out.println();}System.out.println("===============================================================================");}private void printStringArr(String[][] strs) {System.out.println("===============================================================================");for (int i=0; i<9; i++) {for (int j=0; j<9; j++) {System.out.print(strs[i][j] + "\t");}System.out.println();}System.out.println("===============================================================================");}@Testpublic void testLikou() {Set<Integer> temp = new HashSet<>();temp.add(1);temp.add(2);temp.remove(1);//temp.remove(2);long startTime = System.currentTimeMillis();int n=8;int k=8;if (k > n) {return;}if (k == 0) {return;}//combine4(n, k);//System.out.println(allCombineList.size());/*char[][] chars = new char[][]{{'.','.','9','7','4','8','.','.','.'},{'7','.','.','.','.','.','.','.','.'},{'.','2','.','1','.','9','.','.','.'},{'.','.','7','.','.','.','2','4','.'},{'.','6','4','.','1','.','5','9','.'},{'.','9','8','.','.','.','3','.','.'},{'.','.','.','8','.','3','.','2','.'},{'.','.','.','.','.','.','.','.','6'},{'.','.','.','2','7','5','9','.','.'}};*//*char[][] chars = new char[][]{{'5','.','2','.','6','.','.','.','.'},{'.','.','.','4','.','.','.','8','2'},{'.','.','.','.','.','2','.','.','1'},{'.','.','.','8','.','.','.','5','.'},{'.','.','3','.','.','.','6','.','.'},{'.','7','.','.','.','9','.','.','.'},{'8','.','.','2','.','.','.','.','.'},{'2','4','.','.','.','1','.','.','.'},{'.','.','.','.','3','.','2','.','7'}};*//*char[][] chars = new char[][]{{'.','.','8','.','.','.','2','.','.'},{'.','3','.','8','.','2','.','6','.'},{'7','.','.','.','9','.','.','.','5'},{'.','5','.','.','.','.','.','1','.'},{'.','.','4','.','.','.','6','.','.'},{'.','2','.','.','.','.','.','7','.'},{'4','.','.','.','8','.','.','.','6'},{'.','7','.','1','.','3','.','9','.'},{'.','.','1','.','.','.','8','.','.'}};*/// 专家级/*char[][] chars = new char[][]{{'8','.','6','.','5','.','3','.','.'},{'.','5','.','.','.','4','.','.','2'},{'4','.','.','.','.','.','.','1','.'},{'.','.','.','3','.','.','2','.','9'},{'7','.','5','.','.','.','.','.','.'},{'.','.','.','.','8','1','.','.','.'},{'.','.','8','2','.','.','9','.','.'},{'.','.','.','4','.','.','.','.','8'},{'.','2','.','.','6','3','.','.','.'},};*//*char[][] chars = new char[][]{{'6','.','.','.','.','.','5','.','.'},{'.','1','.','.','.','2','.','.','4'},{'.','.','7','.','3','.','.','6','.'},{'.','.','.','.','.','4','.','2','.'},{'.','.','.','.','6','1','.','.','.'},{'9','3','.','.','.','.','4','8','.'},{'.','8','.','7','1','.','.','9','.'},{'.','2','.','.','.','.','.','.','7'},{'7','.','.','.','5','.','.','.','.'},};*/// 世上最难数独char[][] chars = new char[][]{{'8','.','.','.','.','.','.','.','.'},{'.','.','3','6','.','.','.','.','.'},{'.','7','.','.','9','.','2','.','.'},{'.','5','.','.','.','7','.','.','.'},{'.','.','.','.','4','5','7','.','.'},{'.','.','.','1','.','.','.','3','.'},{'.','.','1','.','.','.','.','6','8'},{'.','.','8','5','.','.','.','1','.'},{'.','9','.','.','.','.','4','.','.'}};/*char[][] chars = new char[][]{{'.','.','.','.','5','.','.','3','.'},{'.','.','.','.','4','.','.','.','.'},{'.','9','.','.','.','.','.','.','2'},{'.','.','.','.','.','.','.','.','.'},{'6','.','.','.','.','.','.','4','.'},{'.','.','.','8','.','2','.','.','7'},{'5','.','.','.','.','.','.','.','.'},{'3','.','4','.','6','.','.','.','.'},{'.','.','.','7','.','.','2','.','9'}};*/solveSudoku(chars);printArr(chars);System.out.println("耗时: " + (System.currentTimeMillis() - startTime));}}
结果
- 这边用了递归, 试了几个数独, 都能在1秒之内解开, 相比较而言, 数独只有九宫格, 所以暴力算法不算太耗时间.
- 数独只知道这两个规则, 其他的没弄明白, 看百度百科上面介绍了一堆, 应该还能更快一点.
- 暴力破解的速度很大程度上受到备选数字排序的影响.
参考
力扣解数独