【软件工程基础个人项目】一个数独终局生成和求解的控制台程序

Github 项目地址

Github: acromema/sudoku

任务

实现一个能够生成数独终局并能求解数独问题的控制台程序。

时间预估

PSP 2.1Personal Software Process Stages预估耗时(min)实际耗时(min)
Planning计划30
Estimate估计这个任务需要多少时间3000
Development开发1500
Analysis需求分析(包括学习新技术)200
Design Spec生成设计文档60
Design Review设计复审(和同事审核审计文档)30
Coding Standard代码规范(为目前的开发制定合适的规范)30
Design具体设计120
Coding具体编码500
Code Review代码复审60
Test测试(自我测试,修改代码,提交修改)400
Reporting报告30
Test Report测试报告30
Size Measurement计算工作量10
Postmortem & Process Improvement Plan事后总结,并提出过程改进计划60
合计3000

需求分析

本程序功能比较单一,并且两个功能模块各自独立,输入、输出格式和要求也已经给出,因此需求分析工作比较简单。

模块划分

程序从命令行得到命令与参数,并根据命令实现两个功能,因此把程序初步划分为以下模块:

  • 命令判断与处理
    从命令行获得命令后,判断命令类型是生成命令还是解决命令,并检查参数是否正确(如生成数独的参数必须是1-1e6范围内的数字),参数正确则调用并传参给相应的模块。
  • 生成数独
    生成指定数量的数独终局,并按格式写入 sudoku.txt 文件。
  • 解决数独
    从指定的路径读入待解决的数独题目,将可行解按格式写入 sudoku.txt 文件。

功能建模

通过数据流图来进行功能建模。

顶层图:
顶层图
第 1 层图:
第 1 层图

解题思路描述

生成算法思路

生成算法参考了 xxrxxr的博客 ,以 1 个终局为模版,通过以下两种方式生成新的终局:

  • 数字的交换
    因为当前的终局已经满足数独条件,且数字的交换并不会破坏数字间的位置关系,因此可以通过数字的交换来生成其他终局。
    第 1 行第 1 个数字固定为学号末两位模 9 加 1 ,因此只能交换剩下 8 个数字,可以生成8! = 40,320种终局。
  • 行的交换
    数独终局 1-3、4-6、7-9 行之间可以交换,且不破坏数独条件,因此可以通过行的交换生成其他终局。
    第 1 行第 1 个数字固定为学号末两位模 9 加 1 ,因此只能交换 2-3、4-6、7-9行,可以生成2! * 3! * 3! = 72种终局

两种方式结合,共生成 8! * 2! * 3! * 3! = 2,903,040 种终局,满足最大要求 1e6 。

求解算法思路

参考 暴力算法之美:如何在1毫秒内解决数独问题?| 暴力枚举法+深度优先搜索 POJ 2982
基本求解思路是暴力枚举深度优先搜索。但是由于求解的数独数目最高可以达到 1,000,000 个,且空白数目较多,因此需要对算法进行优化。

优化思路是将数独中的空白按照可填数字数目从低到高的顺序进行排序,优先选择可填数字少的格子,可以减少大量递归调用函数自身时间。

由于每行每列的数字都不相同,且这是一个 9 x 9 的数独,因此每个格子可填的数字数目就是
(9 - max ( 所在行已填数字数目 , 所在列已填数字数目 )
更精准的优化可以通过每个 3 x 3 的小宫格来优化,优化后每个格子可填的数字数目就是
(9 - max ( 所在行已填数字数目 , 所在列已填数字数目,所在宫格已填数字数目)

设计实现

按照功能建模环节的规划,整个程序大体分为3个部分:

  • 命令判断与处理
  • 解决数独
  • 生成数独

其中,命令判断与处理集成在主函数中,因此除主函数之外,还有两个主要的函数:
解决数独函数SolveSudoku()
生成数独函数CreateSudoku()

函数调用关系图如下:FunctionRelationship

性能优化

第一个版本生成 1,000,000 个数独,在 macOS 下运行时间 57 s ,在 Windows 10 环境下运行时间 577 s
通过 Visual Studio 2017 的性能分析工具进行分析:
Version 1.0  Analysis Result
Version 1.0  Analysis Result
发现耗时最多的主要是写入文件的部分,包括

  • std:endl : 插入换行符并且 冲入输出序列。
  • std::basic_filebuf : 函数 underflow()overflow()sync() 进行文件和缓冲区的获取放置区之间的实际 I/O 。

第一个版本的 I/O 方式是在开始生成数独终局前,打开一个文件,每生成一行写入一次。
查询 cppreference.com ,结合代码分析出性能瓶颈主要为以下两点:

  • 每行结束都插入换行符,std::endl操纵符插入换行之后都会 flush() ,由于采用的是生成一行写入一行的 I/O 方式,所以频繁 flush() 耗费较多时间。
  • 生成一行写入一行的 I/O 方式导致 std::basic_filebuf 为了维护文件位置会对指针进行频繁操作。文件在生成终局过程中始终保持打开,sync 函数耗费大量资源保持同步。

针对以上两点,性能提升主要通过改进 I/O 方式。

  • std::endl 插入换行符后 flush() 的性能问题,不再在每行结束后插入endl,改为插入'\n'
  • std::basic_filebuf的性能问题,更改 I/O 方式,不再生成一行写入一行,通过一个大的字符数组缓存所有要写入文件的字符,包括空格和换行符,在生成结束后一次性写入文件。

通过上面两次修改后,第二个版本在 macOS 环境下运行时间 0.78 s ,在 Windows 10 环境下运行时间 3.354 s,速度大幅提升。
通过 Visual Studio 2017 的性能分析工具进行分析:
Version 2 Analysis Result
Version 2 Analysis Result
第二个版本各部分消耗比较均衡,其中最耗时部分是 std::next_permutation 函数,这是用于生成下一个数字排列顺序的全排列函数,由于数独的生成主要依赖与数字顺序的变化和行列的交换,因此生成排列的函数调用次数较多,耗时符合预期,性能瓶颈基本解决。

代码质量分析

利用 Visual Studio 的代码质量分析工具,对代码质量进行检查,发现一个警告:
Code Quality Analysis Result
出现警告的代码行:

cout << "time = " << double(finish - start) / CLOCKS_PER_SEC << "s" << endl;

出现的问题是把用于计时的变量 finish - start 的结果强制转换为 double 型。
clock_t 是 4 byte 的变量,double 是 8 byte的变量,因此代码质量分析工具提示应该在运算前 (before calling operator ‘-’) 就进行转换 (cast the value to the wider type) ,避免出现溢出 (avoid overflow) 。
按照代码质量分析工具的提示修改该行代码:

cout << "time = " << (double(finish) - double(start)) / CLOCKS_PER_SEC << "s" << endl;

修改后,所有警告消除,代码质量检查结束。

关键代码

生成算法核心代码

 do
 {
 	for (int i = 0; i < 9; ++i)        //生成数字交换对应表
            trans[g_row[0][i] - 49] = arr[i];
        
        for (int i = 0; i < 9; ++i)        //按对应表把模版转换为新的数独终局
            for (int j = 0; j < 9; ++j)
                newRow[i][j] = trans[g_row[i][j] - 49];

        for (int i = 0; i < 2 && n; i++)  //以下三个循环分别交换2—3,4-6,7-9行
        {
            for (int j = 0; j < 6 && n; j++)
            {
                for (int k = 0;k < 6 && n; k++)
                {
                    for (int m = 0; m < 9; ++m)
                    {
                        for (int n = 0; n < 9; ++n)
                        {
                            g_output[tempPointer++] = newRow[order[m]][n] +'0';
                            if (n == 8)
                                g_output[tempPointer++] = '\n';
                            else 
                                g_output[tempPointer++] = ' ';
                        }
                    }
                    if (--n)
                        g_output[tempPointer++] = '\n';
                    else
                        return;
                    next_permutation(order+6,order+9);
                }
                next_permutation(order+3,order+6);
            }
            next_permutation(order+1,order+3);
        }
    }
    while(next_permutation(arr+1,arr+9));    //生成下一个数字交换的排列

解决算法核心代码

预处理部分:

if(unsolvedSudoku[r][c] == 0)   //如果是0(代表空白的格子),则记录下位置
                {
                    blank[blankCounter][0] = r;
                    blank[blankCounter][1] = c;
                    blankCounter++;
                }
                else		//否则标记数字,并在用于计算可填数字的每行每列每
                {		//块的对应数组记录中增加1
                    SetMark(r, c, unsolvedSudoku[r][c], 1);
                    row[r]++;
                    col[c]++;
                    block[BlockNum(r, c)]++;
                }

DFS部分:

bool DFS(int deep)
{
    if(deep==blankCounter)	//深度与空白格子数量相同,则数独被成功解决
    {
        return true;
    }

    int r = blank[deep][0], c = blank[deep][1];
    for(int i = 1; i < 10; i++)
    {
        if(!rowMark[r][i] && !colMark[c][i] && !blockMark[BlockNum(r, c)][i])				//枚举并判断是否可以填入1-9数字
        {
            unsolvedSudoku[r][c]=i;
            SetMark(r, c, unsolvedSudoku[r][c], 1);//填入数字
            if(DFS(deep+1))return true;
            SetMark(r, c, unsolvedSudoku[r][c], 0);//恢复原样准备填入下一个
            unsolvedSudoku[r][c]=0;
        }
    }
    return false;
}

时间统计

PSP 2.1Personal Software Process Stages预估耗时(min)实际耗时(min)
Planning计划3030
Estimate估计这个任务需要多少时间3030
Development开发15001000
Analysis需求分析(包括学习新技术)200100
Design Spec生成设计文档6060
Design Review设计复审(和同事审核审计文档)3030
Coding Standard代码规范(为目前的开发制定合适的规范)3030
Design具体设计12060
Coding具体编码500600
Code Review代码复审6060
Test测试(自我测试,修改代码,提交修改)400500
Reporting报告3060
Test Report测试报告3020
Size Measurement计算工作量1010
Postmortem & Process Improvement Plan事后总结,并提出过程改进计划60120
合计30802710
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值