1. 程式人生 > >一次Java解析數獨的經歷

一次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知識進行回顧和學習。採用的是隱形唯一候選數法,什麼是唯一候選數法呢,就是某個數字在某一行列宮格的候選數中只出現一次,就是這個格子只有一個數可選了,那這個格子裡就只能填這個數,這就是唯一候選數法,其實也是排除法。參照的這篇文章進行的一次數獨解析,數獨解題方法大全,可以參考學習一下。

解題思路:

  1. 要解析的數獨,與數獨對應的隱形陣列;
  2. 排除掉隱形陣列中的數字,哪些數字需要排除呢,就是數獨中已有的數字,要排除該數字所在的行、列、宮。例如,如下圖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集合的用法,陣列的深淺複製,值傳遞引用傳遞等,以及怎麼去構建一個數據結構來表示想要表達的東西。有些東西確實是瞭解,但是真正用的時候可能覺得自己知道的還不夠,知識需要去積累學習,希望通過一個數獨的解題思路,來溫故一些基礎知識。感謝閱讀!