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
测试代码
总结
这个数独求解器运用了大量 C# 7 的新特性,特别是 本地函数 和 基于 Tulpe 的简写的多返回值函数,能把本来一团乱的代码理清楚,写清爽。 C# 果然是比 Java 这个躺在功劳簿上吃老本不求上进的坑爹语言爽多了。yield return 返回迭代器这种简直是神仙设计,随时想返回就返回,下次进来还能接着上次的地方继续跑,写这种代码简直爽翻。另外目前多解求解功能还不可用,只是预留了集合返回类型和相关参数,以后看情况吧。
如果你看过我的这篇文章 .Net Core 3 骚操作 之 用 Windows 桌面应用开发 Asp.Net Core 网站 ,你也可以在发布启动网站后访问 https://localhost/Sudoku 来运行数独求解器,注意,调试状态下端口为5001。
转载请完整保留以下内容,未经授权删除以下内容进行转载盗用的,保留追究法律责任的权利!
完整源代码:Github
里面有各种小东西,这只是其中之一,不嫌弃的话可以Star一下。