一、緣起
之前買了一本《演算法的樂趣》,這麼多日子裡根本沒看過。我可能是一個書籍的收藏者而不是讀者,因為在辦公室裡的書架上琳琅滿目的擺放了幾十本書了,可所讀者寥寥無幾!言歸正傳,偶然看了這本書中關於數獨的章節,覺得有意思,但書中程式碼不全,所以自己動手試試,看看能不能按照原作者的思路把這個問題解決了。
二、編碼
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,也是我通過一次次的摸索,根據最後能得到答案的結果來實現的。這裡,因為是遞迴,所以局面太多,簡單的除錯根本進行不下去。。。。。。不知道最終的問題何在,後面還需要慢慢的摸索,更希望有哪位能給指點一下迷津,提前謝謝了。