C# 數獨求解演算法。
前言
數獨是一種有趣的智力遊戲,但是部分高難度數獨在求解過程中經常出現大量單元格有多個候選數字可以填入,不得不嘗試填寫某個數字然後繼續推導的方法。不幸的是這種方法經常出現填到一半才發現有單元格無數可填,說明之前就有單元格填錯了把後面的路堵死了。這時就需要悔步,之前的單元格換個數重新試。然而更坑的是究竟要悔多少步呢?不知道。要換數字的時候該換哪個呢?也不知道。手算時就需要大量草稿紙記錄填寫情況,不然容易忘了哪些試過哪些沒試過。
在朋友那裡玩他手機上的數獨的時候就發現這個問題很煩,到這裡其實就不是一個智力遊戲,而是體力遊戲了。這種體力活實際上交給電腦才是王道。網上搜了一圈,大多都是Java、vb、C++之類的實現,且多是遞迴演算法。遞迴有一個問題,隨著問題規模的擴大,很容易不小心就把棧撐爆,而且大多數實現只是求出答案就完了,很多求解中的資訊就沒了,而我更想看看這些過程資訊。改別人的程式碼實在是太蛋疼,想了想,不如自己重新寫一個。
正文
說回正題,先簡單說明一下演算法思路(標準數獨):
1、先尋找並填寫那些唯一數單元格。在部分數獨中有些單元格會因為同行、列、宮內題目已知數的限制,實際只有一個數可以填,這種單元格就應該趁早填好,因為沒有嘗試的必要,不提前處理掉還會影響之後求解的效率。在填寫數字後,同行、列、宮的候選數就會減少,可能會出現新的唯一數單元格,那麼繼續填寫,直到沒有唯一數單元格為止。
2、檢查是否已經完成遊戲,也就是所有單元格都有數字。部分簡單數獨一直填唯一數單元格就可以完成遊戲。
3、按照從單元格左到右、從上到下,數字從小到大的順序嘗試填寫有多個候選數的單元格,直到全部填完或者發現有單元格候選數為空。如果出現無候選數的單元格說明之前填錯數導致出現死路,就需要悔步清除上一個單元格填過的數,換成下一個候選數繼續嘗試。如果清除後發現沒有更大的候選數可填,說明更早之前就已經填錯了,要繼續悔步並換下一個候選數。有可能需要連續悔多步,一直悔步直到有更大的候選數可填的單元格。如果一路到最開始的單元格都沒法填,說明這個數獨有問題,無解。
程式碼(包括數獨求解器,求解過程資訊,答案儲存三個主要類):
數獨求解器
1 public class SudokuSolver 2 { 3 /// <summary> 4 /// 題目面板 5 /// </summary> 6 public SudokuBlock[][] SudokuBoard { get; } 7 8 public SudokuSolver(byte[][] board) 9 { 10 SudokuBoard = new SudokuBlock[board.Length][]; 11 //初始化數獨的行 12 for (int i = 0; i < board.Length; i++) 13 { 14 SudokuBoard[i] = new SudokuBlock[board[i].Length]; 15 //初始化每行的列 16 for (int j = 0; j < board[i].Length; j++) 17 { 18 SudokuBoard[i][j] = new SudokuBlock( 19 board[i][j] > 0 20 , board[i][j] <= 0 ? new BitArray(board.Length) : null 21 , board[i][j] > 0 ? (byte?)board[i][j] : null 22 , (byte)i 23 , (byte)j); 24 } 25 } 26 } 27 28 /// <summary> 29 /// 求解數獨 30 /// </summary> 31 /// <returns>獲得的解</returns> 32 public IEnumerable<(SudokuState sudoku, PathTree path)> Solve(bool multiAnswer = false) 33 { 34 //初始化各個單元格能填入的數字 35 InitCandidate(); 36 37 var pathRoot0 = new PathTree(null, -1, -1, -1); //填寫路徑樹,在非遞迴方法中用於記錄回退路徑和其他有用資訊,初始生成一個根 38 var path0 = pathRoot0; 39 40 //迴圈填入能填入的數字只有一個的單元格,每次填入都可能產生新的唯一數單元格,直到沒有唯一數單元格可填 41 while (true) 42 { 43 if (!FillUniqueNumber(ref path0)) 44 { 45 break; 46 } 47 } 48 49 //檢查是否在填唯一數單元格時就已經把所有單元格填滿了 50 var finish = true; 51 foreach (var row in SudokuBoard) 52 { 53 foreach (var cell in row) 54 { 55 if (!cell.IsCondition && !cell.IsUnique) 56 { 57 finish = false; 58 break; 59 } 60 } 61 if (!finish) 62 { 63 break; 64 } 65 } 66 if (finish) 67 { 68 yield return (new SudokuState(this), path0); 69 yield break; 70 } 71 72 var pathRoot = new PathTree(null, -1, -1, -1); //填寫路徑樹,在非遞迴方法中用於記錄回退路徑和其他有用資訊,初始生成一個根 73 var path = pathRoot; 74 var toRe = new List<(SudokuState sudoku, PathTree path)>(); 75 //還存在需要試數才能求解的單元格,開始暴力搜尋 76 int i = 0, j = 0; 77 while (true) 78 { 79 (i, j) = NextBlock(i, j); 80 81 //正常情況下返回-1表示已經全部填完 82 if (i == -1 && j == -1 && !multiAnswer) 83 { 84 var pathLast = path;//記住最後一步 85 var path1 = path; 86 while(path1.Parent.X != -1 && path1.Parent.Y != -1) 87 { 88 path1 = path1.Parent; 89 } 90 91 //將暴力搜尋的第一步追加到唯一數單元格的填寫步驟的最後一步之後,連線成完整的填數步驟 92 path0.Children.Add(path1); 93 path1.Parent = path0; 94 yield return (new SudokuState(this), pathLast); 95 break; 96 } 97 98 var numNode = path.Children.LastOrDefault(); 99 //確定要從哪個數開始進行填入嘗試 100 var num = numNode == null 101 ? 0 102 : numNode.Number; 103 104 bool filled = false; //是否發現可以填入的數 105 //迴圈檢視從num開始接下來的候選數是否能填(num是最後一次填入的數,傳到Candidate[]的索引器中剛好指向 num + 1是否能填的儲存位,對於標準數獨,候選數為 1~9,Candidate的索引範圍就是 0~8) 106 for (; !SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique && num < SudokuBoard[i][j].Candidate.Length; num++) 107 { 108 //如果有可以填的候選數,理論上不會遇見沒有可以填的情況,這種死路情況已經在UpdateCandidate時檢查了 109 if (SudokuBoard[i][j].Candidate[num] && !path.Children.Any(x => x.Number - 1 == num && !x.Pass)) 110 { 111 filled = true; //進來了說明單元格有數可以填 112 //記錄步驟 113 var node = new PathTree(SudokuBoard[i][j], i, j, num + 1, path); 114 path = node; 115 //如果更新相關單元格的候選數時發現死路(更新函式會在發現死路時自動撤銷更新) 116 (bool canFill, (byte x, byte y)[] setList) updateResult = UpdateCandidate(i, j, (byte)(num + 1)); 117 if (!updateResult.canFill) 118 { 119 //記錄這條路是死路 120 path.SetPass(false); 121 } 122 //僅在確認是活路時設定填入數字 123 if (path.Pass) 124 { 125 SudokuBoard[i][j].SetNumber((byte)(num + 1)); 126 path.SetList = updateResult.setList;//記錄相關單元格可填數更新記錄,方便在回退時撤銷更新 127 } 128 else //出現死路,要進行回退,重試這個單元格的其他可填數字 129 { 130 path.Block.SetNumber(null); 131 path = path.Parent; 132 } 133 //填入一個候選數後跳出迴圈,不再繼續嘗試填入之後的候選數 134 break; 135 } 136 } 137 if (!filled)//如果沒有成功填入數字,說明上一步填入的單元格就是錯的,會導致後面的單元格怎麼填都不對,要回退到上一個單元格重新填 138 { 139 path.SetPass(false); 140 path.Block.SetNumber(null); 141 foreach (var pos in path.SetList) 142 { 143 SudokuBoard[pos.x][pos.y].Candidate.Set(path.Number - 1, true); 144 } 145 path = path.Parent; 146 i = path.X < 0 ? 0 : path.X; 147 j = path.Y < 0 ? 0 : path.Y; 148 } 149 } 150 } 151 152 /// <summary> 153 /// 初始化候選項 154 /// </summary> 155 private void InitCandidate() 156 { 157 //初始化每行空缺待填的數字 158 var rb = new List<BitArray>(); 159 for (int i = 0; i < SudokuBoard.Length; i++) 160 { 161 var r = new BitArray(SudokuBoard.Length); 162 r.SetAll(true); 163 for (int j = 0; j < SudokuBoard[i].Length; j++) 164 { 165 //如果i行j列是條件(題目)給出的數,設定數字不能再填(r[x] == false 表示 i 行不能再填 x + 1,下標加1表示數獨可用的數字,下標對應的值表示下標加1所表示的數是否還能填入該行) 166 if (SudokuBoard[i][j].IsCondition || SudokuBoard[i][j].IsUnique) 167 { 168 r.Set(SudokuBoard[i][j].Number.Value - 1, false); 169 } 170 } 171 rb.Add(r); 172 } 173 174 //初始化每列空缺待填的數字 175 var cb = new List<BitArray>(); 176 for (int j = 0; j < SudokuBoard[0].Length; j++) 177 { 178 var c = new BitArray(SudokuBoard[0].Length); 179 c.SetAll(true); 180 for (int i = 0; i < SudokuBoard.Length; i++) 181 { 182 if (SudokuBoard[i][j].IsCondition || SudokuBoard[i][j].IsUnique) 183 { 184 c.Set(SudokuBoard[i][j].Number.Value - 1, false); 185 } 186 } 187 cb.Add(c); 188 } 189 190 //初始化每宮空缺待填的數字(目前只能算標準 n×n 數獨的宮) 191 var gb = new List<BitArray>(); 192 //n表示每宮應有的行、列數(標準數獨行列、數相同) 193 var n = (int)Sqrt(SudokuBoard.Length); 194 for (int g = 0; g < SudokuBoard.Length; g++) 195 { 196 var gba = new BitArray(SudokuBoard.Length); 197 gba.SetAll(true); 198 for (int i = g / n * n; i < g / n * n + n; i++) 199 { 200 for (int j = g % n * n; j < g % n * n + n; j++) 201 { 202 if (SudokuBoard[i][j].IsCondition || SudokuBoard[i][j].IsUnique) 203 { 204 gba.Set(SudokuBoard[i][j].Number.Value - 1, false); 205 } 206 } 207 } 208 gb.Add(gba); 209 } 210 211 //初始化每格可填的候選數字 212 for (int i = 0; i < SudokuBoard.Length; i++) 213 { 214 for (int j = 0; j < SudokuBoard[i].Length; j++) 215 { 216 217 if (!SudokuBoard[i][j].IsCondition) 218 { 219 var c = SudokuBoard[i][j].Candidate; 220 c.SetAll(true); 221 //當前格能填的數為其所在行、列、宮同時空缺待填的數字,按位與運算後只有同時能填的候選數保持1(可填如當前格),否則變成0 222 // i / n * n + j / n:根據行號列號計算宮號, 223 c = c.And(rb[i]).And(cb[j]).And(gb[i / n * n + j / n]); 224 SudokuBoard[i][j].SetCandidate(c); 225 } 226 } 227 } 228 } 229 230 /// <summary> 231 /// 求解開始時尋找並填入單元格唯一可填的數,減少解空間 232 /// </summary> 233 /// <returns>是否填入過數字,如果為false,表示能立即確定待填數字的單元格已經沒有,要開始暴力搜尋了</returns> 234 private bool FillUniqueNumber(ref PathTree path) 235 { 236 var filled = false; 237 for (int i = 0; i < SudokuBoard.Length; i++) 238 { 239 for (int j = 0; j < SudokuBoard[i].Length; j++) 240 { 241 if (!SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique) 242 { 243 var canFillCount = 0; 244 var index = -1; 245 for (int k = 0; k < SudokuBoard[i][j].Candidate.Length; k++) 246 { 247 if (SudokuBoard[i][j].Candidate[k]) 248 { 249 index = k; 250 canFillCount++; 251 } 252 if (canFillCount > 1) 253 { 254 break; 255 } 256 } 257 if (canFillCount == 0) 258 { 259 throw new Exception("有單元格無法填入任何數字,數獨無解"); 260 } 261 if (canFillCount == 1) 262 { 263 var num = (byte)(index + 1); 264 SudokuBoard[i][j].SetNumber(num); 265 SudokuBoard[i][j].SetUnique(); 266 filled = true; 267 var upRes = UpdateCandidate(i, j, num); 268 if (!upRes.canFill) 269 { 270 throw new Exception("有單元格無法填入任何數字,數獨無解"); 271 } 272 path = new PathTree(SudokuBoard[i][j], i, j, num, path); 273 path.SetList = upRes.setList; 274 } 275 } 276 } 277 } 278 return filled; 279 } 280 281 /// <summary> 282 /// 更新單元格所在行、列、宮的其它單元格能填的數字候選,如果沒有,會撤銷更新 283 /// </summary> 284 /// <param name="row">行號</param> 285 /// <param name="column">列號</param> 286 /// <param name="canNotFillNumber">要剔除的候選數字</param> 287 /// <returns>更新候選數後,所有被更新的單元格是否都有可填的候選數字</returns> 288 private (bool canFill, (byte x, byte y)[] setList) UpdateCandidate(int row, int column, byte canNotFillNumber) 289 { 290 var canFill = true; 291 var list = new List<SudokuBlock>(); // 記錄修改過的單元格,方便撤回修改 292 293 bool CanFillNumber(int i, int j) 294 { 295 var re = true; 296 var _canFill = false; 297 for (int k = 0; k < SudokuBoard[i][j].Candidate.Length; k++) 298 { 299 if (SudokuBoard[i][j].Candidate[k]) 300 { 301 _canFill = true; 302 break; 303 } 304 } 305 if (!_canFill) 306 { 307 re = false; 308 } 309 310 return re; 311 } 312 bool Update(int i, int j) 313 { 314 if (!(i == row && j == column) && !SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique && SudokuBoard[i][j].Candidate[canNotFillNumber - 1]) 315 { 316 SudokuBoard[i][j].Candidate.Set(canNotFillNumber - 1, false); 317 list.Add(SudokuBoard[i][j]); 318 319 return CanFillNumber(i, j); 320 } 321 else 322 { 323 return true; 324 } 325 } 326 327 //更新該行其餘列 328 for (int j = 0; j < SudokuBoard[row].Length; j++) 329 { 330 canFill = Update(row, j); 331 if (!canFill) 332 { 333 break; 334 } 335 } 336 337 if (canFill) //只在行更新時沒發現無數可填的單元格時進行列更新才有意義 338 { 339 //更新該列其餘行 340 for (int i = 0; i < SudokuBoard.Length; i++) 341 { 342 canFill = Update(i, column); 343 if (!canFill) 344 { 345 break; 346 } 347 } 348 } 349 350 if (canFill)//只在行、列更新時都沒發現無數可填的單元格時進行宮更新才有意義 351 { 352 //更新該宮其餘格 353 //n表示每宮應有的行、列數(標準數獨行列、數相同) 354 var n = (int)Sqrt(SudokuBoard.Length); 355 //g為宮的編號,根據行號列號計算 356 var g = row / n * n + column / n; 357 for (int i = g / n * n; i < g / n * n + n; i++) 358 { 359 for (int j = g % n * n; j < g % n * n + n; j++) 360 { 361 canFill = Update(i, j); 362 if (!canFill) 363 { 364 goto canNotFill; 365 } 366 } 367 } 368 canNotFill:; 369 } 370 371 //如果發現存在沒有任何數字可填的單元格,撤回所有候選修改 372 if (!canFill) 373 { 374 foreach (var cell in list) 375 { 376 cell.Candidate.Set(canNotFillNumber - 1, true); 377 } 378 } 379 380 return (canFill, list.Select(x => (x.X, x.Y)).ToArray()); 381 } 382 383 /// <summary> 384 /// 尋找下一個要嘗試填數的格 385 /// </summary> 386 /// <param name="i">起始行號</param> 387 /// <param name="j">起始列號</param> 388 /// <returns>找到的下一個行列號,沒有找到返回-1</returns> 389 private (int x, int y) NextBlock(int i = 0, int j = 0) 390 { 391 for (; i < SudokuBoard.Length; i++) 392 { 393 for (; j < SudokuBoard[i].Length; j++) 394 { 395 if (!SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique && !SudokuBoard[i][j].Number.HasValue) 396 { 397 return (i, j); 398 } 399 } 400 j = 0; 401 } 402 403 return (-1, -1); 404 } 405 406 public override string ToString() 407 { 408 static string Str(SudokuBlock b) 409 { 410 var n1 = new[] { "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨" }; 411 var n2 = new[] { "⑴", "⑵", "⑶", "⑷", "⑸", "⑹", "⑺", "⑻", "⑼" }; 412 return b.Number.HasValue 413 ? b.IsCondition 414 ? " " + b.Number 415 : b.IsUnique 416 ? n1[b.Number.Value - 1] 417 : n2[b.Number.Value - 1] 418 : "▢"; 419 } 420 return 421 $@"{Str(SudokuBoard[0][0])},{Str(SudokuBoard[0][1])},{Str(SudokuBoard[0][2])},{Str(SudokuBoard[0][3])},{Str(SudokuBoard[0][4])},{Str(SudokuBoard[0][5])},{Str(SudokuBoard[0][6])},{Str(SudokuBoard[0][7])},{Str(SudokuBoard[0][8])} 422 {Str(SudokuBoard[1][0])},{Str(SudokuBoard[1][1])},{Str(SudokuBoard[1][2])},{Str(SudokuBoard[1][3])},{Str(SudokuBoard[1][4])},{Str(SudokuBoard[1][5])},{Str(SudokuBoard[1][6])},{Str(SudokuBoard[1][7])},{Str(SudokuBoard[1][8])} 423 {Str(SudokuBoard[2][0])},{Str(SudokuBoard[2][1])},{Str(SudokuBoard[2][2])},{Str(SudokuBoard[2][3])},{Str(SudokuBoard[2][4])},{Str(SudokuBoard[2][5])},{Str(SudokuBoard[2][6])},{Str(SudokuBoard[2][7])},{Str(SudokuBoard[2][8])} 424 {Str(SudokuBoard[3][0])},{Str(SudokuBoard[3][1])},{Str(SudokuBoard[3][2])},{Str(SudokuBoard[3][3])},{Str(SudokuBoard[3][4])},{Str(SudokuBoard[3][5])},{Str(SudokuBoard[3][6])},{Str(SudokuBoard[3][7])},{Str(SudokuBoard[3][8])} 425 {Str(SudokuBoard[4][0])},{Str(SudokuBoard[4][1])},{Str(SudokuBoard[4][2])},{Str(SudokuBoard[4][3])},{Str(SudokuBoard[4][4])},{Str(SudokuBoard[4][5])},{Str(SudokuBoard[4][6])},{Str(SudokuBoard[4][7])},{Str(SudokuBoard[4][8])} 426 {Str(SudokuBoard[5][0])},{Str(SudokuBoard[5][1])},{Str(SudokuBoard[5][2])},{Str(SudokuBoard[5][3])},{Str(SudokuBoard[5][4])},{Str(SudokuBoard[5][5])},{Str(SudokuBoard[5][6])},{Str(SudokuBoard[5][7])},{Str(SudokuBoard[5][8])} 427 {Str(SudokuBoard[6][0])},{Str(SudokuBoard[6][1])},{Str(SudokuBoard[6][2])},{Str(SudokuBoard[6][3])},{Str(SudokuBoard[6][4])},{Str(SudokuBoard[6][5])},{Str(SudokuBoard[6][6])},{Str(SudokuBoard[6][7])},{Str(SudokuBoard[6][8])} 428 {Str(SudokuBoard[7][0])},{Str(SudokuBoard[7][1])},{Str(SudokuBoard[7][2])},{Str(SudokuBoard[7][3])},{Str(SudokuBoard[7][4])},{Str(SudokuBoard[7][5])},{Str(SudokuBoard[7][6])},{Str(SudokuBoard[7][7])},{Str(SudokuBoard[7][8])} 429 {Str(SudokuBoard[8][0])},{Str(SudokuBoard[8][1])},{Str(SudokuBoard[8][2])},{Str(SudokuBoard[8][3])},{Str(SudokuBoard[8][4])},{Str(SudokuBoard[8][5])},{Str(SudokuBoard[8][6])},{Str(SudokuBoard[8][7])},{Str(SudokuBoard[8][8])}"; 430 } 431 }View Code
大多數都有註釋,配合註釋應該不難理解,如有問題歡迎評論區交流。稍微說一下,過載ToString是為了方便除錯和檢視狀態,其中空心方框表示未填寫數字的單元格,數字表示題目給出數字的單元格,圈數字表示唯一數單元格填寫的數字,括號數字表示有多個候選數通過嘗試(暴力搜尋)確定的數字。注意類檔案最上面有一個 using static System.Math; 匯入靜態類,不然每次呼叫數學函式都要 Math. ,很煩。
求解過程資訊
1 public class PathTree 2 { 3 public PathTree Parent { get; set; } 4 public List<PathTree> Children { get; } = new List<PathTree>(); 5 6 public SudokuBlock Block { get; } 7 public int X { get; } 8 public int Y { get; } 9 public int Number { get; } 10 public bool Pass { get; private set; } = true; 11 public (byte x, byte y)[] SetList { get; set; } 12 13 public PathTree(SudokuBlock block, int x, int y, int number) 14 { 15 Block = block; 16 X = x; 17 Y = y; 18 Number = number; 19 20 } 21 22 public PathTree(SudokuBlock block, int row, int column, int number, PathTree parent) 23 : this(block, row, column, number) 24 { 25 Parent = parent; 26 Parent.Children.Add(this); 27 } 28 29 public void SetPass(bool pass) 30 { 31 Pass = pass; 32 } 33 }View Code
其中記錄了每個步驟在哪個單元格填寫了哪個數字,上一步是哪一步,之後嘗試過哪些步驟,這一步是否會導致之後的步驟出現死路,填寫數字後影響到的單元格和候選數字(用來在悔步的時候恢復相應單元格的候選數字)。
答案儲存
1 public class SudokuState 2 { 3 public SudokuBlock[][] SudokuBoard { get; } 4 public SudokuState(SudokuSolver sudoku) 5 { 6 SudokuBoard = new SudokuBlock[sudoku.SudokuBoard.Length][]; 7 //初始化數獨的行 8 for (int i = 0; i < sudoku.SudokuBoard.Length; i++) 9 { 10 SudokuBoard[i] = new SudokuBlock[sudoku.SudokuBoard[i].Length]; 11 //初始化每行的列 12 for (int j = 0; j < sudoku.SudokuBoard[i].Length; j++) 13 { 14 SudokuBoard[i][j] = new SudokuBlock( 15 sudoku.SudokuBoard[i][j].IsCondition 16 , null 17 , sudoku.SudokuBoard[i][j].Number 18 , (byte)i 19 , (byte)j); 20 if (sudoku.SudokuBoard[i][j].IsUnique) 21 { 22 SudokuBoard[i][j].SetUnique(); 23 } 24 } 25 } 26 } 27 28 public override string ToString() 29 { 30 static string Str(SudokuBlock b) 31 { 32 var n1 = new[] { "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨" }; 33 var n2 = new[] { "⑴", "⑵", "⑶", "⑷", "⑸", "⑹", "⑺", "⑻", "⑼" }; 34 return b.Number.HasValue 35 ? b.IsCondition 36 ? " " + b.Number 37 : b.IsUnique 38 ? n1[b.Number.Value - 1] 39 : n2[b.Number.Value - 1] 40 : "▢"; 41 } 42 return 43 $@"{Str(SudokuBoard[0][0])},{Str(SudokuBoard[0][1])},{Str(SudokuBoard[0][2])},{Str(SudokuBoard[0][3])},{Str(SudokuBoard[0][4])},{Str(SudokuBoard[0][5])},{Str(SudokuBoard[0][6])},{Str(SudokuBoard[0][7])},{Str(SudokuBoard[0][8])} 44 {Str(SudokuBoard[1][0])},{Str(SudokuBoard[1][1])},{Str(SudokuBoard[1][2])},{Str(SudokuBoard[1][3])},{Str(SudokuBoard[1][4])},{Str(SudokuBoard[1][5])},{Str(SudokuBoard[1][6])},{Str(SudokuBoard[1][7])},{Str(SudokuBoard[1][8])} 45 {Str(SudokuBoard[2][0])},{Str(SudokuBoard[2][1])},{Str(SudokuBoard[2][2])},{Str(SudokuBoard[2][3])},{Str(SudokuBoard[2][4])},{Str(SudokuBoard[2][5])},{Str(SudokuBoard[2][6])},{Str(SudokuBoard[2][7])},{Str(SudokuBoard[2][8])} 46 {Str(SudokuBoard[3][0])},{Str(SudokuBoard[3][1])},{Str(SudokuBoard[3][2])},{Str(SudokuBoard[3][3])},{Str(SudokuBoard[3][4])},{Str(SudokuBoard[3][5])},{Str(SudokuBoard[3][6])},{Str(SudokuBoard[3][7])},{Str(SudokuBoard[3][8])} 47 {Str(SudokuBoard[4][0])},{Str(SudokuBoard[4][1])},{Str(SudokuBoard[4][2])},{Str(SudokuBoard[4][3])},{Str(SudokuBoard[4][4])},{Str(SudokuBoard[4][5])},{Str(SudokuBoard[4][6])},{Str(SudokuBoard[4][7])},{Str(SudokuBoard[4][8])} 48 {Str(SudokuBoard[5][0])},{Str(SudokuBoard[5][1])},{Str(SudokuBoard[5][2])},{Str(SudokuBoard[5][3])},{Str(SudokuBoard[5][4])},{Str(SudokuBoard[5][5])},{Str(SudokuBoard[5][6])},{Str(SudokuBoard[5][7])},{Str(SudokuBoard[5][8])} 49 {Str(SudokuBoard[6][0])},{Str(SudokuBoard[6][1])},{Str(SudokuBoard[6][2])},{Str(SudokuBoard[6][3])},{Str(SudokuBoard[6][4])},{Str(SudokuBoard[6][5])},{Str(SudokuBoard[6][6])},{Str(SudokuBoard[6][7])},{Str(SudokuBoard[6][8])} 50 {Str(SudokuBoard[7][0])},{Str(SudokuBoard[7][1])},{Str(SudokuBoard[7][2])},{Str(SudokuBoard[7][3])},{Str(SudokuBoard[7][4])},{Str(SudokuBoard[7][5])},{Str(SudokuBoard[7][6])},{Str(SudokuBoard[7][7])},{Str(SudokuBoard[7][8])} 51 {Str(SudokuBoard[8][0])},{Str(SudokuBoard[8][1])},{Str(SudokuBoard[8][2])},{Str(SudokuBoard[8][3])},{Str(SudokuBoard[8][4])},{Str(SudokuBoard[8][5])},{Str(SudokuBoard[8][6])},{Str(SudokuBoard[8][7])},{Str(SudokuBoard[8][8])}"; 52 } 53 }View Code
沒什麼好說的,就是儲存答案的,因為有些數獨的解不唯一,將來有機會擴充套件求多解時避免相互覆蓋。
還有一個輔助類,單元格定義
1 public class SudokuBlock 2 { 3 /// <summary> 4 /// 填入的數字 5 /// </summary> 6 public byte? Number { get; private set; } 7 8 /// <summary> 9 /// X座標 10 /// </summary> 11 public byte X { get; } 12 13 /// <summary> 14 /// Y座標 15 /// </summary> 16 public byte Y { get; } 17 18 /// <summary> 19 /// 候選數字,下標所示狀態表示數字“下標加1”是否能填入 20 /// </summary> 21 public BitArray Candidate { get; private set; } 22 23 /// <summary> 24 /// 是否為條件(題目)給出數字的單元格 25 /// </summary> 26 public bool IsCondition { get; } 27 28 /// <summary> 29 /// 是否為遊戲開始就能確定唯一可填數字的單元格 30 /// </summary> 31 public bool IsUnique { get; private set; } 32 33 public SudokuBlock(bool isCondition, BitArray candidate, byte? number, byte x, byte y) 34 { 35 IsCondition = isCondition; 36 Candidate = candidate; 37 Number = number; 38 IsUnique = false; 39 X = x; 40 Y = y; 41 } 42 43 public void SetNumber(byte? number) 44 { 45 Number = number; 46 } 47 48 public void SetCandidate(BitArray candidate) 49 { 50 Candidate = candidate; 51 } 52 53 public void SetUnique() 54 { 55 IsUnique = true; 56 } 57 }View Code
測試程式碼
View Code
總結
這個數獨求解器運用了大量 C# 7 的新特性,特別是 本地函式 和 基於 Tulpe 的簡寫的多返回值函式,能把本來一團亂的程式碼理清楚,寫清爽。 C# 果然是比 Java 這個躺在功勞簿上吃老本不求上進的坑爹語言爽多了。yield return 返回迭代器這種簡直是神仙設計,隨時想返回就返回,下次進來還能接著上次的地方繼續跑,寫這種程式碼簡直爽翻。另外目前多解求解功能還不可用,只是預留了集合返回型別和相關引數,以後看情況吧。
如果你看過我的這篇文章 .Net Core 3 騷操作 之 用 Windows 桌面應用開發 Asp.Net Core 網站 ,你也可以在釋出啟動網站後訪問 https://localhost/Sudoku 來執行數獨求解器,注意,除錯狀態下埠為5001。
轉載請完整保留以下內容,未經授權刪除以下內容進行轉載盜用的,保留追究法律責任的權利!
本文地址:
完整原始碼:Github
裡面有各種小東西,這只是其中之一,不嫌棄的話可以Star一下。