關於一個最簡單的數獨解題實現與疑惑一

punisher_cn發表於2018-10-31

一、緣起

  之前買了一本《演算法的樂趣》,這麼多日子裡根本沒看過。我可能是一個書籍的收藏者而不是讀者,因為在辦公室裡的書架上琳琅滿目的擺放了幾十本書了,可所讀者寥寥無幾!言歸正傳,偶然看了這本書中關於數獨的章節,覺得有意思,但書中程式碼不全,所以自己動手試試,看看能不能按照原作者的思路把這個問題解決了。

 二、編碼

  1、首先說我自己是一個非常業餘的程式設計愛好者,既不是本專業,也不從事相關工作,所以程式碼中肯定有很多亂七八糟的寫法,如果有人看到後想揍人,那麼。。。。。。

        /// <summary>
        /// 關於單元格的類
        /// </summary>
        [Serializable]
        public class SudokuCell
        {
            public int num;        // 該單元格的值
            public bool isFixed;   // 該單元格是否是確定的數值
            public List<int> candidatures ;  // 候選數列表
            public SudokuCell()
            {
                candidatures = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 };  // 候選數列表
            }
        }    

  這裡的思想是這樣的,對於數獨遊戲而言(這裡只考慮9*9),有81個格子。每個格子,可以認為是一個單元格。這裡用SudokuCell這個類來表示,同時,用一個候選數列表來表示該單元格還可以填入的數字。

  2、再定義一個關於整個盤面的類,表示數獨遊戲自身,因為內容較多,我分開來寫

     /// <summary>
        /// 關於整個盤面的類
        /// </summary>
        [Serializable]
        public class SudokuGame
        {
        ......  
     }

  2.1、SudokuGame類中,定義幾個資料成員

      // 定義單元格陣列,表示81個單元格
         public SudokuCell[,] cells = new SudokuCell[9,9];
         // 記錄已經確認的單元格的數量,就是那些數字已經確認無誤的單元格的數量
         public int fixedCount;

  2.2、建構函式

            /// <summary>
            /// 建構函式,自己給了一個局面
            /// </summary>
            public  SudokuGame()
            {
                int[] data = new int[] {
                    7,6,1,0,3,0,0,2,0,
                    0,5,0,0,0,8,1,0,7,
                    0,0,0,0,0,7,0,3,4,
                    0,0,9,0,0,6,0,7,8,
                    0,0,3,2,7,9,5,0,0,
                    5,7,0,3,0,0,9,0,2,
                    1,9,0,7,6,0,0,0,0,
                    8,0,2,4,0,0,0,6,0,
                    6,4,0,0,1,0,2,5,0
                };
for (int i = 0; i < 81; i++) { int num = data[i]; // 單個的值 cells[i / 9, i % 9] = new SudokuCell(); cells[i / 9, i % 9].num = num; // 這裡,把給定的盤面複製到cells中 if (num != 0) { cells[i / 9, i % 9].isFixed = true; fixedCount++; } } }

  2.3、最終盤面輸出

 1             /// <summary>
 2             ///  輸出得到的結果
 3             /// </summary>
 4             public void WriteResult()
 5             {
 6                 Console.WriteLine("當前的fixedCount為:{0},局面為:",fixedCount);
 7                 // 這裡因為已知是9*9的陣列
 8                
 9                 for (int i = 0; i < 9; i++)
10                 {
11                     for (int j = 0; j < 9; j++)
12                     {
13                         string s = cells[i, j].num.ToString();
14                         Console.Write(s + " ");
15                     }
16                     // 輸出換行
17                     Console.WriteLine();    
18                 }
19             }

  2.4 、傳入一個需要判定的位置,看該位置是否已經有確定的值,有的話就跳到下一個位置

            /// <summary>
            ///  跳過已被確定好了數字的位置
            /// </summary>
            /// <param name="sp">需要判斷的位置,0-80 </param>
            /// <returns>比傳入的位置+1的位置</returns>
            public int SkipFixedCell(int sp)
            {
                // 判斷傳入的位置是否在陣列範圍內
                if (sp<0 || sp >=81)
                {
                    return 81;  // 因為從0開始,sp=80的時候是最後一個位置,這個位置的資料是需要處理的,處理之後,sp就變為81了
                }
                // 這裡,要把這個sp修改為行列值
                int row = sp / 9;
                int col = sp % 9;
                if (cells[row,col].isFixed == true)
                {
                    // 如果當前位置已被確定,繼續判定下一個位置
                    SkipFixedCell(++sp);
                }
                return sp;
            }

  2.5 、設定某個單元格的值

            /// <summary>
            ///  將某個值,設定到某個指定的位置,並進行檢驗
            /// </summary>
            /// <param name="row">需要設定的行號</param>
            /// <param name="col">需要設定的列號</param>
            /// <param name="num">需要設定的值</param>
            /// <returns></returns>
            public bool SetCandidatureToFixed(int row,int col,int num)
            {
                //1、設定值
                cells[row, col].num = num;
                // 確定已確認的數字
                cells[row, col].isFixed = true;
                //2、排除相關20個格中候選數中的num值
                if (!ExclusiveCorrelativeCandidatures(row, col, num))
                {
                    return false;
                }
                //3、檢視相關20格在刪除num之後的狀態,並且如果觸發了唯一值,則繼續進行遞迴
                if (!ProcessSinglesCandidature(row, col))
                {
                    return false;
                }
                //4、到這裡,說明前面填入的num沒有問題,可以確定這個單元格的數字了
                fixedCount++;
                return true;

            }

  2.6、刪除相關20格的函式

  1             /// <summary>
  2             /// 在某個單元格的相關20格中刪除該單元格已經確定的數字
  3             /// </summary>
  4             /// <param name="row">該單元格所在的行</param>
  5             /// <param name="col">該單元格所在的列</param>
  6             /// <param name="num">該單元格的填充值</param>
  7             /// <returns>要刪除的數字如果不存在,則證明錯誤,返回false</returns>
  8             public bool ExclusiveCorrelativeCandidatures(int row,int col,int num)
  9             {
 10                 // 遍歷行
 11                 for (int currentCol = 0; currentCol < 9; currentCol++)
 12                 {
 13                     // 傳入的數字的當前數的位置自然要跳過的,自己和自己比是沒有意義的
 14                     if (currentCol == col)
 15                     {
 16                         continue;
 17                     }
 18                     // 如果當前單元格未確定數字
 19                     if (!cells[row, currentCol].isFixed )
 20                     {
 21                         //如果候選數中存在這個要刪除的數字
 22                         if (cells[row, currentCol].candidatures.Contains(num))
 23                         {
 24                             // 如果要刪除的數字是這個候選數列表的最後一個數字了
 25                             // 那麼刪除的話,導致候選數列表為空,則表示資料填入錯誤了
 26                             if (cells[row, currentCol].candidatures.Count == 1)
 27                             {
 28                                 return false;
 29                             }
 30                             // 從候選數列表中刪除這個指定的數字
 31                             cells[row, currentCol].candidatures.Remove(num);
 32                         }
 33                     }
 34                     else
 35                     {
 36                         // 對於已經確定了數字的單元格,則要比較是否與新填入的數字一致
 37                         if (cells[row, currentCol].num == num)
 38                         {
 39                             return false;
 40                         }
 41                     }
 42                 }
 43                 // 遍歷列
 44                 for (int currentRow = 0; currentRow < 9; currentRow++)
 45                 {
 46                     if (currentRow == row)
 47                     {
 48                         continue;
 49                     }
 50                     // 如果當前單元格未確定數字
 51                     if (!cells[currentRow, col].isFixed)
 52                     {
 53                         //如果候選數中存在這個要刪除的數字
 54                         if (cells[currentRow, col].candidatures.Contains(num))
 55                         {
 56                             // 如果要刪除的數字是這個候選數列表的最後一個數字了
 57                             // 那麼刪除的話,導致候選數列表為空,則表示資料填入錯誤了
 58                             if (cells[currentRow, col].candidatures.Count == 1)
 59                             {
 60                                 return false;
 61                             }
 62                             // 從候選數列表中刪除這個指定的數字
 63                             cells[currentRow, col].candidatures.Remove(num);
 64                         }
 65                     }
 66                     else
 67                     {
 68                         // 對於已經確定了數字的單元格,則要比較是否與新填入的數字一致
 69                         if (cells[currentRow, col].num == num)
 70                         {
 71                             return false;
 72                         }
 73                     }
 74                 }
 75                 // 遍歷所在的九宮格
 76                 // 這裡,可以把81格看成9個9*9的九宮格,那麼根據row、col的值,讓其除以3,則可以得到
 77                 // 0,0   0,1   0,2
 78                 // 1,0   1,1   1,2
 79                 // 2,0   2,1   2,2
 80                 // 得到這樣的位置資訊
 81                 // 再進一步,這裡用一個3維陣列來表示怎麼樣?這裡估計可能是一個笨方法
 82                 // 我的想法是可以避免去判斷某個位置的九宮
 83                 int[,][] arr = new int[3,3][];
 84                 arr[0, 0] = new int[] { 0, 1, 2,  9, 10, 11, 18, 19, 20 };
 85                 arr[0, 1] = new int[] { 3, 4, 5, 12, 13, 14, 21, 22, 23 };
 86                 arr[0, 2] = new int[] { 6, 7, 8, 15, 16, 17, 24, 25, 26 };
 87                 arr[1, 0] = new int[] { 27, 28, 29, 36, 37, 38, 45, 46, 47 };
 88                 arr[1, 1] = new int[] { 30, 31, 32, 39, 40, 41, 48, 49, 50 };
 89                 arr[1, 2] = new int[] { 33, 34, 35, 42, 43, 44, 51, 52, 53 };
 90                 arr[2, 0] = new int[] { 54, 55, 56, 63, 64, 65, 72, 73, 74 };
 91                 arr[2, 1] = new int[] { 57, 58, 59, 66, 67, 68, 75, 76, 77 };
 92                 arr[2, 2] = new int[] { 60, 61, 62, 69, 70, 71, 78, 79, 80 };
 93 
 94                 // 獲取給定的點所在的位置,可以從上述的二維陣列中定位
 95                 int r = row / 3;
 96                 int c = col / 3;
 97 
 98                 // 根據二維資料的定位,獲取了在3維陣列中的值
 99                 for (int i = 0; i < 9; i++)
100                 {
101                     int indexOfAll = arr[r, c][i];
102                     // 把諸如14,21這樣的值再轉化為row、col形式
103                     int rr = indexOfAll / 9;
104                     int cc = indexOfAll % 9;
105 
106                     // 判斷是否是當前位置
107                     if (rr == row && cc == col)
108                     {
109                         continue;
110                     }
111 
112                     if (!cells[rr, cc].isFixed)
113                     {
114                         //如果候選數中存在這個要刪除的數字
115                         if (cells[rr, cc].candidatures.Contains(num))
116                         {
117                             // 如果要刪除的數字是這個候選數列表的最後一個數字了
118                             // 那麼刪除的話,導致候選數列表為空,則表示資料填入錯誤了
119                             if (cells[rr, cc].candidatures.Count == 1)
120                             {
121                                 return false;
122                             }
123                             // 從候選數列表中刪除這個指定的數字
124                             cells[rr, cc].candidatures.Remove(num);
125                         }
126                     }
127                     else
128                     {
129                         // 對於已經確定了數字的單元格,則要比較是否與新填入的數字一致
130                         if (cells[rr, cc].num == num)
131                         {
132                             return false;
133                         }
134                     }
135                 }
136                 return true;
137             }

  2.7、檢視相關20格函式,看是否有唯一解出現

  1             /// <summary>
  2             /// 檢視相關20格的狀態,是否存在唯一候選數,存在,則繼續呼叫SetCandidatureToFixed
  3             /// </summary>
  4             /// <param name="row">該單元格所在的行</param>
  5             /// <param name="col">該單元格所在的列</param>
  6             /// <returns>是否存在錯誤</returns>
  7             public bool ProcessSinglesCandidature(int row, int col)
  8             {
  9                 // ExclusiveCorrelativeCandidatures,該函式保證了候選數至少會存在一個
 10                 // 遍歷行
 11                 for (int currentCol = 0; currentCol < 9; currentCol++)
 12                 {
 13                     // 需要判斷是否是當前的位置
 14                     if (currentCol == col)
 15                     {
 16                         continue;
 17                     }
 18                     // 如果當前單元格的數字沒有確定,而且只有一個候選數
 19                     if (!cells[row, currentCol].isFixed && cells[row, currentCol].candidatures.Count == 1)
 20                     {
 21                         // 那麼就應該把這個數字確定,並且以它為原則,繼續確認其他數字
 22                         int num = cells[row, currentCol].candidatures[0];
 23 
 24                         // 遞迴呼叫,繼續搜尋
 25                         //fixedCount++;
 26                         //if (!ProcessSinglesCandidature(row, currentCol))
 27                         //{
 28                         //    return false;
 29                         //}
 30 
 31 
 32                         if (!SetCandidatureToFixed(row, currentCol, num))
 33                         {
 34                             return false;
 35                         }
 36                     }
 37                 }
 38                 // 遍歷列
 39                 for (int currentRow = 0; currentRow < 9; currentRow++)
 40                 {
 41                     // 需要判斷是否是當前的位置
 42                     if (currentRow == row)
 43                     {
 44                         continue;
 45                     }
 46                     // 如果當前單元格的數字沒有確定,而且只有一個候選數
 47                     if (!cells[currentRow, col].isFixed && cells[currentRow, col].candidatures.Count == 1)
 48                     {
 49                         // 那麼就應該把這個數字確定,並且以它為原則,繼續確認其他數字
 50                         int num = cells[currentRow, col].candidatures[0];
 51                         // 遞迴呼叫,繼續搜尋
 52                         //fixedCount++;
 53                         //if (!ProcessSinglesCandidature(currentRow, col))
 54                         //{
 55                         //    return false;
 56                         //}
 57 
 58                         if (!SetCandidatureToFixed(currentRow, col, num))
 59                         {
 60                             return false;
 61                         }
 62                     }
 63                 }
 64                 // 遍歷所在的九宮格
 65                 // 這裡,可以把81格看成9個9*9的九宮格,那麼根據row、col的值,讓其除以3,則可以得到
 66                 // 0,0   0,1   0,2
 67                 // 1,0   1,1   1,2
 68                 // 2,0   2,1   2,2
 69                 // 得到這樣的位置資訊
 70                 // 再進一步,這裡用一個3維陣列來表示怎麼樣?
 71                 // 我的想法是可以避免去判斷某個位置的九宮
 72                 int[,][] arr = new int[3, 3][];
 73                 arr[0, 0] = new int[] { 0, 1, 2, 9, 10, 11, 18, 19, 20 };
 74                 arr[0, 1] = new int[] { 3, 4, 5, 12, 13, 14, 21, 22, 23 };
 75                 arr[0, 2] = new int[] { 6, 7, 8, 15, 16, 17, 24, 25, 26 };
 76                 arr[1, 0] = new int[] { 27, 28, 29, 36, 37, 38, 45, 46, 47 };
 77                 arr[1, 1] = new int[] { 30, 31, 32, 39, 40, 41, 48, 49, 50 };
 78                 arr[1, 2] = new int[] { 33, 34, 35, 42, 43, 44, 51, 52, 53 };
 79                 arr[2, 0] = new int[] { 54, 55, 56, 63, 64, 65, 72, 73, 74 };
 80                 arr[2, 1] = new int[] { 57, 58, 59, 66, 67, 68, 75, 76, 77 };
 81                 arr[2, 2] = new int[] { 60, 61, 62, 69, 70, 71, 78, 79, 80 };
 82 
 83                 // 獲取給定的點所在的位置,可以從上述的二維陣列中定位
 84                 int r = row / 3;
 85                 int c = col / 3;
 86 
 87                 // 根據二維資料的定位,獲取了在3維陣列中的值
 88                 for (int i = 0; i < 9; i++)
 89                 {
 90                     int indexOfAll = arr[r, c][i];
 91                     // 把諸如14,21這樣的值再轉化為row、col形式
 92                     int rr = indexOfAll / 9;
 93                     int cc = indexOfAll % 9;
 94                     // 需要判斷是否是當前的位置
 95                     if (rr == row && cc == col)
 96                     {
 97                         continue;
 98                     }
 99                     if (!cells[rr, cc].isFixed && cells[rr, cc].candidatures.Count == 1)
100                     {
101                         // 那麼就應該把這個數字確定,並且以它為原則,繼續確認其他數字
102                         int num = cells[rr, cc].candidatures[0];
103                         // 遞迴呼叫,繼續搜尋
104 
105                         //fixedCount++;
106                         //if (!ProcessSinglesCandidature(rr, cc))
107                         //{
108                         //    return false;
109                         //}
110 
111                         if (!SetCandidatureToFixed(rr, cc, num))
112                         {
113                             return false;
114                         }
115                     }
116                 }
117                 return true;
118             }  

   3、遊戲類,問題在這裡,隨後再說

 1         /// <summary>
 2         ///  問題解決類
 3         /// </summary>
 4         public class Solution
 5         {
 6             public static int gameCount = 0;
 7             // 3、類似於窮舉的演算法
 8             /// <summary>
 9             /// 求解演算法,這是一個遞迴演算法
10             /// </summary>
11             /// <param name="game">當前的局面</param>
12             /// <param name="sp">查詢的位置,從0開始,到80結束</param>
13             public void FindSudokuSolution(SudokuGame game, int sp)
14             {
15                 // 0、設定遞迴演算法的結束條件
16                 if (game.fixedCount >= 95)
17                 {
18                     game.WriteResult();
19                     return;
20                 }
21                 // 1、判斷要查詢的位置是否已經被確定
22                 // 之前所使用函式,是因為可能存在連續被確定的情況
23                 // 所以,下面這個函式也是一個遞迴函式
24                 sp = game.SkipFixedCell(sp);
25                 if (sp >= 81 || sp < 0)
26                 {
27                     // 如果已經超出了陣列的範圍,那麼直接返回即可
28                     return;
29                 }
30                 // 2、獲取當前位置的cell
31                 int row = sp / 9;
32                 int col = sp % 9;
33                 SudokuCell currentCell = new SudokuCell();
34                 currentCell = game.cells[row, col];
35 
36                 // 3、定義一個新狀態,用於儲存當前的game的狀態
37                 SudokuGame newGameState = new SudokuGame();
38 
39                 // 
40                 for (int i = 0; i < currentCell.candidatures.Count; i++)
41                 {
42                     newGameState = DeepCopyGame(game);    //把當前的狀態儲存一下
43 
44                    // Console.WriteLine("建立了{0}個局面了",gameCount++);
45 
46                     int currentCandidature = currentCell.candidatures.ElementAt(i);
47                     if (newGameState.SetCandidatureToFixed(row, col, currentCandidature))
48                     {
49                         // 試數成功,沒有衝突,進行下一個單元格
50                         
51                         FindSudokuSolution(newGameState, sp+1);
52                     }
53                 }
54                 return;
55             }
56         }

  4、深拷貝類

 1         /// <summary>
 2         ///  通過序列化實現Game的深複製
 3         /// </summary>
 4         /// <param name="obj"></param>
 5         /// <returns></returns>
 6         public static SudokuGame DeepCopyGame(SudokuGame obj)
 7         {
 8             object retVal;
 9             using (MemoryStream ms = new MemoryStream())
10             {
11                 BinaryFormatter bf = new BinaryFormatter();
12                 //序列化
13                 bf.Serialize(ms,obj);
14                 ms.Seek(0,SeekOrigin.Begin);
15                 // 反序列化
16                 retVal = bf.Deserialize(ms);
17                 ms.Close();
18             }
19             return (SudokuGame)retVal;
20         }

  5、呼叫方式

1             SudokuGame game = new SudokuGame();
2             Solution s = new Solution();
3             s.FindSudokuSolution(game,0);

  6、疑惑與說明

  上面,就把所有的程式碼都完成了,試了幾個局面,也能得到正確的結果。但有個問題讓我百思不得其解。

在3中,有    if (game.fixedCount >= 95) 此句作為遞迴的結束條件。可是,這裡這個數字不應該是81嗎????但是,如果寫81,最終輸出的盤面中,會有一部分數字沒有得到答案,而這個95,也是我通過一次次的摸索,根據最後能得到答案的結果來實現的。這裡,因為是遞迴,所以局面太多,簡單的除錯根本進行不下去。。。。。。不知道最終的問題何在,後面還需要慢慢的摸索,更希望有哪位能給指點一下迷津,提前謝謝了。




 

 

 

 

 

  

相關文章