大作业

个人项目——数独

1. Github项目地址

Github: SuperSudoku
包括数独生成器、UI界面及文档部分
如果觉得对你有帮助的话请帮我打个星星呀

2. PSP耗时预估

PSP2.1Personal Software Process Stages预估耗时(分钟)
Planning计划10
· Estimate· 估计这个任务需要多少时间10
Development开发1400
· Analysis· 需求分析(包括学习新技术)120
· Design Spec· 生成设计文档60
· Design Review· 设计复审(和同事审核设计文档)/
· Coding Standard· 代码规范(为目前的开发指定合适的规范)20
· Design· 具体设计300
· Coding· 具体编码360
· Code Review· 代码复审60
· Test· 测试(自我测试,修改代码,提交修改)480
Reporting报告260
· Test Report· 测试报告120
· Size Measurement· 计算工作量20
· Postmortem & Process Improvement Plan· 事后总结,并提出过程改进计划120
合计1670

3. 解题思路描述

题目要求:实现一个数独程序,能够完成

  1. 生成不重复的数独终局至文件

  2. 读取文件内的数独问题,求解并将结果输出到文件

程序要求实现两种功能,生成数独终局和求解数独问题。
因为之前没有玩过数独,所以我先下载了一个数独游戏了解一下游戏规则:
数独盘面是9*9的网格,由9宫组成,每一宫又分为9个小格,在81格中给出一定的已知数字,玩家需要在其它空格上填入1-9的数字,使得1-9每个数字再每一行、每一列和每一宫中都只出现一次。

3.1 数独终局生成

  对于数独终局生成功能,我使用了两种方法实现,dfs深搜填充和模板法生成,因为题目要求不能有重复数独终局,所以不能采用随机化的方法,必须按一定规律生成数独。下面我对这两种方法进行介绍并比较它们的差异。

3.1.1 dfs回溯填充

  我首先想到的是dfs回溯,在每个小格中依次从1~9试填,每得到一个合法的可行解后就输出,然后回溯下一种情况,直到满足题目要求的生成数量,这种方法一定能保证生成无重复的合法终局。

  1. 我先试了从(1,1)填到(9,9),在填充过程中判断行列合法,生成终局后判断9宫的合法性。结果这种方法复杂度过高,无法得到答案。

  2. 在上一步dfs中进行优化剪枝,每生成3行,就进行一次宫合法性判断,不合法的情况直接舍弃。结果时间复杂度依旧过高,无法在时间接受范围内得到可行解。

  3. 经过分析,数独中每个3*3的宫内需要出现1~9所有的数,但从(1,1)dfs到(9,9)有很强的顺序性,而且对不合法的宫没有尽早剪枝,导致难以快速搜索到合法解。所以我调整了dfs的顺序,按照从1宫到9宫的顺序填充,可以剪枝掉所有不合法的情况,只对合法填充dfs。结果成功生成合法数独终局,时间复杂度也在可接受范围内,第一个功能初步完成。

3.1.2 模板法

  虽然dfs生成终局的时间满足基本要求,即个数在1-1000范围内不超过60s,但当个数达到1000000级别时,生成时间还是过长。经过查阅,我在网上看到一种模板生成法,生成数独终局效率较高。

模板法分为两步:

  1. 事先准备合法数独的模板矩阵,然后给不同的字母赋初值,得到初始数独,因为第一个空(1,1)由学号固定,所以共有 8 ! = 40320 8!=40320 8!=40320种初始数独矩阵

    modle

  2. 通过对初始数独矩阵的局部行列变换,可以产生 2 ! ∗ 3 ! ∗ 3 ! ∗ 2 ! ∗ 3 ! ∗ 3 ! = 72 ∗ 72 = 5184 2!*3!*3!*2!*3!*3!=72*72=5184 2!3!3!2!3!3!=7272=5184种数独终局

    modle_change

  3. 因为题目要求生成终局数量最多为1000000个,所以在本项目中,仅使用初始数独的行变换即可满足要求

  4. 关于模板法生成数独终局是否满足条件的证明

    • 合法性:由于模板是满足行列宫数字全包含且无重复的,所以初始数独一定是满足要求的。对于行列变换,整行整列交换一定不改变行列的合法性,并且由于是局部变换,仅针对每个3*3宫内进行变换,不同宫之间的变换是独立无关的,所以也不影响宫的合法性。
    • 无重复:对模板进行不同的字母数字映射,一定会得到不同的数独;对同一个初始数独,进行不同的行列变换,也一定能得到不同的数独终局;但对于不同映射的不同变换,是否存在两个不同的映射进过不同变换后得到相同的数独终局,我无法进行严格的数学证明。但我对生成的1000000个数独终局了进行两两暴力比较,验证了在1000000范围内,模板法生成的数独终局确实没有重复。
3.1.3 两种方法的比较
  • 时间效率:我对两种方法生成数独终局进行了时间效率测试,分别记录了在生成终局个数为10,100,1000,10000,100000和1000000时所需的时间,如下图(仅生成数独终局,不包含文件输出)。

    dfs&modle

  • 生成终局数量:dfs可以求得所有满足情况的终局数量,而模板法生成终局的数量有限,为 8 ! ∗ 2 ! ∗ 3 ! ∗ 3 ! ∗ 2 ! ∗ 3 ! ∗ 3 ! 8!*2!*3!*3!*2!*3!*3! 8!2!3!3!2!3!3!种,但在本项目中,已满足数量要求

3.2 数独求解

  关于数独求解问题,我使用了dfs深搜回溯寻找可行解的方法,后又在此基础上做了一些改进,提高了求解效率。

3.2.1 dfs深搜求解

  数独求解最直接的做法就是对于每个空格位置,遍历所有可以填的数,再依次递归求解下一格,直到找到一组满足条件的值为止。dfs时也可以做一些小优化,对于每个空格位置,找到该位置可以填的所有数,然后按照空格位置可填数的数量排升序,再依次搜索。对于每个空格位置可填数的查找,可以对其所在行、列、宫各设置一个数r,c,k,表示其行、列、宫已有的数(若包含这个数,则对应二进制位为1,否则为0),然后对r,c,k求或运算,结果中为0的位即时该空格可填的数。

3.2.2 唯一数确定

  dfs是计算机对于数独问题的暴力求解方式,但人求解数独时,有很多其他方法(唯一数确定,摒除法,唯一余数等多种高级技巧)。

我在数独爱好者论坛上对数独高级求解技巧有了一定的了解与学习。

  在本项目中,由于时间所限,我仅在dfs前进行了简单的预处理,先确定显性唯一数与简单隐性唯一数,循环遍历所有空格位置,直到可确定唯一数集没有更新为止。再调用之前实现的dfs程序求解。

3.2.3 两种方法的比较

  我使用同样的数独题目对两种方法进行测试比价,结果显示,加入唯一数预处理后,求解效率有了少量的提升,如下图(时间仅包含读入和求解数独,不包括输出),可能因为数独读入占时间比较重,所以时间效率上并没有明显的提升,但可以看出,当数独求解数量增大后,唯一数预处理还是能对效率的提升有一定的帮助。

pre&dfs

4. 设计实现过程

4.1 函数关系图

函数关系调用图如下:

function1
细节图:
function2

4.2 类关系图

主要类有两个,分别是数独终局类Generator和数独求解类Puzzle,相互独立。(类图由vs2017自动生成)

class

4.3 程序流程图

程序流程图如下:

flowchart

4.4 单元测试设计

单元测试主要分为以下几个部分:

  1. 输入合法性检测

  2. 数独能否成功生成

  3. 生成的数独合法性

  4. 数独能否正确求解

  5. 求解的数独合法性

具体测试用例将在后面单元测试设计模块给出。

4.5 代码质量分析

  在代码完成后,使用vs2017自带的代码分析功能进行了质量分析,改正了一些可能出现问题的警告,全部都是类的部分成员变量和成员函数没有初始化造成的。虽然这些变量会在后续的程序运行过程中进行赋值,但初始化符合代码规范,规避了日后可能因此出现异常情况的风险。

5. 性能分析及改进

  利用vs2017自带的性能分析工具对本项目进行分析,找出代码中的性能瓶颈并改进,提高程序运行效率。

5.1 性能分析

性能分析结果如下图所示:

analyze1
analyze2
analyze3
analyze4
analyze6

5.2 改进方法

由程序的性能分析可知,整个运行过程最耗时的部分就是IO。针对文件输入输出操作,我做了如下改进:

  1. 将数独的输入、输出及储存由int型改为了char型

  2. 输入输出函数由fgets,fprintf的文本形式改为fread,fwrite可以按块处理的二进制形式

  3. 由对于每个数独单独多次进行IO操作改为将输入输出一次读入到字符串中再进行处理

5.3 改进效果

对输入、输出改进后,运行效率有了明显的提升,如下图所示。

fwrite
fread

开启编译优化后的最终release版本运行时间如下:

generator
solution

6. 单元测试

为了进行有效单元测试,我在这里又对之前完成的代码做了些许调整,但不影响整体算法和设计架构。

6.1 单元测试用例

  本项目一共设置了10组单元测试用例,分别对输入参数的合法性、数独终局生成部分和数独终局求解部分进行了测试。
  为了测试输入参数的合法性,设置了test_input_flag变量,不同取值对应不同的输出提示。并将main函数部分独立出来,方便测试时的参数调用。
  数独终局生成部分,对生成的数独终局文件进行了测试,判断是否生成了符合数量要求的数独,并测试了数独的合法性和无重复性。
  数独求解部分,给定挖空的数独,对其求解,读取求解后的文件,判断其求解的合法性。
测试结果如下,全部通过:

test1

6.2 测试分支覆盖率指标

由vs自动生成的单元测试覆盖率达到92.90%,对程序各部分检测基本完成,符合预期。指标图如下:

test2

7. 关键代码说明

  1. 数独终局生成,通过全排列和行变换生成对数独模板进行映射,生成合法数独终局。
void Generator::Create()
{
    //学号9不变
    char firstrow[SIZE] = { "912345678" };
    int ord[SIZE];
    do {
        if (Create_exchange(ord, firstrow)) break;
    } while (std::next_permutation(firstrow + 1, firstrow + 9));
}
bool Generator::Create_exchange(int ord[], char firstrow[])
{
    char per1[2][4] = { "123", "132" },
        per2[6][4] = { "456", "465", "546", "564", "645", "654" },
        per3[6][4] = { "789", "798", "879", "897", "978", "987" };
    for (int i = 0; i < 2; i++) {
        for (int ii = 0; ii < 3; ii++) ord[ii] = per1[i][ii] - '0';

        for (int j = 0; j < 6; j++) {
            for (int jj = 0; jj < 3; jj++) ord[jj + 3] = per2[j][jj] - '0';

            for (int k = 0; k < 6; k++) {
                for (int kk = 0; kk < 3; kk++) ord[kk + 6] = per3[k][kk] - '0';

                num--;
                Getchessboard(ord, firstrow);
                if (!num)
                    return true;
            }
        }
    }
    return false;
}

2.数独求解确定唯一数空格,通过对只能填一个数的空位进行判断,先填确定空格,降低dfs的复杂度。

//填写唯一解空格
while (!que.isEmpty()) {
    tmp = que.front(); que.pop();

    if (tmp.k == 0) { //一轮标记
        if (flag) {
            flag = false;
            que.push(tmp);
        }
        else
            break;  //无唯一确定方格
    }
    else {
        int result = row[tmp.r] | column[tmp.c] | sub[tmp.k];
        if (Num1(result) == 8) {  //找到一个唯一确定方格
            int sure_num = getNum(result);
            int shift = sure_num - 1;
            puzzleboard[tmp.r][tmp.c] = sure_num + '0';

            flag = true;
            empty_num--;
            column[tmp.c] |= 1 << shift;
            row[tmp.r] |= 1 << shift;
            sub[tmp.k] |= 1 << shift;
        }
        else {  //重新加入队列
            que.push(tmp);
        }
    }
}

8. UI界面

  本项目的UI界面基于qt5.12.0开发,与数独生成程序为两个独立的项目,但数独生成算法是相同的。

8.1 界面预览

UI界面如下:

level_2

8.2 数独生成

  先生成一个数独终局,算法与数独生成器相同,对于挖空部分,保证每宫内至少有2个空格,整个数独盘的空格数为30~60,其中,又根据数独等级进行了数量上具体的划分,为保证游戏质量,空格生成时按照上一宫的数量进行动态修改,具体实现如下(以中级难度为例):

while (sum < 30 || sum > 60) {
    sum = 0;
    empty[1] = rand() % 5 + 4; //第一宫随机生成4~9个空格
    sum += empty[1];
    for (int i = 2; i <= 9; i++) {
        if (empty[i - 1] >= 6)  //上一宫空格过多,本宫生成4~6个空格
            empty[i] = rand() % 3 + 4;
        else                    //上一宫空格过少,本宫生成6~9个空格
            empty[i] = rand() % 4 + 6;
        sum += empty[i];
    }
}

8.3 玩法介绍

  1. 打开游戏,自动生成一个难度为中级的数独游戏

  2. 点击空格位置,通过选择下方数字或在键盘上输入这两种方式,进行填数
    clickclick2

  3. 预置数和用户填写数通过字体进行区分
    different

  4. 不符合填写规则时,将以红色警示,更改为合法数后复原
    redredchange

  5. 难度选择。游戏共设置了3个难度,分为初级、中级和高级,用户可进行选择
    在这里插入图片描述
    下图依次为初级、中级和高级(按照空格数量划分)
    level1level2level3

  6. 游戏完成后“成功”提示
    success

  7. 同时游戏也设置了清除,新游戏与计时功能,以提升游戏体验

9. PSP实际耗时

PSP2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划1020
· Estimate· 估计这个任务需要多少时间1020
Development开发14001540
· Analysis· 需求分析(包括学习新技术)120180
· Design Spec· 生成设计文档6060
· Design Review· 设计复审(和同事审核设计文档)//
· Coding Standard· 代码规范(为目前的开发指定合适的规范)2010
· Design· 具体设计300360
· Coding· 具体编码360300
· Code Review· 代码复审6030
· Test· 测试(自我测试,修改代码,提交修改)480600
Reporting报告260270
· Test Report· 测试报告120120
· Size Measurement· 计算工作量2030
· Postmortem & Process Improvement Plan· 事后总结,并提出过程改进计划120120
合计16701830

10. 总结

10.1 收获

  在本项目的完成过程中,我学习到了很多新知识,初步接触到软件项目开发的基本流程,也对软件需求分析、软件设计、代码编写、测试等几个模块有了深入的理解。

  1. 新工具的学习与使用。我学习到了git的使用,其版本信息记录的功能在开发过程中给予了我许多帮助。代码质量分析、性能测试及单元测试的使用也帮助我找到程序中存在的不合理部分,并做出进一步改进。在界面设计中,我第一次学习了qt的使用。

  2. 认识到需求分析的重要性。刚拿到题目时急于编码,没有认真进行需求分析,仔细阅读要求,导致在项目初期浪费了不少时间。

  3. 对面向对象程序设计的认识。本项目采用了c++面向对象的程序设计方法,模块化的设计方法使代码结构更加清晰。

  4. 对不同输入输出函数有了深入的理解。由于本项目运行时间的瓶颈主要在IO上,所以为了提高运行效率,我对不同的输入输出方式进行了学习和测试比较,使用较快的方式提升了程序性能。

10.2 不足

  由于时间限制,本项目的数独求解部分了采用了较为简单的暴力搜索求解法,运行效率较低,日后将对求解数独的更好方法(例如,舞蹈链算法)进行研究实现,以对本项目进行完善。
  界面部分,生成的数独游戏不能保证唯一解,对于空格的生成算法还需进一步研究。此外,还可以增加保存、提示、记录等功能,有待进一步完善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值