一次Java解析數獨的經歷
1. 背景
中午下樓去吃飯,電梯裡看到有人在玩數獨,之前也玩過,不過沒有用程式去解過,萌生了一個想法,這兩天就一直想怎麼用程式去解一個數獨。要去解開一個數獨,首先要先了解數獨的遊戲規則,這樣才能找到對應的演算法去解開。以下是本人用Java語言對數獨進行的解析,程式碼只是拋磚引玉,歡迎大神們給指點指點。
2. 數獨知識
數獨是源自18世紀瑞士的一種數學遊戲。是一種運用紙、筆進行演算的邏輯遊戲。玩家需要根據9×9盤面上的已知數字,推理出所有剩餘空格的數字,並滿足每一行、每一列、每一個粗線宮(3*3)內的數字均含1-9,不重複。
數獨盤面是個九宮,每一宮又分為九個小格。在這八十一格中給出一定的已知數字和解題條件,利用邏輯和推理,在其他的空格上填入1-9的數字。使1-9每個數字在每一行、每一列和每一宮中都只出現一次,所以又稱“九宮格”。
水平方向有九橫行,垂直方向有九縱列的矩形,畫分八十一個小正方形,稱為九宮格(Grid),如圖一所示,是數獨(Sudoku)的作用範圍。
三行與三列相交之處有九格,每一單元稱為小九宮(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集合的用法,陣列的深淺複製,值傳遞引用傳遞等,以及怎麼去構建一個數據結構來表示想要表達的東西。有些東西確實是瞭解,但是真正用的時候可能覺得自己知道的還不夠,知識需要去積累學習,希望通過一個數獨的解題思路,來溫故一些基礎知識。感謝閱讀!