1. 背景
中午下楼去吃饭,电梯里看到有人在玩数独,之前也玩过,不过没有用程序去解过,萌生了一个想法,这两天就一直想怎么用程序去解一个数独。要去解开一个数独,首先要先了解数独的游戏规则,这样才能找到对应的算法去解开。以下是本人用Java语言对数独进行的解析,代码只是抛砖引玉,欢迎大神们给指点指点。
2. 数独知识
![](https://images2018.cnblogs.com/blog/1312542/201808/1312542-20180806142621795-1075559394.png)
三行与三列相交之处有九格,每一单元称为小九宫(Box、Block),简称宫,如图四所示
更多关于数独的知识可以查看百度百科。
3. 生成随机数独
在解开一个数独之前,首先要知道数独是怎么生成的,接下来先随机生成一个9*9的数独。
生成思路:使用嵌套for循环,给每个格子填数,这个格子中的数必是1-9中的某一个数字,在填第n个格子时,要排除行、列、宫中已经存在的数字,在剩下的数字中随机选一个,如果排除掉行、列、宫中的数字后,已经没有可选数字了,说明这个数独生成错了,while循环重新开始生成,直到生成一个可用的数独。这个地方用到了Set集合及集合中的方法,以下是生成数独的代码。
package com.woasis.demo; import java.util.*; /** * 数独 * 1 3 3 4 5 6 7 8 9 * 1. [1, 2, 3, 4, 5, 6, 7, 8, 9] * 2. [1, 2, 3, 4, 5, 6, 7, 8, 9] * 3. [1, 2, 3, 4, 5, 6, 7, 8, 9] * 4. [1, 2, 3, 4, 5, 6, 7, 8, 9] * 5. [1, 2, 3, 4, 5, 6, 7, 8, 9] * 6. [1, 2, 3, 4, 5, 6, 7, 8, 9] * 7. [1, 2, 3, 4, 5, 6, 7, 8, 9] * 8. [1, 2, 3, 4, 5, 6, 7, 8, 9] * 9. [1, 2, 3, 4, 5, 6, 7, 8, 9] * */ public class Sudoku { public static void main(String[] args) { boolean flag = true; while (flag) { try { start(); flag = false; } catch (ArithmeticException e) { System.out.println(e); } } } /** * 开始生成数独 */ private static void start(){ int[][] source = new int[9][9]; //第i行 for (int i=0; i<9; i++){ // 第i行中的第j个数字 for (int j=0; j<9; j++){ //第i行目前的数组 int[] row = Arrays.copyOf(source[i], j); int[] column = new int[i]; for (int k=0; k<i; k++){ column[k] = source[k][j]; } //所在宫 List<Integer> palaceList = new ArrayList<>(); //取整,获取宫所在数据 int palaceRow = i/3; int palaceColumn = j/3; for (int m=0; m<3; m++){ for (int n=0; n<3; n++){ palaceList.add(source[palaceRow*3+m][palaceColumn*3+n]); } } source[i][j] = getNumber(row, column, palaceList.stream().mapToInt(Integer::intValue).toArray());; } } //打印随机生成的数独数组 for (int i=0; i<source.length; i++){ System.out.println(Arrays.toString(source[i])); } } /** * 从即没有在行也没有在列中,选出一个随机数 * @param row * @param column * @return */ private static int getNumber(int[] row, int[] column, int[] palace ){ //数组合并,并去重,使用Set集合 Set<Integer> mergeSet = new HashSet<>(); for (int i=0; i<row.length; i++){ mergeSet.add(row[i]); } for (int j=0; j<column.length; j++){ mergeSet.add(column[j]); } for (int k=0; k<palace.length; k++){ mergeSet.add(palace[k]); } Set<Integer> source = new HashSet<>(); for (int m=1; m<10; m++){ source.add(m); } //取差集 source.removeAll(mergeSet); int[] merge = source.stream().mapToInt(Integer::intValue).toArray(); //随机返回一个下标 return merge[getRandomCursor(merge.length)]; } /** * 获取一个随机下标 * @param length * @return */ public static int getRandomCursor(int length) { return Math.abs(new Random().nextInt())%length; } }
如下图是代码执行后生成的随机数独,行、列、宫中都是1-9个数字,没有重复。
4. 数独的解析
数独已经可以生成了,现在就对数独进行解析,首先声明一下,接下来的方法可能对一些数独是解不开的,解开数独不是唯一目的,而是在解析数独中对一些Java知识进行回顾和学习。采用的是隐形唯一候选数法,什么是唯一候选数法呢,就是某个数字在某一行列宫格的候选数中只出现一次,就是这个格子只有一个数可选了,那这个格子里就只能填这个数,这就是唯一候选数法,其实也是排除法。参照的这篇文章进行的一次数独解析,数独解题方法大全,可以参考学习一下。
解题思路:
- 要解析的数独,与数独对应的隐形数组;
- 排除掉隐形数组中的数字,哪些数字需要排除呢,就是数独中已有的数字,要排除该数字所在的行、列、宫。例如,如下图R4C4格是2,则R4行、C4列以及2所在的宫除了R4C4格子之外,其余的候选数中都不能有2这个数字了。
3. 排除一次完成后,看剩下的隐形数组中有没有剩下的单个数,如果有则剩下的这个候选数字就是该位置所要填的数字,有的话需要递归一次2步骤;查看行中有没有唯一的单数,如果有递归一次2步骤;查看列中有没有唯一的单数,如果有递归一次2步骤。
4. 排除以部门隐形数字之后,有一些数字是不好排除的,就是一些对数,对数就是在一个宫两个格子,候选数字都是AB,要么这个格子是A要么另一个格子是B。到这个地方之后不好排除,只能用试探法,假如一个格子是A,那么另一个格子是B,这样去试探,如果试探一次后发现试探的对的,那么就确认这种试探是可行的,如果不对,则数值对换。
5. 步骤4试探对之后,再从步骤2进行递归,直到获得最终解。
以下是完整代码:
其中demo中解析的数独就是数独解题方法大全中隐形唯一候选数法中的一个例子。
1 package com.woasis.demo; 2 3 import java.util.*; 4 5 public class SudokuCrack { 6 public static void main(String[] args) { 7 //生成候选数字表,9行9列,每个格子有9个数字 8 int[][][] candi = new int[9][9][9]; 9 //初始化候选数字表 10 for (int i=0; i<9; i++){ 11 for (int j=0; j<9; j++){ 12 candi[i][j] = new int[]{1,2,3,4,5,6,7,8,9};; 13 } 14 } 15 int[][] sudo = { 16 {0,0,9,6,0,0,0,3,0}, 17 {0,0,1,7,0,0,0,4,0}, 18 {7,0,0,0,9,0,0,8,0}, 19 {0,7,0,0,8,0,5,0,0}, 20 {1,0,0,0,4,0,0,2,0}, 21 {0,2,0,0,1,0,9,0,0}, 22 {5,0,0,0,0,9,0,0,0}, 23 {6,0,0,0,0,3,0,0,2}, 24 {4,0,0,0,0,0,0,0,1} 25 }; 26 27 if (isOkSudo(candi, sudo)){ 28 System.out.println("校验是不是一个合法数独:是"); 29 }else { 30 System.out.println("校验是不是一个合法数独:不是"); 31 return; 32 } 33 34 crack(candi, sudo); 35 36 //获取隐形数组中两个相等的数 37 List<CandiInfo> equalCandi = getEqualCandi(candi,sudo); 38 39 //获取其中一个进行试探。 40 for (CandiInfo info : equalCandi){ 41 42 //获取坐标 43 String[] location = info.location.split("\\|"); 44 String[] ALocation = location[0].split("-"); 45 int aRow = Integer.parseInt(ALocation[0]); 46 int aColumn = Integer.parseInt(ALocation[1]); 47 String[] BLocation = location[1].split("-"); 48 int bRow = Integer.parseInt(BLocation[0]); 49 int bColumn = Integer.parseInt(BLocation[1]); 50 //获取数据 51 int[] data = info.nums.stream().mapToInt(Integer::intValue).toArray(); 52 53 System.out.println("开始进行试探:data="+data[0]+", "+data[1]+" 位置:"+aRow+"-"+aColumn+", "+bRow+"-"+bColumn); 54 55 if(isRight(candi, sudo,aRow, aColumn, bRow, bColumn, data[0], data[1])){ 56 modifySudoAndCandi(candi, sudo, aRow, aColumn, data[0]); 57 modifySudoAndCandi(candi, sudo, bRow, bColumn, data[1]); 58 }else{ 59 modifySudoAndCandi(candi, sudo, aRow, aColumn, data[1]); 60 modifySudoAndCandi(candi, sudo, bRow, bColumn, data[0]); 61 } 62 crack(candi, sudo); 63 } 64 65 66 System.out.println("解析完成:"); 67 for (int i=0; i<9; i++){ 68 System.out.println(Arrays.toString(sudo[i])); 69 } 70 } 71 72 /** 73 * 试探这样的组合是否正确 74 * @param candi 75 * @param sudo 76 * @param aRow 77 * @param aColumn 78 * @param bRow 79 * @param bColumn 80 * @param data0 81 * @param data1 82 * @return 83 */ 84 private static boolean isRight(int[][][] candi, int[][] sudo, int aRow, int aColumn, int bRow, int bColumn, int data0, int data1){ 85 int[][][] deepCandiCopy = new int[9][9][9]; 86 for (int i=0; i<9; i++){ 87 deepCandiCopy[i] = candi[i].clone(); 88 } 89 int[][] deepSudoCopy = new int[9][9]; 90 for (int i=0; i<9; i++){ 91 deepSudoCopy[i]= sudo[i].clone(); 92 } 93 modifySudoAndCandi(deepCandiCopy, deepSudoCopy, aRow, aColumn, data0); 94 modifySudoAndCandi(deepCandiCopy, deepSudoCopy, bRow, bColumn, data1); 95 96 crack(deepCandiCopy, deepSudoCopy); 97 98 return isOkSudo(deepCandiCopy,deepSudoCopy); 99 } 100 101 /** 102 * 隐藏数法解析数独 103 * @param candi 隐藏数数组 104 * @param sudo 要解的数独 105 */ 106 private static void crack(int[][][] candi, int[][] sudo){ 107 108 eliminateCandidateNumbers(candi, sudo); 109 110 //一轮结束后,查看隐形数组里有没有单个的,如果有继续递归一次 111 boolean flag = false; 112 for (int k=0; k<9; k++){ 113 for (int q=0; q<9; q++){ 114 int f = sudo[k][q]; 115 if (f == 0){ 116 int[] tmp = candi[k][q]; 117 Set<Integer> s = new HashSet<>(); 118 for (int t=0; t<tmp.length; t++){ 119 if (tmp[t]>0){ 120 s.add(tmp[t]); 121 } 122 } 123 //说明有单一成数据可以用的 124 if (s.size() == 1){ 125 flag = true; 126 modifySudoAndCandi(candi, sudo, k, q, s.stream().mapToInt(Integer::intValue).toArray()[0]); 127 } 128 } 129 } 130 } 131 //如果有确定的单个数,进行递归一次 132 if (flag){ 133 crack(candi, sudo); 134 } 135 //查看行有没有唯一数字,有就递归一次 136 flag = checkRow(candi, sudo); 137 if (flag){ 138 crack(candi, sudo); 139 } 140 //查看列有没有唯一数字,有就递归一次 141 flag = checkColumn(candi, sudo); 142 if (flag){ 143 crack(candi, sudo); 144 } 145 } 146 147 /** 148 * 剔除数组中的候选数字,剔除行、列、宫 149 * @param candi 150 * @param sudo 151 */ 152 private static void eliminateCandidateNumbers(int[][][] candi, int[][] sudo){ 153 for (int i=0; i<9; i++){ 154 for (int j=0; j<9; j++){ 155 int num = sudo[i][j]; 156 //剔除备选区数字 157 if (num>0){ 158 candi[i][j] = new int[]{0,0,0,0,0,0,0,0,0}; 159 for (int m=0; m<9; m++){ 160 int[] r = candi[i][m]; 161 r[num-1] = 0; 162 int[] c = candi[m][j]; 163 c[num-1] = 0; 164 } 165 //摒除宫里的唯一性 166 //取整,获取宫所在数据 167 int palaceRow = i/3; 168 int palaceColumn = j/3; 169 for (int m=0; m<3; m++){ 170 for (int n=0; n<3; n++){ 171 int[] p = candi[palaceRow*3+m][palaceColumn*3+n]; 172 p[num-1] = 0; 173 } 174 } 175 } 176 } 177 } 178 } 179 180 /** 181 * 修改数独的值并剔除隐形数字 182 * @param candi 183 * @param sudo 184 * @param row 185 * @param column 186 * @param v 187 */ 188 private static void modifySudoAndCandi(int[][][] candi, int[][] sudo, int row, int column, int v){ 189 //修改数独的值 190 sudo[row][column] = v; 191 192 //剔除备选区数字 193 candi[row][column] = new int[]{0,0,0,0,0,0,0,0,0}; 194 for (int m=0; m<9; m++){ 195 int[] r = candi[row][m]; 196 r[v-1] = 0; 197 int[] c = candi[m][column]; 198 c[v-1] = 0; 199 } 200 //摒除宫里的唯一性 201 //取整,获取宫所在数据 202 int palaceRow = row/3; 203 int palaceColumn = column/3; 204 for (int m=0; m<3; m++){ 205 for (int n=0; n<3; n++){ 206 int[] p = candi[palaceRow*3+m][palaceColumn*3+n]; 207 p[v-1] = 0; 208 } 209 } 210 } 211 212 /** 213 * 查看行中的隐形数组有没有唯一存在的候选值 214 * @param candi 215 * @param sudo 216 * @return 217 */ 218 private static boolean checkRow(int[][][] candi, int[][] sudo){ 219 boolean flag = false; 220 for (int i=0; i<9; i++){ 221 Map<String ,Set<Integer>> candiMap = new HashMap<>(); 222 int[] row = sudo[i]; 223 for (int j=0; j<9; j++){ 224 if (row[j]==0){ 225 int[] tmp = candi[i][j]; 226 Set<Integer> set = new HashSet<>(); 227 for (int k=0; k<tmp.length; k++){ 228 if (tmp[k]>0) { 229 set.add(tmp[k]); 230 } 231 } 232 candiMap.put(String.valueOf(i)+"-"+String.valueOf(j), set); 233 } 234 } 235 if (candiMap.size()>0) { 236 Set<String> keys = candiMap.keySet(); 237 Iterator iterator = keys.iterator(); 238 while (iterator.hasNext()){ 239 String tKey = (String) iterator.next(); 240 //要查看的集合 241 Set<Integer> set = deepCopySet(candiMap.get(tKey)); 242 //深复制 243 Set<String> tmpKeys = candiMap.keySet(); 244 Iterator tmpKeyIterator =tmpKeys.iterator(); 245 while (tmpKeyIterator.hasNext()){ 246 String tmpKey = (String) tmpKeyIterator.next(); 247 //取交集 248 if (!tKey.equals(tmpKey)) { 249 set.removeAll(candiMap.get(tmpKey)); 250 } 251 } 252 //交集取完,集合空了,看下一个结合有没有 253 if (set.size() == 0){ 254 continue; 255 }else { 256 //还剩一个唯一值 257 if (set.size() == 1){ 258 String[] ks = tKey.split("-"); 259 flag = true; 260 modifySudoAndCandi(candi, sudo, Integer.parseInt(ks[0]),Integer.parseInt(ks[1]), set.stream().mapToInt(Integer::intValue).toArray()[0] ); 261 } 262 } 263 } 264 } 265 } 266 return flag; 267 } 268 269 /** 270 * 查看列中的隐形数组有没有唯一存在的候选值 271 * @param candi 272 * @param sudo 273 * @return 274 */ 275 private static boolean checkColumn(int[][][] candi, int[][] sudo){ 276 boolean flag = false; 277 for (int i=0; i<9; i++){ 278 Map<String ,Set<Integer>> candiMap = new HashMap<>(); 279 for (int j=0; j<9; j++){ 280 if (sudo[j][i]==0){ 281 int[] tmp = candi[j][i]; 282 Set<Integer> set = new HashSet<>(); 283 for (int k=0; k<tmp.length; k++){ 284 if (tmp[k]>0) { 285 set.add(tmp[k]); 286 } 287 } 288 candiMap.put(String.valueOf(i)+"-"+String.valueOf(j), set); 289 } 290 } 291 if (candiMap.size()>0) { 292 Set<String> keys = candiMap.keySet(); 293 Iterator iterator = keys.iterator(); 294 while (iterator.hasNext()){ 295 String tKey = (String) iterator.next(); 296 //要查看的集合 297 Set<Integer> set = deepCopySet(candiMap.get(tKey)); 298 //深复制 299 Set<String> tmpKeys = candiMap.keySet(); 300 Iterator tmpKeyIterator =tmpKeys.iterator(); 301 while (tmpKeyIterator.hasNext()){ 302 String tmpKey = (String) tmpKeyIterator.next(); 303 //取交集 304 if (!tKey.equals(tmpKey)) { 305 set.removeAll(candiMap.get(tmpKey)); 306 } 307 } 308 //交集取完,集合空了,看下一个结合有没有 309 if (set.size() == 0){ 310 continue; 311 }else { 312 //还剩一个唯一值 313 if (set.size() == 1){ 314 String[] ks = tKey.split("-"); 315 flag = true; 316 modifySudoAndCandi(candi,sudo, Integer.parseInt(ks[1]),Integer.parseInt(ks[0]),set.stream().mapToInt(Integer::intValue).toArray()[0]); 317 } 318 } 319 } 320 } 321 } 322 return flag; 323 } 324 325 /** 326 * 获取隐形数字中宫中两个相等的数字 327 * @return 328 */ 329 private static List<CandiInfo> getEqualCandi(int[][][] candi, int[][] sudo){ 330 //找到两个相等数字 331 //遍历宫 332 List<CandiInfo> maps = new ArrayList<>(); 333 for (int m=0; m<3; m++){ 334 for (int n=0; n<3; n++){ 335 Map<String, Set<Integer>> palaceMap = new HashMap<>(); 336 for (int i=0; i<3; i++){ 337 for (int j=0; j<3; j++){ 338 int sudoRow = m*3 + i; 339 int sudoColumn = n*3 +j; 340 if (sudo[sudoRow][sudoColumn] == 0) { 341 int[] tmpX = candi[sudoRow][sudoColumn]; 342 Set<Integer> set = new HashSet<>(); 343 for (int k=0; k<tmpX.length; k++){ 344 if (tmpX[k]>0) { 345 set.add(tmpX[k]); 346 } 347 } 348 if (set.size() == 2) { 349 palaceMap.put(String.valueOf(sudoRow) + "-" + String.valueOf(sudoColumn), set); 350 } 351 } 352 } 353 } 354 355 Set<String> pSet = palaceMap.keySet(); 356 Iterator pIterator = pSet.iterator(); 357 while (pIterator.hasNext()){ 358 String key = (String) pIterator.next(); 359 Iterator tmpIterator = pSet.iterator(); 360 while (tmpIterator.hasNext()){ 361 String tmpKey = (String) tmpIterator.next(); 362 if (!key.equals(tmpKey)){ 363 Set<Integer> tmpIntSet = palaceMap.get(tmpKey); 364 Set<Integer> palaceIntSet = deepCopySet(palaceMap.get(key)); 365 palaceIntSet.removeAll(tmpIntSet); 366 //说明两个集合相等 367 if (palaceIntSet.size() == 0){ 368 CandiInfo candiInfo = new CandiInfo(); 369 candiInfo.location = key+"|"+tmpKey; 370 candiInfo.nums = palaceMap.get(key); 371 maps.add(candiInfo); 372 } 373 } 374 } 375 } 376 } 377 } 378 List<CandiInfo> infos = new ArrayList<>(); 379 CandiInfo candiInfo = null; 380 for (CandiInfo info : maps){ 381 if (candiInfo == null){ 382 candiInfo = info; 383 }else { 384 if (candiInfo.nums.equals(info.nums)) { 385 infos.add(info); 386 } 387 candiInfo = info; 388 } 389 } 390 return infos; 391 } 392 393 /** 394 * 校验这个数独是不是还满足数独的特点 395 * 思路: 396 * 1. 校验行和列有没有重复的数字 397 * 2. 校验数独是0的格子,对应的隐形数组还有没有值,如果没有候选值,肯定是某一个地方填错了 398 * @param candi 隐形数组 399 * @param sudo 数独二维数组 400 * @return 401 */ 402 private static boolean isOkSudo(int[][][] candi, int[][] sudo){ 403 boolean flag = true; 404 for (int i=0; i<9; i++){ 405 //校验行 406 Set<Integer> rowSet = new HashSet<>(); 407 //校验列 408 Set<Integer> clumnSet = new HashSet<>(); 409 for (int j=0; j<9; j++){ 410 int rowV = sudo[i][j]; 411 int cloumV = sudo[j][i]; 412 if (rowV>0){ 413 if (!rowSet.add(rowV)) { 414 flag = false; 415 break; 416 } 417 } 418 if (cloumV>0){ 419 if (!clumnSet.add(cloumV)) { 420 flag = false; 421 break; 422 } 423 } 424 425 } 426 if (!flag){ 427 break; 428 } 429 } 430 //校验隐形数字是否为空 431 for (int m=0; m<9; m++){ 432 for (int n=0; n<9; n++){ 433 if (sudo[m][n] == 0){ 434 int[] s = candi[m][n]; 435 Set<Integer> set = new HashSet<>(); 436 for (int p=0; p<s.length; p++){ 437 if (s[p]>0){ 438 set.add(s[p]); 439 } 440 } 441 if (set.size() == 0){ 442 flag = false; 443 break; 444 } 445 } 446 } 447 } 448 return flag; 449 } 450 451 /** 452 * 深度复制set集合 453 * @param source 454 * @return 455 */ 456 private static Set<Integer> deepCopySet(Set<Integer> source){ 457 Set<Integer> deepCopy = new HashSet<>(); 458 Iterator iterator = source.iterator(); 459 while (iterator.hasNext()){ 460 deepCopy.add((Integer) iterator.next()); 461 } 462 return deepCopy; 463 } 464 465 public static class CandiInfo{ 466 String location; 467 Set<Integer> nums; 468 } 469 }
以下是解析出的结果:
5. 经验总结
从有解析数独这个想法,到代码实现,大约经历了3天左右,在这个过程中会想一下怎么去构造解析,以及代码的逻辑,和解题的思路。对其中的收获就是Set集合的用法,数组的深浅复制,值传递引用传递等,以及怎么去构建一个数据结构来表示想要表达的东西。有些东西确实是了解,但是真正用的时候可能觉得自己知道的还不够,知识需要去积累学习,希望通过一个数独的解题思路,来温故一些基础知识。感谢阅读!