资讯详情

资讯详情

C#数独源码实战:随机出题与自动解题的完整实现

简介这是一份面向C#初学者与算法入门者的数独游戏源码基于9x9网格与3x3小九宫格规则实现支持随机出题与自动解题两大核心功能代码结构简洁适合用来练习面向对象编程与基础算法。资源包共47个文件以12个cs源码文件为主体配合resx与resources资源文件、config配置、sln与csproj工程文件以及exe、dll、pdb等编译产物整体约95KB体量轻便便于直接打开工程阅读与调试。目前已有956人学习下载说明其在入门实践场景中具有一定参考价值。读者可从中了解数独网格类与单元格类的划分方式、随机出题时生成完整解再挖空的思路以及深度优先搜索配合候选数字列表、回溯与剪枝的解题流程还可借鉴LINQ对行列与九宫格进行规则校验的写法进而尝试优化搜索策略提升编程与问题解决能力。1. 一份能跑起来的 C# 数独源码随机出题与自动解题到底怎么落地很多人第一次接触数独程序是在刷算法题的时候——LeetCode 上那道「解数独」写完回溯就扔了真到要做一个能随机出题、能自动求解、还能在 WinForm 上点着玩的完整小工具时反而不知道从哪下手。这份 C# 数独源码就是冲着这个场景来的它把「生成合法终盘 → 随机挖洞出题 → 回溯求解 → 界面渲染」这条链路完整串了一遍代码量不大结构清晰适合拿来当 C# 入门到进阶之间的一个综合练习。它解决的不是「数独算法有多难」的问题而是「一个能跑、能改、能看懂的小项目长什么样」的问题。适合刚学完 C# 基础语法、想找一个有实际逻辑的练手项目的人也适合想快速拿到一份可运行源码、在此基础上改造成自己版本的人。下面我按「先看懂结构、再动手跑、最后避坑」的顺序拆一遍。2. 源码结构与核心类拆解从 Sudoku.sln 到 SudokuPanel.cs拿到压缩包解压后第一件事不是急着双击 .sln而是先看清楚目录里有什么。这份源码的目录结构是典型的 Visual Studio WinForm 工程布局没有多余的第三方依赖这意味着你不需要配 NuGet 源、不需要装额外 SDK只要本机有对应版本的 .NET 开发环境就能编译。2.1 工程文件清单与职责划分先把根目录下的文件按职责分个类心里有数再动手文件/目录类型职责Sudoku.sln解决方案文件VS 入口双击打开整个工程Sudoku/Sudoku.csproj项目文件定义目标框架、编译项、资源引用Sudoku/Program.cs入口Main 方法启动主窗体Sudoku/FrmSudoku.cs窗体逻辑按钮事件、出题/解题触发、状态更新Sudoku/FrmSudoku.Designer.cs设计器生成控件布局、初始化代码一般不改Sudoku/FrmSudoku.resx资源窗体资源含图标等Sudoku/SudokuPanel.cs自定义控件9x9 网格的绘制与交互核心Sudoku/Sudoku.cs算法类出题、求解、校验逻辑Sudoku/App.config配置运行时配置通常保持默认Sudoku/Properties/属性AssemblyInfo 等元数据Sudoku/bin、obj输出编译产物可删可重建9.ico图标窗体图标资源这个划分里最关键的是三个文件Sudoku.cs负责算法SudokuPanel.cs负责把二维数组画成能点的格子FrmSudoku.cs负责把两者粘起来。很多人看源码容易一头扎进 Designer 文件里那里面全是自动生成的坐标和属性看了也没用直接跳过。2.2 二维数组与单元格状态的设计数独盘面在代码里最自然的表示就是一个 9x9 的二维整型数组0 表示空格1 到 9 表示已填数字。但光有数字不够界面要区分「题目给定的数字」和「玩家填的数字」否则解题后你没法知道哪些是原始线索。常见做法是再加一个布尔二维数组isFixed或者用一个枚举标记每个格子的来源。// Sudoku.cs 中盘面数据的典型定义 private int[,] board new int[9, 9]; // 0 表示空格 private bool[,] isFixed new bool[9, 9]; // true 表示题目给定不可修改 // 判断在 (row, col) 填入 num 是否合法 private bool IsValid(int row, int col, int num) { for (int i 0; i 9; i) { // 检查行和列 if (board[row, i] num || board[i, col] num) return false; } // 检查所在 3x3 小九宫格 int startRow row / 3 * 3; int startCol col / 3 * 3; for (int r startRow; r startRow 3; r) for (int c startCol; c startCol 3; c) if (board[r, c] num) return false; return true; }这段IsValid是整个程序的校验基石出题时的挖洞验证、解题时的候选判断、玩家手动填数时的即时反馈全都调它。参数含义很直白row、col是零基索引num是待填入的 1 到 9。注意小九宫格起点用row / 3 * 3这种整数除法来算这是数独代码里最常见的写法比查表更省事。如果你要改成 16x16 的数独这里的分母和循环上界都要跟着改不是简单改个常量就完事。2.3 随机出题的生成与挖洞策略随机出题分两步先生成一个完整合法终盘再随机挖掉若干数字形成题目。生成终盘最稳的办法还是回溯——从左上角开始逐格尝试填 1 到 9配合随机打乱候选顺序这样每次生成的终盘都不一样。// 生成一个完整合法终盘 private bool GenerateFullBoard(int row, int col) { if (row 9) return true; // 填满即成功 int nextRow col 8 ? row 1 : row; int nextCol col 8 ? 0 : col 1; int[] candidates Enumerable.Range(1, 9).OrderBy(x Guid.NewGuid()).ToArray(); foreach (int num in candidates) { if (IsValid(row, col, num)) { board[row, col] num; if (GenerateFullBoard(nextRow, nextCol)) return true; board[row, col] 0; // 回溯 } } return false; }这里用Guid.NewGuid()排序来做洗牌是 C# 里比较省事的随机打乱写法比手写 Fisher-Yates 短但性能略差对 9x9 完全够用。挖洞阶段则是在终盘基础上随机选格子置零每挖一个就用求解器验证是否仍有唯一解——这一步是很多简易实现偷懒的地方如果不验证唯一解玩家可能遇到多解题目体验直接翻车。3. 自动解题的回溯实现DFS、剪枝与候选数优化自动解题是这份源码里最值得细看的部分。摘要里提到用深度优先搜索配合候选数字列表实际代码里通常就是回溯法的标准写法但写得好不好差别在剪枝和候选顺序上。3.1 基础回溯求解器最朴素的做法是从第一个空格开始依次尝试 1 到 9合法就填、递归、失败就回退。这个版本代码最短但遇到极端盘面会慢。// 基础回溯求解 public bool Solve() { for (int row 0; row 9; row) { for (int col 0; col 9; col) { if (board[row, col] 0) // 找到第一个空格 { for (int num 1; num 9; num) { if (IsValid(row, col, num)) { board[row, col] num; if (Solve()) return true; // 递归求解 board[row, col] 0; // 回溯 } } return false; // 1-9 都试过无解 } } } return true; // 没有空格求解完成 }逻辑说明外层双重循环负责定位空格内层循环负责试数。IsValid保证当前填入不冲突递归返回 true 说明后续全部填完直接向上传递成功信号返回 false 则把当前格清空继续试下一个数。参数上没有任何可调项它的效率完全取决于盘面难度。对于普通难度的数独这个版本毫秒级出结果对于接近空盘的极端情况可能会卡顿。3.2 候选数列表与最少候选优先剪枝要让求解器稳一点常见做法是每次不选「第一个空格」而是选「候选数最少的空格」来填。这能大幅减少无效分支是数独求解里性价比最高的优化。// 找出候选数最少的空格返回其候选列表 private (int row, int col, Listint candidates) FindBestCell() { int bestRow -1, bestCol -1; Listint bestCandidates null; for (int row 0; row 9; row) { for (int col 0; col 9; col) { if (board[row, col] ! 0) continue; var candidates new Listint(); for (int num 1; num 9; num) if (IsValid(row, col, num)) candidates.Add(num); if (bestCandidates null || candidates.Count bestCandidates.Count) { bestRow row; bestCol col; bestCandidates candidates; if (candidates.Count 1) return (bestRow, bestCol, bestCandidates); } } } return (bestRow, bestCol, bestCandidates); }这个函数返回候选数最少的空格及其候选列表求解器改成先调它、再遍历候选。参数上candidates.Count 1时直接返回是因为唯一候选无需再比较属于提前退出。把Solve里的「找第一个空格」换成「找最优空格」代码改动不大但求解速度在难题上能差出几倍。这也是从「能解」到「解得快」的关键一步。3.3 出题唯一性验证的调用方式挖洞出题时每挖一个格子都要验证剩余盘面是否唯一解。做法是先复制当前盘面调求解器数解的个数超过 1 就说明挖多了把格子填回去。// 统计解的个数用于唯一性验证 private int CountSolutions(int[,] grid, int limit 2) { // 找到第一个空格 for (int row 0; row 9; row) for (int col 0; col 9; col) if (grid[row, col] 0) { int count 0; for (int num 1; num 9; num) { if (IsValidOn(grid, row, col, num)) { grid[row, col] num; count CountSolutions(grid, limit - count); grid[row, col] 0; if (count limit) return count; // 剪枝够 limit 就停 } } return count; } return 1; // 没有空格说明找到一个解 }参数limit是提前终止阈值默认 2 表示只要发现第二个解就立刻返回不用数完所有解。这个剪枝在出题阶段非常重要否则每挖一个洞都完整数一遍解出题会慢到没法用。注意这里用的是独立的IsValidOn和传入的grid不能直接复用成员变量上的board否则会污染当前盘面。4. 编译运行与界面交互从 .sln 到可点击的 9x9 网格算法看懂了接下来要让它真正跑起来。这一章讲环境准备、编译步骤以及SudokuPanel是怎么把二维数组变成可交互网格的。4.1 开发环境准备与编译步骤这份工程是 WinForm 项目只能在 Windows 上跑用 Wine 或跨平台方案不在本文讨论范围。环境准备按下面几步走安装 Visual Studio勾选「.NET 桌面开发」工作负载这会带上 WinForm 所需的全部组件。确认目标框架。打开Sudoku.csproj看TargetFramework节点常见是net48或net6.0-windows这类。如果本机没装对应框架VS 会提示你改目标或装运行时。双击Sudoku.sln等待解决方案加载完成。在解决方案资源管理器里右键Sudoku项目选择「设为启动项目」。按 F5 或点「启动」按钮编译运行。如果编译报错集中在FrmSudoku.Designer.cs多半是设计器版本差异导致的可以先尝试「重新生成解决方案」仍然报错就检查.csproj里的目标框架是否和本机 SDK 匹配。bin和obj目录可以直接删掉重新生成不影响源码。4.2 SudokuPanel 的绘制与鼠标交互SudokuPanel.cs是一个继承自Panel或Control的自定义控件核心工作是重写OnPaint画格子和数字重写鼠标事件处理点击。// 绘制 9x9 网格与数字简化示意 protected override void OnPaint(PaintEventArgs e) { Graphics g e.Graphics; int cellSize this.Width / 9; // 假设控件为正方形 for (int row 0; row 9; row) { for (int col 0; col 9; col) { Rectangle rect new Rectangle(col * cellSize, row * cellSize, cellSize, cellSize); g.DrawRectangle(Pens.Black, rect); // 画格子边框 if (board[row, col] ! 0) { // 题目给定用黑色玩家填入用蓝色 Brush brush isFixed[row, col] ? Brushes.Black : Brushes.Blue; g.DrawString(board[row, col].ToString(), this.Font, brush, rect, new StringFormat { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center }); } } } // 加粗 3x3 小九宫格的分隔线 for (int i 0; i 9; i 3) { g.DrawLine(new Pen(Color.Black, 2), i * cellSize, 0, i * cellSize, 9 * cellSize); g.DrawLine(new Pen(Color.Black, 2), 0, i * cellSize, 9 * cellSize, i * cellSize); } }逻辑说明cellSize由控件宽度除以 9 得到所以控件本身要保持正方形否则格子会变形。数字用DrawString居中绘制StringFormat负责水平和垂直居中。最后那个循环每隔 3 格画一条粗线把 9x9 分成 9 个 3x3 区域这是数独界面的视觉刚需少了它玩家很难看清宫格边界。鼠标点击则通过MouseDown事件把坐标换算成行列索引再更新选中状态并触发重绘。4.3 出题与解题按钮的事件串联FrmSudoku.cs里的按钮事件是算法和界面的粘合层逻辑不复杂但顺序要对// 「出题」按钮 private void btnGenerate_Click(object sender, EventArgs e) { sudoku.Generate(); // 生成终盘并挖洞 sudokuPanel.Bind(sudoku.Board, sudoku.IsFixed); // 绑定数据 sudokuPanel.Invalidate(); // 触发重绘 } // 「解题」按钮 private void btnSolve_Click(object sender, EventArgs e) { if (sudoku.Solve()) { sudokuPanel.Invalidate(); // 求解成功刷新界面 } else { MessageBox.Show(当前盘面无解请检查输入。); } }参数和调用顺序说明Generate内部完成终盘生成和挖洞Bind把盘面数据和固定标记传给控件Invalidate通知系统重绘。解题按钮先调Solve成功才刷新失败给提示。这里有个容易忽略的点——如果玩家手动改过格子Solve会直接在当前盘面上继续解所以「解题」前最好确认盘面没有冲突数字否则会得到无解提示。5. 避坑与常见问题排查那些让程序跑不起来的细节这份源码整体不复杂但实际动手时踩坑的地方还挺集中。下面几条是我自己复现和帮人排查时遇到最多的按「现象 → 原因 → 解决」列出来。现象一编译通过但运行时报「未将对象引用设置到对象的实例」。原因通常是SudokuPanel在窗体初始化时还没绑定盘面数据OnPaint里直接访问了空的board数组。解决方式是在控件构造函数里先初始化一个全零的 9x9 数组或者在OnPaint开头加空值判断数据没绑定时只画网格不画数字。现象二出题按钮点下去界面卡住几秒。原因是挖洞阶段每个格子都完整数了一遍解没有加提前终止。解决方式是给CountSolutions加limit参数发现第二个解就立刻返回同时挖洞数量设一个上限不要无限制挖到接近空盘。现象三解题结果和预期不符或者提示无解。多半是盘面里存在冲突数字——玩家手动填了一个和同行/同列/同宫冲突的数求解器从冲突盘面出发自然无解。解决方式是在解题前先跑一遍全盘校验发现冲突就高亮提示而不是直接丢给求解器。现象四格子点击后数字显示位置偏移。原因是cellSize用整数除法算的控件宽度不能被 9 整除时会有余数累积到右边和下边就偏了。解决方式是让控件宽度固定为 9 的倍数或者用浮点计算格子边界再取整绘制时统一用同一套坐标。现象五换台机器打开 .sln 提示目标框架不受支持。原因是.csproj里写的目标框架本机没装。解决方式是右键项目 → 属性 → 目标框架改成你本机已安装的版本或者去装对应版本的 .NET 运行时/SDK。改完记得清理bin、obj再重新生成。6. 进阶改造与验证把这份源码变成你自己的数独工具源码能跑通之后真正有意思的是按自己的需求改。这里给几个可落地的改造方向和验证方法都是在这份代码基础上小步改动就能见效的。第一个方向是难度分级。现在的挖洞数量是固定的或随机的你可以按挖洞数分三档简单留 40 个左右已知数中等留 30 个困难留 25 个以下。改法是在Generate里加一个难度参数控制挖洞循环的终止条件。验证方法是连续出 20 题用求解器反解一遍确认每题都是唯一解且难度符合预期。第二个方向是加计时和错误计数。在FrmSudoku里放一个Timer控件出题时启动、解题成功时停止界面上显示用时玩家每填一个和正确答案不符的数就累加错误次数。这里要注意判断「和正确答案不符」需要你在出题时把完整终盘存一份副本不能只存挖洞后的题目。第三个方向是把求解器换成更高效的版本。前面讲的「最少候选优先」已经能应付绝大多数盘面如果你想再进一步可以研究舞蹈链Dancing Links算法它在处理极端数独时优势明显。不过对 9x9 标准数独来说优化过的回溯已经足够上舞蹈链更多是学习价值。验证改造是否成功我一般会走一套固定流程先跑 100 次随机出题统计平均耗时和唯一解比例再拿几道公认的困难题手动输入看求解器能否秒解最后把界面在不同 DPI 缩放下过一遍确认格子不变形、数字不重叠。这套流程走下来基本能覆盖大部分回归问题。说个我自己的习惯每次改完算法相关代码我都会先写一个临时的控制台测试入口把出题和求解各跑一千次看有没有异常和性能退化确认没问题再接回 WinForm 界面。这个习惯帮我省过好几次「界面点着没事、批量跑就崩」的后悔药。希望这份拆解能帮你把这份 C# 数独源码真正用起来改出属于自己的版本。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →