65.9K
CodeProject 正在变化。 阅读更多。
Home

具有一些高级功能的数独程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (21投票s)

2015 年 1 月 13 日

CPOL

7分钟阅读

viewsIcon

32937

downloadIcon

1962

Sudoku 是一个用 C# 编写并带有 WPF 用户界面的程序。

 

引言

Sudoku 是一个用 C# 编写并带有 WPF 用户界面的程序。它可以生成 9x9 或 16x16 的数独棋盘,难度级别分为简单、中等或高级。我多年前就编写了它,最后决定发布,因为其中包含了一些在其他帖子中找不到的功能。

特点

这款数独程序特色

  • 9x9 或 16x16 棋盘,不同难度级别
  • 解决任何 9x9 或 16x16 的数独谜题
  • 可自定义字体和颜色
  • 可自定义用于表示值的符号(数字、字母或符号)
  • 输入模式选择:单元格切换或数值选择器
  • 输入值模式:仅接受有效值或接受任何值
  • 打印数独棋盘(当前棋盘或批量打印)
  • 设计你自己的数独(设计模式)
  • 保存或加载数独棋盘
  • 撤销/重做功能
  • 不同类型的提示
  • 等等。

Image1

                                   使用符号而非数字的数独棋盘

                                   使用字母而非数字的 16x16 数独棋盘

版本控制

版本 2.01

原始发布版本。

 

提供演示

提供的演示是带有 WPF 界面的 C# 数独程序。

提供源码

除了提供的演示源码外,我还包含了一个使用 Windows Forms 编写的该程序早期版本。此后者版本不受支持。

需要改进的地方?

    - 如果能够注释棋盘就好了。
    - 我在其他数独游戏中看到了有趣的输入方法,可以添加到此版本中。
    - 难度级别可以更精细。
    - 在游戏中添加帮助文件会很受欢迎。

源码说明

项目中包含的主要类有

引擎类

  • CellPos: 单元格位置 (x,y)
  • SudBoard: 实现数独棋盘的抽象类
  • SudBoard9: 9x9 棋盘的 SudBoard 实现
  • SudBoard16: 16x16 特定功能的 SudBoard 实现
  • SudBoardGenerator: 同步或异步生成 9x9 或 16x16 棋盘
  • ValCount: 实现计数器的抽象类,用于计算某类型区域(行、方块或列)的每个可能值的数量
  • ValCount9: 9x9 棋盘的 ValCount 实现
  • ValCount16: 16x16 棋盘的 ValCount 实现

WPF 和用户界面类

  • app: 应用程序
  • dlgAbout: 关于框
  • dlgBoardAppearance: 用于更改绘制界面的字体和颜色的对话框
  • dlgGenerateABoard: 处理棋盘生成的对话框
  • SudControl: 继承自 UserControl 的可视化控件,包含一个数独棋盘 (SudBoard)
  • ValPickerControl: 实现用于选择要播放的值的数值选择器。由 SudControl 使用
  • wndMain: 主窗口

1. 数据结构

值存储在一维数组 int[] m_arrBoardVal 中。使用一维数组而不是二维数组可以显著提高性能。数组中的值范围是 0 到 9(对于 9x9 棋盘)或 0 到 16(对于 16x16 棋盘)。值 0 表示一个空单元格。数组的第一个值用于存储左上角单元格的值,最后一个值用于存储右下角单元格的值。

        存储在一维数组 ValueTypeE[] m_arrBoardValType 中的值的范围

None (0) 空单元格
Fix (1) 固定值。用户无法更改
User (2) 用户可以更改的值

ValCount 类旨在计算行、列和方块中每个可能值的计数。每次更新单元格值时,都会相应更新该单元格所在行、列和方块的 ValCount。

m_LineValCount 记录每行每个可能值的计数
m_ColValCount 记录每列每个可能值的计数
m_SqrValCount 记录每个方块每个可能值的计数

程序需要将线性位置转换为行/列和方块位置。这些转换使用非常频繁,必须进行优化(尤其是在 16x16 棋盘上)。在 16x16 棋盘上的转换是通过乘以 16 和取模 16 来完成的。这些操作是使用位移和 '&' 运算符完成的。这就是为什么存在 SudBoard9/16 和 ValCount9/16 类。

从方块位置到线性位置的转换是通过这两个数组完成的

m_arrPosToSqr 线性位置 -> 方块位置
m_arrSqrToPos 方块位置 -> 方块的第一个线性位置

数独棋盘类支持撤销或重做移动。为此,两个堆栈用于存储可以撤销或重做的移动

Stack<Move> m_stackUndoMoves
Stack<Move> m_stackRedoMoves

2. 移动

当输入的值在其列、行和方块中唯一时,该移动被定义为有效。

更改单元格值的最主要方法是 PlayAt。该方法更新 m_arrBoardVal 和 m_arrBoardValType 数组。它更新相应的方块、行和列的 ValCount 实例。如果指定,会更新撤销和重做堆栈。

3. 求解棋盘

给定的数独棋盘可能没有解、有一个解或有多个解。求解方法必须能够找到棋盘的解,并指明是否存在多个解。

简单的回溯法足以求解 9x9 棋盘。

            while No Solution Found
                Find the first empty cell
                For each possible value in this cell
                    Play it
                    Call recursively
                    If Solved
                        Solution Found, exit
                    Else
                        Undo the move
                    EndIf
                Next
                Oops, bad board!
            wend

然而,使用这种简单的方法来求解 16x16 棋盘速度太慢(至少在生成数独棋盘时需要多次调用 Solve 方法)。更好的方法是使用改进的回溯法。

            do {
                Fill the obvious cells
                If solution not found,
                    Find the first empty cell
                    For each possible value in this cell
                        Play it
                        Call recursively
                        If solve
                            Solution Found, exit
                        Else
                            Undo the move
                        EndIf
                    Next
                    Oops, bad board!
                EndIf
            } while No solution found

进一步提高性能的一种方法是提前计算可能移动的列表。问题现在是如何实现“填充显而易见单元格”。在此上下文中,“显而易见”意味着

  • 在一个方块、一行或一列中只能在一个单元格中播放的值。
  • 在一个单元格中只能播放一个值的单元格。
FillListOfValidValuesForEmptyCells 填充一个数组,其中包含可能移动的列表
FillValueWhichCanBePlayedInSingleCellOfAZone 填充只能在一个行、列或方块中播放值的单元格。
FillObviousCells 填充所有显而易见的值
SolveBacktrack 改进的回溯方法
Solve 求解棋盘

4. 生成数独棋盘

此棋盘生成器的不同目标是

  • 生成一个有效的、只有一个解的棋盘。
  • 生成 9x9 或 16x16 棋盘。
  • 生成不同难度的棋盘(简单、中等和困难)。

计算机使用三种策略来求解棋盘

  1. 用只能在一个方块、一行或一列中播放的值填充单元格。
  2. 填充只有一个值可以播放的单元格。
  3. 暴力破解(一个接一个地尝试排列)

对人类来说,第一种方法最简单,第二种更难,第三种最难。一个简单的棋盘只需要第一种方法就能解决。中等难度的棋盘需要前两种方法,而困难的棋盘则需要三种方法来解决。

该程序生成棋盘的通用方法是

  1. 生成一个完整的棋盘
  2. 逐个删除值,只要
  • 棋盘只有一个解决方案
  • 只使用与难度级别兼容的策略

棋盘生成由 SudBoardGenerator 类完成。棋盘可以在主线程或工作线程上生成。

4.1 生成完整棋盘

可以使用简单的回溯方法填充棋盘

            Fill(iPos)
                If Board Filled,
                    Return
                ForEach Value
                    If Value is valid at this position
                        Play it
                        If Fill(iPos+1) = Filled
                            Return
                        Else
                            Undo the move
                        EndIf
                    EndIf
                Next
                <Must never get here>

这种方法总是会生成相同的棋盘。为了实现随机棋盘生成,我们只需打乱填充位置的顺序。

总的来说,使用这种方法生成完整棋盘速度很快。但是,有时某些 16x16 棋盘可能需要很长时间才能完成。发生这种情况时,取消此生成并重新开始会更快。

4.2 削减棋盘(挖洞)

一旦生成了完整的棋盘,我们只需从随机单元格中删除值。

ForEach Pos in Random Order
    Set the position value to 0
    If Solve doesn't give 1 solution or used strategies not compatible with the expected difficulty level
        Restore the value
    EndIf
Next

在 16x16 棋盘上,此例程可能需要相对较长的时间。因此,可以提供超时。如果达到超时,棋盘仍然有效,但会包含更多已完成的单元格。

5 数独控件

SudControl 实现了一个继承自 System.Windows.Controls.UserControl 的可视化控件。它用于玩数独。

一个具有一些高级功能的数独程序 - CodeProject - 代码之家
© . All rights reserved.