-------------------------------------------------------------- 摘要 --------------------------------------------------------------
本文是以java语言和基本的数据结构的前提下,得到标准数独的解的算法,该算法比较充分的考虑了获取数独解的过程中,尽可能的去覆盖各种情形,
以及算法运行时的资源占用与性能问题。当然该算法还存在一定的优化空间,考虑到笔者的时间及精力的情况下,后续也不太可能去优化感兴趣的读者可以
尝试优化。该算法主要分为2部分,一部分:判断一个完整的数独是否是合理的数独,另一部分:获取初始数独的解(有比较完整的流程图帮助理解)。
-------------------------------------------------------------- 算法数据结构及细节 --------------------------------------------------------------
该算法前部分获取数独的基本信息特征和定义描述算法需要用到的数据结构(定义的原则是尽可能的简洁),详述如下:
1.定义3个List<List<Integer>>,集合中的元素——其实依次序对应的就是9行 9列 9个单元格,而每个元素本身也是List
其存储的是空单元格的下标。
2.定义1个List<Integer>,存储空单元格下标,按单元格候选数数量 -> 单元格的下标的优先级 正序排序
3.定义1个Map<Integer, int[]>,存储空单元格位置信息,空单元格位置以三元组的形式表示<行,列,格>,空单元格下标-<行,列,格>
4.定义List<Map<Integer, Integer>>,存储空单元格的候选数信息满足以下规则:
4.1 该集合中元素的次序与emptyCellIndexList中元素的次序一致,即emptyCellIndexList中的空单元格信息在该集合对应位置获取.
4.2 该集合中元素的数据结构为:候选数-被其他空单元格影响次数,'被其他空单元格影响次数'该量是一个动态值,若空单元格A填入试填数后,
对空单元格B中的候选数有影响,就让B对应候选数的计数+1.
5.分配2个指针,其中一个指向下一个需遍历的空单元格,另一个指向填入了正确候选数的最新空单元格
6.定义空单元格候选数指针数组,所有空单元格分配一个指针,用于指向空单元格当前填入的候选数位置
-------------------------------------------------------------- 算法实现 --------------------------------------------------------------
package algorithm; /** * @author Medusa * @date 09/30/2020 * @description 校验是否为有效数独 */ public class ValidSudoku { /** * 是否为有效数独 * @return 无效数独返回格式为:x1-x2;其中x1:l -> 行,c -> 列,g -> 格;x2:1-9数字 * 有效返回 null */ public static String algorithm(int[][] val) { String lineResult = checkLine(val); // 先校验行 if (null != lineResult) return lineResult; String columnResult = checkColumn(val); // 其次校验列 if (null != columnResult) return columnResult; return checkGrid(val); // 最后校验单元格 } private static String checkLine(int[][] val) { String result = null; for (int i = 0; i < val.length; i++) { if (isNotContainNum(val[i])) { result = "l" + (i + 1); break; } } return result; } private static String checkColumn(int[][] val) { String result = null; for (int i = 0; i < val.length; i++) { if (isNotContainNum(getColumn(val, i))) { result = "c" + (i + 1); break; } } return result; } private static String checkGrid(int[][] val) { String result = null; for (int i = 0; i < val.length; i++) { if (isNotContainNum(getGrid(val, i))) { result = "g" + (i + 1); break; } } return result; } private static int[] getColumn(int[][] val, int num) { int[] columnArr = new int[val.length]; for (int i = 0; i < columnArr.length; i++) columnArr[i] = val[i][num]; return columnArr; } private static int[] getGrid(int[][] val, int num) { int[] grid = new int[val.length]; int beginLineNum = num / 3 * 3, beginColumnNum = num % 3 * 3; for (int i = 0; i < grid.length; i++) grid[i] = val[beginLineNum + i / 3][beginColumnNum + i % 3]; return grid; } private static boolean isNotContainNum(int[] val) { boolean boo = false; loop: for (int i = 1; i < val.length + 1; i++) { for (int j = 0; j < val.length; j++) { if (i == val[j]) break; if (val.length - 1 == j) { boo = true; break loop; } } } return boo; } }
package algorithm; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * @author Medusa * @date 09/30/2020 * @description 解数独算法 */ public class Sudoku { /** * 获取数独解,如果无解返回null */ public static int[][] algorithm(int[][] val) { return traversalCell(val); } /** * 对所有单元格对应的候选数,依次遍历试错,直至得到可能存在的解为止 */ private static int[][] traversalCell(int[][] val) { // 对应行 列 格的区域内对应的空单元格下标集合 List<List<Integer>> lineEmptyCellIndexList = generateAreaInfoList(val.length); List<List<Integer>> columnEmptyCellIndexList = generateAreaInfoList(val.length); List<List<Integer>> gridEmptyCellIndexList = generateAreaInfoList(val.length); // 存储空单元格下标,按单元格的候选数数量 -> 单元格的下标的优先级,正序排序 List<Integer> emptyCellIndexList = new ArrayList<>(); // 存储空单元格位置信息,空单元格以三元组形式表示<行, 列, 格>,空单元格下标-[行, 列, 格] Map<Integer, int[]> emptyCellPositionMap = new HashMap<>(); zeroStageInit(val, lineEmptyCellIndexList, columnEmptyCellIndexList, gridEmptyCellIndexList, emptyCellIndexList, emptyCellPositionMap); if (emptyCellIndexList.size() < 1) return null == ValidSudoku.algorithm(val) ? val : null; /* 存储空单元格的候选数信息,满足以下规则: 1.该集合中元素的次序与emptyCellIndexList中元素的次序一致,即emptyCellIndexList中的空单元格信息在该集合对应位置获取. 2.该集合中元素的数据结构为:候选数-被其他空单元格影响次数,'被其他空单元格影响次数'该量是一个动态值,若空单元格A填入试填数后, 对空单元格B中的候选数有影响,就让B对应候选数的计数+1. */ List<Map<Integer, Integer>> emptyCellCandidateNumList = new ArrayList<>(); firstStageInit(val, emptyCellIndexList, emptyCellPositionMap, emptyCellCandidateNumList); // 分配2个指针,其中一个指向下一个需遍历的空单元格,另一个指向填入了正确候选数的最新空单元格 int activityPointer = 0, effectivePointer = -1; // 所有空单元格分配一个指针,用于指向空单元格当前填入的候选数位置 int[] emptyCellCandidateNumPointerArr = new int[emptyCellIndexList.size()]; for (int i = 0; i < emptyCellCandidateNumPointerArr.length; i++) emptyCellCandidateNumPointerArr[i] = -1; for (;;) { Integer emptyCellIndex = emptyCellIndexList.get(activityPointer); int[] emptyCellPositionArr = emptyCellPositionMap.get(emptyCellIndex); Map<Integer, Integer> emptyCellCandidateNumMap = emptyCellCandidateNumList.get(activityPointer); int candidateNum = moveCandidateNumPointer(activityPointer, emptyCellCandidateNumPointerArr, emptyCellCandidateNumMap); if (activityPointer - effectivePointer == 1 && nonExistentCandidateNum(emptyCellCandidateNumPointerArr[activityPointer], emptyCellCandidateNumMap)) ++effectivePointer; candidateNumCount(lineEmptyCellIndexList.get(emptyCellPositionArr[0]), columnEmptyCellIndexList.get(emptyCellPositionArr[1]), gridEmptyCellIndexList.get(emptyCellPositionArr[2]), emptyCellIndexList, emptyCellCandidateNumList, activityPointer, candidateNum); if (isUnrational(lineEmptyCellIndexList.get(emptyCellPositionArr[0]), columnEmptyCellIndexList.get(emptyCellPositionArr[1]), gridEmptyCellIndexList.get(emptyCellPositionArr[2]), emptyCellIndexList, emptyCellCandidateNumList, activityPointer)) { int backActivityPointer = backActivityPointer(emptyCellCandidateNumList, emptyCellCandidateNumPointerArr, activityPointer, effectivePointer); if (backActivityPointer == -1) { val = null; break; } int oldActivityPointer = activityPointer; activityPointer = backActivityPointer; recoveryCandidateNumState(lineEmptyCellIndexList, columnEmptyCellIndexList, gridEmptyCellIndexList, emptyCellIndexList, emptyCellPositionMap, emptyCellCandidateNumList, emptyCellCandidateNumPointerArr, activityPointer, oldActivityPointer); } else { if (activityPointer + 1 == emptyCellIndexList.size()) { fillSolution(val, emptyCellIndexList, emptyCellCandidateNumList, emptyCellPositionMap, emptyCellCandidateNumPointerArr); break; } else ++activityPointer; } } return val; } /** * 用于生成行 列 格的区域内对应的信息,如下所示 * [ * [x, x, x, ...], 第一行对应的信息,余下依序类似 * [x, x, x, ...], * ... * ] */ private static List<List<Integer>> generateAreaInfoList(int size) { List<List<Integer>> areaEmptyCellIndexList = new ArrayList<>(size); for (int i = 0; i < size; i++) areaEmptyCellIndexList.add(new ArrayList<>(size)); return areaEmptyCellIndexList; } private static void zeroStageInit(int[][] val, List<List<Integer>> lineEmptyCellIndexList, List<List<Integer>> columnEmptyCellIndexList, List<List<Integer>> gridEmptyCellIndexList, List<Integer> emptyCellIndexList, Map<Integer, int[]> emptyCellPositionMap) { for (int i = 0; i < val.length; i++) { for (int j = 0; j < val[i].length; j++) { if (val[i][j] == 0) { int emptyCellIndex = i * 9 + j; lineEmptyCellIndexList.get(i).add(emptyCellIndex); columnEmptyCellIndexList.get(j).add(emptyCellIndex); int gridIndex = i / 3 * 3 + j / 3; gridEmptyCellIndexList.get(gridIndex).add(emptyCellIndex); emptyCellIndexList.add(emptyCellIndex); emptyCellPositionMap.put(emptyCellIndex, new int[]{i, j, gridIndex}); } } } } private static void firstStageInit(int[][] val, List<Integer> emptyCellIndexList, Map<Integer, int[]> emptyCellPositionMap, List<Map<Integer, Integer>> emptyCellCandidateNumList) { List<List<Integer>> lineFillNumList = generateAreaInfoList(val.length); List<List<Integer>> columnFillNumList = generateAreaInfoList(val.length); List<List<Integer>> gridFillNumList = generateAreaInfoList(val.length); for (int i = 0; i < val.length; i++) { for (int j = 0; j < val.length; j++) { if (val[i][j] != 0) { lineFillNumList.get(i).add(val[i][j]); columnFillNumList.get(j).add(val[i][j]); gridFillNumList.get(i / 3 * 3 + j / 3).add(val[i][j]); } } } for (int i = 0; i < emptyCellIndexList.size(); i++) { int[] emptyCellPositionArr = emptyCellPositionMap.get(emptyCellIndexList.get(i)); List<Integer> fillNumGivenLineList = lineFillNumList.get(emptyCellPositionArr[0]); List<Integer> fillNumGivenColumnList = columnFillNumList.get(emptyCellPositionArr[1]); List<Integer> fillNumGivenGridList = gridFillNumList.get(emptyCellPositionArr[2]); int[] flagNumArr = new int[]{1, 1, 1, 1, 1, 1, 1, 1, 1}; for (int j = 0; j < fillNumGivenLineList.size(); j++) flagNumArr[fillNumGivenLineList.get(j) - 1]--; for (int j = 0; j < fillNumGivenColumnList.size(); j++) flagNumArr[fillNumGivenColumnList.get(j) - 1]--; for (int j = 0; j < fillNumGivenGridList.size(); j++) flagNumArr[fillNumGivenGridList.get(j) - 1]--; Map<Integer, Integer> emptyCellCandidateNumMap = new LinkedHashMap<>(); for (int j = 0; j < flagNumArr.length; j++) { if (flagNumArr[j] == 1) emptyCellCandidateNumMap.put(j + 1, 0); } emptyCellCandidateNumList.add(emptyCellCandidateNumMap); } // 第一次完成后是以单元格的候选数数量排序,但单元格的下标是无序的 int pointer = 0; for (int i = 0; i < val.length; i++) { for (int j = pointer; j < emptyCellIndexList.size(); j++) { Map<Integer, Integer> emptyCellCandidateNumMap = emptyCellCandidateNumList.get(j); if (emptyCellCandidateNumMap.size() == i + 1) { Integer tempEmptyCellIndex = emptyCellIndexList.get(pointer); emptyCellIndexList.set(pointer, emptyCellIndexList.get(j)); emptyCellIndexList.set(j, tempEmptyCellIndex); Map<Integer, Integer> tempEmptyCellCandidateNumMap = emptyCellCandidateNumList.get(pointer); emptyCellCandidateNumList.set(pointer, emptyCellCandidateNumMap); emptyCellCandidateNumList.set(j, tempEmptyCellCandidateNumMap); ++pointer; } } } // 第二次完成后单元格的下标也满足有序 List<int[]> boundarySectionList = new ArrayList<>(val.length); int start = -1, end = -1; for (int i = 0; i < val.length; i++) { for (int j = 0; j < emptyCellCandidateNumList.size(); j++) { if (emptyCellCandidateNumList.get(j).size() == i + 1) { end = j; if (start == -1) start = j; } if (emptyCellCandidateNumList.get(j).size() > i + 1) break; } boundarySectionList.add(new int[]{start, end}); start = -1; end = -1; } for (int i = 0; i < boundarySectionList.size(); i++) { int[] boundarySectionArr = boundarySectionList.get(i); for (int j = boundarySectionArr[0]; j < boundarySectionArr[1]; j++) { for (int k = boundarySectionArr[0]; k < boundarySectionArr[1] - (j - boundarySectionArr[0]); k++) { if (emptyCellIndexList.get(k) > emptyCellIndexList.get(k + 1)) { Integer tempEmptyCellIndex = emptyCellIndexList.get(k); emptyCellIndexList.set(k, emptyCellIndexList.get(k + 1)); emptyCellIndexList.set(k + 1, tempEmptyCellIndex); Map<Integer, Integer> tempEmptyCellCandidateNumMap = emptyCellCandidateNumList.get(k); emptyCellCandidateNumList.set(k, emptyCellCandidateNumList.get(k + 1)); emptyCellCandidateNumList.set(k + 1, tempEmptyCellCandidateNumMap); } } } } } /** * 向后移动空单元格的候选数指针,使之满足计数为零的最小候选数为止 * @return 需填入的候选数 */ private static int moveCandidateNumPointer(int activityPointer, int[] emptyCellCandidateNumPointerArr, Map<Integer, Integer> emptyCellCandidateNumMap) { int candidateNum = 0, ordinalNum = 0; for (Map.Entry<Integer, Integer> entry : emptyCellCandidateNumMap.entrySet()) { if (ordinalNum > emptyCellCandidateNumPointerArr[activityPointer] && entry.getValue() == 0) { emptyCellCandidateNumPointerArr[activityPointer] = ordinalNum; candidateNum = entry.getKey(); break; } ++ordinalNum; } return candidateNum; } /** * 某空单元格的候选数指针之后是否不存在计数为零的候选数 */ private static boolean nonExistentCandidateNum(int emptyCellCandidateNumPointer, Map<Integer, Integer> emptyCellCandidateNumMap) { int ordinalNum = 0; for (Map.Entry<Integer, Integer> entry : emptyCellCandidateNumMap.entrySet()) { if (ordinalNum > emptyCellCandidateNumPointer && entry.getValue() == 0) return false; ++ordinalNum; } return true; } /** * 对当前单元格所影响的其他单元格的对应候选数计数(排除已填入候选数的格) */ private static void candidateNumCount(List<Integer> emptyCellIndexByLineList, List<Integer> emptyCellIndexByColumnList, List<Integer> emptyCellIndexByGridList, List<Integer> emptyCellIndexList, List<Map<Integer, Integer>> emptyCellCandidateNumList, int activityPointer, int candidateNum) { if (activityPointer + 1 == emptyCellIndexList.size()) return; candidateNumCountByArea(emptyCellIndexByLineList, emptyCellIndexList, emptyCellCandidateNumList, activityPointer, candidateNum, 0); candidateNumCountByArea(emptyCellIndexByColumnList, emptyCellIndexList, emptyCellCandidateNumList, activityPointer, candidateNum, 0); candidateNumCountByArea(emptyCellIndexByGridList, emptyCellIndexList, emptyCellCandidateNumList, activityPointer, candidateNum, 0); } /** * @param flag 0-加1 1-减1 */ private static void candidateNumCountByArea(List<Integer> emptyCellIndexByAreaList, List<Integer> emptyCellIndexList, List<Map<Integer, Integer>> emptyCellCandidateNumList, int start, int candidateNum, int flag) { for (int i = 0; i < emptyCellIndexByAreaList.size(); i++) { int index = inSectionListIndex(emptyCellIndexByAreaList.get(i), emptyCellIndexList, start + 1); if (index != -1) { Map<Integer, Integer> emptyCellCandidateNumMap = emptyCellCandidateNumList.get(index); Integer count = emptyCellCandidateNumMap.get(candidateNum); if (null != count) { if (flag == 0) emptyCellCandidateNumMap.put(candidateNum, ++count); else emptyCellCandidateNumMap.put(candidateNum, --count); } } } } /** * 特定元素在指定集合区间里的下标,若不在指定区间返回-1 */ private static int inSectionListIndex(Integer element, List<Integer> emptyCellIndexList, int start) { for (int i = start; i < emptyCellIndexList.size(); i++) { if (element.intValue() == emptyCellIndexList.get(i).intValue()) return i; } return -1; } /** * 验证数独状态是否不合理,验证依据:特定单元格所影响任意一个单元格的候选数计数全部不为零,则为不合理 */ private static boolean isUnrational(List<Integer> emptyCellIndexByLineList, List<Integer> emptyCellIndexByColumnList, List<Integer> emptyCellIndexByGridList, List<Integer> emptyCellIndexList, List<Map<Integer, Integer>> emptyCellCandidateNumList, int activityPointer) { // 当activityPointer + 1 = emptyCellIndexList.size()时,数独状态一定是合理的,故无需验证 if (activityPointer + 1 == emptyCellIndexList.size()) return false; if (isUnrationalByArea(emptyCellIndexByLineList, emptyCellIndexList, emptyCellCandidateNumList, activityPointer)) return true; if (isUnrationalByArea(emptyCellIndexByColumnList, emptyCellIndexList, emptyCellCandidateNumList, activityPointer)) return true; return isUnrationalByArea(emptyCellIndexByGridList, emptyCellIndexList, emptyCellCandidateNumList, activityPointer); } private static boolean isUnrationalByArea(List<Integer> emptyCellIndexByAreaList, List<Integer> emptyCellIndexList, List<Map<Integer, Integer>> emptyCellCandidateNumList, int activityPointer) { for (int i = 0; i < emptyCellIndexByAreaList.size(); i++) { int index = inSectionListIndex(emptyCellIndexByAreaList.get(i), emptyCellIndexList, activityPointer + 1); if (index != -1) { Map<Integer, Integer> emptyCellCandidateNumMap = emptyCellCandidateNumList.get(index); boolean state = true; for (Map.Entry<Integer, Integer> entry : emptyCellCandidateNumMap.entrySet()) { if (entry.getValue() == 0) { state = false; break; } } if (state) return true; } } return false; } private static void fillSolution(int[][] val, List<Integer> emptyCellIndexList, List<Map<Integer, Integer>> emptyCellCandidateNumList, Map<Integer, int[]> emptyCellPositionMap, int[] emptyCellCandidateNumPointerArr) { int candidateNum = 0; for (int i = 0; i < emptyCellIndexList.size(); i++) { Map<Integer, Integer> emptyCellCandidateNumMap = emptyCellCandidateNumList.get(i); int emptyCellCandidateNumPointer = emptyCellCandidateNumPointerArr[i]; for (Map.Entry<Integer, Integer> entry : emptyCellCandidateNumMap.entrySet()) { if (emptyCellCandidateNumPointer == 0) { candidateNum = entry.getKey(); break; } --emptyCellCandidateNumPointer; } int[] emptyCellPositionArr = emptyCellPositionMap.get(emptyCellIndexList.get(i)); val[emptyCellPositionArr[0]][emptyCellPositionArr[1]] = candidateNum; } } /** * 回退activityPointer直至某单元格的候选数指针之后还有计数为零的候选数 * @return activityPointer回退至的位置,回退失败返回-1 */ private static int backActivityPointer(List<Map<Integer, Integer>> emptyCellCandidateNumList, int[] emptyCellCandidateNumPointerArr, int activityPointer, int effectivePointer) { for (int i = activityPointer; i > effectivePointer; i--) { int count = 0; for (Map.Entry<Integer, Integer> entry : emptyCellCandidateNumList.get(i).entrySet()) { if (count > emptyCellCandidateNumPointerArr[i] && entry.getValue() == 0) return i; ++count; } } return -1; } /** * 1.activityPointer + 1到oldActivityPointer的格的候选数计数需恢复activityPointer的情形 * 2.activityPointer + 1到oldActivityPointer的格的所有候选数指针可全部置为-1 */ private static void recoveryCandidateNumState(List<List<Integer>> lineEmptyCellIndexList, List<List<Integer>> columnEmptyCellIndexList, List<List<Integer>> gridEmptyCellIndexList, List<Integer> emptyCellIndexList, Map<Integer, int[]> emptyCellPositionMap, List<Map<Integer, Integer>> emptyCellCandidateNumList, int[] emptyCellCandidateNumPointerArr, int activityPointer, int oldActivityPointer) { for (int i = activityPointer; i <= oldActivityPointer; i++) { Map<Integer, Integer> emptyCellCandidateNumMap = emptyCellCandidateNumList.get(i); int emptyCellCandidateNumPointer = emptyCellCandidateNumPointerArr[i]; if (i > activityPointer) emptyCellCandidateNumPointerArr[i] = -1; int candidateNum = 0; for (Map.Entry<Integer, Integer> entry : emptyCellCandidateNumMap.entrySet()) { if (emptyCellCandidateNumPointer == 0) { candidateNum = entry.getKey(); break; } --emptyCellCandidateNumPointer; } int[] emptyCellPositionArr = emptyCellPositionMap.get(emptyCellIndexList.get(i)); candidateNumCountByArea(lineEmptyCellIndexList.get(emptyCellPositionArr[0]), emptyCellIndexList, emptyCellCandidateNumList, i, candidateNum, 1); candidateNumCountByArea(columnEmptyCellIndexList.get(emptyCellPositionArr[1]), emptyCellIndexList, emptyCellCandidateNumList, i, candidateNum, 1); candidateNumCountByArea(gridEmptyCellIndexList.get(emptyCellPositionArr[2]), emptyCellIndexList, emptyCellCandidateNumList, i, candidateNum, 1); } } }
-------------------------------------------------------------- 流程图 --------------------------------------------------------------