【BUAA软工】第1次作业 个人项目 数独

一、项目地址

github地址:https://github.com/BuaaAlen/sudoku

二、PSP表格

三、解题思路描述

在拿到这个题目时,我的第一个想法是这样的:9乘9的大方格有9个3乘3的小方格,在每个方格中随机的写入2到3个数,同时不违背数独的规则,之后从第一个空格开始,按照从左至右,从上至下的原则进行回溯,每次回溯所选取的值是在当前情况下,满足数独游戏规则所能选取的值中的一个,如果发现当前空格内没有可选取的值,则证明至少上一步的选择是错误的,回溯到上次选择重新进行选取。这可能是逻辑上最简单的算法之一,但考虑到后续有对程序性能的考察,那么这种暴力回溯算法就放弃了。但是求解数独时还是采用的这种方法。

暴力回溯不可取,于是自然而然的想到了按照某一种数学规则进行数独的生成。请教了一位同学,将9个数字分为3组,比如147,258,369,之后在每个3乘3的方格中,将这9个数字分成3次填入,每次直接填1组的三个数字,下图就是一个例子:

后来在网上查资料时看到了一篇博客,博客中的算法是对于一个已知的数独矩阵,将矩阵中的{1,2,3,...,9}映射到{1,2,3,...,9}中,并且保证映射后的数字与之前的数字存在不同,按照映射后的数字重新生成数独,就会得到一个与之前不同的数独。这是算法的基本原理,需要进一步改进后才可以适应我们的问题。

首先,需要解决映射问题。现有123456789这样一个序列,我们要将它不断的加以变换,也就是要得到它的全排列。在大二学习的组合数学中,是有讲过生成全排列(不断得到下一字典序的序列)的算法的,具体的算法和代码请看之后的代码说明部分。

映射的问题解决了,之后我们要找到要施以变换的数独,需要几个数独呢?一个9个数字的序列的全排列一共可以产生9!个序列,但是,因为题目中有对数独左上角第一个数字的要求:

"在生成数独矩阵时,左上角的第一个数为:(学号后两位相加)% 9 + 1。例如学生A学号后2位是80,则该数字为(8+0)% 9 + 1 = 9"

所以实际上是对8个数字全排列,得到的序列数是8!个,近40000,为满足最后测试数据1000000的需求,我们要准备至少25个基础数独。

这里是一个百度文库的连接,上面是200个数独题目的答案,但是其中的数字排列是不规范的,因为只想得到字符串,复制粘贴下来后再一个个去删空格有太麻烦,于是我写了一个python程序,只有几行,但我觉得这几行应该节约了我差不多20分钟的时间。

ans = ""
while (True):
s = input("Please Input String: ")
if len(s) == 3:
print(ans)
ans = ""
for i in s:
if i in ['1','2','3','4','5','6','7','8','9']:
ans += i

最后得到的效果是这样的:

四、设计实现过程

整体程序的实现过程是这样的:

1.在程序运行之前,要进行初始化工作。

  • initBasicSudokuNum()

    初始化每个基础数独的映射序列,全部初始化为123456789

  • initBasicSudoku()

    初始化基础数独,即将数字字符串全部转换成9*9的二维数组basicSudoku

2.当命令行输入生成数独的命令,"-c 数字",获取数字num,并进行num次循环,每次循环执行以下内容。

  • updateBasicSudokuNum()

    更新当前基础数独的序列,得到该序列下一字典序的序列

  • createCompleteSudoku()

    按照得到的序列进行一一映射,替换掉基础数独中的数字,得到一个全新的数独,用二维数组sudoku来存储,并输出到文件中

3.当命令行输入求解数独的命令,"-s 文件路径",按照输入的文件路径读取文件内容到二维数组puzzle后,进行以下操作。

  • solvePuzzle(i, j)

    回溯函数,i,j表示当前回溯到的坐标,通过调用下面的函数来决定某个值是否可以被选取,以此来决定之后的计算。

  • checkPuzzle(i, j, k)

    一个布尔型函数,表示在当前情况下,puzzle[i][j]是否可以选取值k,可以选取就返回true

4.如果生成数独的输入请求是不合法的,比如-1,abc,10000000000等,或者求解数独的puzzle文件输入是不合法的,则在控制台输出error,并将一个全局布尔变量赋值为false。

5.单元测试的设计:

定义了一个布尔函数checkSudoku(sudoku[][]),用来检测一个9乘9的数字矩阵是否满足数独的游戏规则,满足则返回true,否则返回false

  • TestMethod1

    生成1000个数独,并对这些数独做正确性的检查

  • TestMethod2

    生成1000个数独,并对这些数独做重复性的检查

  • TestMethod3

    测试生成数独的命令里,规定数独个数的部分为字符串,并检查程序是否能够检测到输入不合法

  • TestMethod4

    测试生成数独的命令里,规定数独个数的部分为负数,并检查程序是否能够检测到输入不合法

  • TestMethod5

    测试生成数独的命令里,规定数独个数的部分为无穷大(用一个足够大的整数代替),并检查程序是否能够检测到输入不合法

  • TestMethod6

    测试生成数独的命令里,规定数独个数的部分为0,并检查程序是否能够检测到输入不合法

  • TestMethod7

    当求解数独的输入为正常数独题目时,测试答案的正确性

  • TestMethod8

    当求解数独的输入全为0时,测试答案的正确性

  • TestMethod9

    当求解数独的输入有多个题目时,测试答案的正确性

  • TestMethod10

    当求解数独的输入为一个不合法的数独题目时,测试程序是否能够检测到输入不合法

五、性能分析

1.最开始的版本生成100万个数独的时间为73.43秒,之后通过visualStudio自带的性能分析工具进行检测,发现耗时最多的函数是fprintf

想到的优化方案是将要输出的数字先都整合在一个字符串上,最后一次性输出。优化后生成100万个数独的时间为38.67秒,提速了近50%。

2.将第一次优化后的版本再次使用性能分析工具进行分析,得到如下的结果。

发现占93%时间的是c++里string类中的字符串拼接符号 += ,然而,我们需要每个数字之间都有空格,红色最深的两行已经不能再进行优化了。但是,如果我们使用字符串数组中对于单个字符的赋值符 = ,会提速不少。经过改进,生成100万个数独的时间只需要1.88秒,与之前的结果不在一个数量级上。

最终的版本进行性能分析后,程序中消耗最大的函数是fputs。

3.呃,在debug环境生成的exe得到100万个数独需要1.88秒,而在release环境下得到100万个数独只需要0.88秒。

六、代码说明

生成数独功能最重要的部分是得到全排列,求解数独功能关键的部分则是递归函数。

生成全排列:

/*得到全排列来进行数独中数字的映射*/
void updateBasicSudokuNum(int num)
{
int k = -1;
//从右向左,找到一个a[k],使得a[k]<a[k+1]
for (int i = 6; i >= 0; i--) {
if (basicSudokuNum[num][i] < basicSudokuNum[num][i + 1]) {
k = i;
break;
}
}
if (k == -1) {
return;
}
int min = 10;
int minNum = 0;
//a[k+1]到a[7]中,所有数字都比a[k]大,在其中找到最小的数字
for (int i = k + 1; i <= 7; i++) {
if (basicSudokuNum[num][i] < min && basicSudokuNum[num][i] > basicSudokuNum [num][k]) {
min = basicSudokuNum[num][i];
minNum = i;
}
}
//将找到的这个最小的数字与a[k]进行交换
int tmp = basicSudokuNum[num][minNum];
basicSudokuNum[num][minNum] = basicSudokuNum[num][k];
basicSudokuNum[num][k] = tmp;
//之后将a[k+1]到a[7]的顺序颠倒
for (int i = k + 1; i <= 7; i++) {
int t = 7 - (i - k - 1);
tmp = basicSudokuNum[num][i];
basicSudokuNum[num][i] = basicSudokuNum[num][t];
basicSudokuNum[num][t] = tmp;
if (t - i <= 1) {
break;
}
}
}

回溯求解数独:

/*回溯求解数独*/
void solvePuzzle(int x, int y)
{
for (int i = x; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (puzzle[i][j] != 0)
continue;
for (int k = 1; k <= 9; k++) {
//判断(i,j)位置上是否可以填数字k
if (checkPuzzle(i, j, k)) {
puzzle[i][j] = k;
//递归下面的步骤
solvePuzzle(i, j);
//flag为true表示当前的数独已经正确的填好了,即可退出
if (flag) return;
puzzle[i][j] = 0;
}
}
//如果该位置最终选取的数字是0,则证明之前的某一步填错了,需进行回溯
if (puzzle[i][j] == 0)
return;
}
}
//判断数独的所有位置是否都已经填满
flag = true;
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (puzzle[i][j] == 0)
flag = false;
}
}
//判断生成的数独是否正确
if (!checkSudoku(puzzle))
flag = false;
if (flag) {
//输出数据到文件中
...
}
}

[BUAA2017软工]第1次个人项目 数独的更多相关文章

  1. 软工造梦厂团队项目(Alpha版本发布2)

    课程 (https://edu.cnblogs.com/campus/xnsy/GeographicInformationScience) 作业要求 https://www.cnblogs.com/h ...

  2. [BUAA2017软工]个人作业week-1

    一.快速看完整部教材,列出你仍然不懂的5到10个问题,发布在你的个人博客上. 1.在第二章个人技术和流程,邹欣老师提到了一张表格,主要解释了效能分析的一些名词,其中有这么几个概念: 调用者:函数Foo ...

  3. [敏捷软工团队博客]Beta阶段项目展示

    团队成员简介和个人博客地址 头像 姓名 博客园名称 自我介绍 PM 测试 前端 后端 dzx 秃头院的大闸蟹 大闸蟹是1706菜市场里无菜可卖的底层水货.大闸蟹喜欢音乐(但可惜不会),喜欢lol(可惜 ...

  4. 2020BUAA软工结伴项目作业

    2020BUAA软工结伴项目作业 17373010 杜博玮 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 结伴项目作业 我在这个课程的目标是 学 ...

  5. [BUAA软工]第一次博客作业---阅读《构建之法》

    [BUAA软工]第一次博客作业 项目 内容 这个作业属于哪个课程 北航软工 这个作业的要求在哪里 第1次个人作业 我在这个课程的目标是 学习如何以团队的形式开发软件,提升个人软件开发能力 这个作业在哪 ...

  6. BUAA2020软工团队beta得分总表

    BUAA2020软工团队beta得分总表 [TOC] 零.团队博客目录及beta阶段各部分博客地址 团队博客 计划与设计博客 测试报告博客 发布声明博客 事后分析博客 敏 杰 开 发♂ https:/ ...

  7. 关于软工项目beta版本

    项目总结 项目成员: 黄丰润 031302307 王旭銮 031302320 张家俊 031302329 张晓燕 031302343 项目完成度:实现了专业信息填写.查看,教师信息填写,报课和查看课表 ...

  8. 【2017集美大学1412软工实践_助教博客】团队作业10——项目复审与事后分析(Beta版本)

    写在前面的话 转眼轰轰烈烈本学期的软工实践就结束了,这个过程中想必在熬夜敲代码,激烈讨论中留下诸多回忆的同时,也收获了不少.恭喜所有团队完成了本阶段冲刺,此外,由于大家的贡献分给的都很平均,将个人贡献 ...

  9. [2017BUAA软工助教]个人项目准备工作

    BUAA软工个人项目准备工作 零.注册Github个人账号(你不会没有吧..) 这是Git的使用教程: http://www.cnblogs.com/schaepher/p/5561193.html ...

随机推荐

  1. 计算器和Menu

    MainActivity.java import android.app.Activity; import android.content.Intent; import android.os.Bund ...

  2. cf C. Finite or not? 数论

    You are given several queries. Each query consists of three integers pp, qq and bb. You need to answ ...

  3. CPU指令分类

    指令可以分为三类: 有运算单元参与:compq.subq 无运算单元参与:jge.movq MOV指令可以在CPU内或CPU和存储器之间传送字或字节,它传送的信息可以从寄存器到寄存器,立即数到寄存器, ...

  4. codechef Row and Column Operations 题解

    版权声明:本文作者靖心,靖空间地址:http://blog.csdn.net/kenden23/,未经本作者同意不得转载. https://blog.csdn.net/kenden23/article ...

  5. SpringMVC 常用applicationContext.xml、web.xml、servlet-mvc.xml简单配置

    在进行学习配置文件之前,为了加深对框架的认识,简单的做了SSM框架的简单实验.然后画出listAll查询方法的整个过程的思维导图. 整个过程中的web.xml.SpringMVC.xml.applic ...

  6. df 与 du 已使用空间不一致的原因及解决办法

    通过 df -Th 查看 /var 目录使用了78%, 当登录到/var 目录,du -sh 实际使用112G 分析原因:应该是被删掉的文件 没被真正释放 解决办法: 1.lsof | grep de ...

  7. (二 -2) 天猫精灵接入Home Assistant-自动发现Mqtt设备

    参考中文教程:  https://www.hachina.io/docs/7230.html 英文官网 两个温度传感器:https://www.home-assistant.io/docs/mqtt/ ...

  8. zabbix学习小结

    一.zabbix是干什么的?    zabbix主要用来做监控.监控什么呢?比如日常巡检的CPU.内存.磁盘.swap交换分区和各端口进程等.    以往日常巡检,通过df -h命令获得磁盘的使用量和 ...

  9. QT QListWidget 简单的操作

    以下是简单的 listWidget 的方法的功能 listWidget = QListWidget() #实例化一个(item base)的列表 listWidget.addItem('dd') #添 ...

  10. omcat+java的web程序持续占cpu高问题调试【转】

    1.top -c 2.查看具体线程 ps -m -p 30997 -o tid,%cpu,%mem > threads.log 3.printf %x 31865 其次将需要的线程ID转换为16 ...