数独游戏
一、项目开发的PSP表格总结
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 80 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 600 | 600 |
Development | 开发 | 120 | 120 |
· Analysis | · 需求分析 (包括学习新技术) | 180 | 180 |
· Design Spec | · 生成设计文档 | 150 | 150 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 60 |
· Design | · 具体设计 | 180 | 180 |
· Coding | · 具体编码 | 120 | 120 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 300 |
Reporting | 报告 | 200 | 220 |
· Test Report | · 测试报告 | 180 | 200 |
· Size Measurement | · 计算工作量 | 20 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 2340 | 2370 |
二、项目介绍及要求
项目:生成数独终局并且能求解数独问题的控制台程序
项目需求:
-
在命令行中使用-c参数加数字N(1<=N<=1000000)控制生成数独终局的数量,例如下述命令将生成20个数独终局至文件中:
sudoku -c 20
-
将生成的数独终局用一个文本文件(假设名字叫 sudoku.txt)的形式保存起来,每次生成的txt文件需要覆盖上次生成的txt文件,文件内的格式如下,数与数之间由空格分开,终局与终局之间空一行,行末无空格:
2 6 8 4 7 3 9 5 1 3 4 1 9 6 5 2 7 8 7 9 5 8 1 2 3 6 4 5 7 4 6 2 1 8 3 9 1 3 9 5 4 8 6 2 7 8 2 6 3 9 7 4 1 5 9 1 7 2 8 6 5 4 3 6 8 3 1 5 4 7 9 2 4 5 2 7 3 9 1 8 6 4 5 1 7 8 2 3 6 9 7 8 6 4 9 3 5 2 1 3 9 2 1 5 6 4 8 7 5 2 7 6 4 9 8 1 3 9 6 8 5 3 1 2 7 4 1 3 4 2 7 8 6 9 5 8 1 5 3 6 7 9 4 2 6 7 3 9 2 4 1 5 8 2 4 9 8 1 5 7 3 6 9 5 8 3 6 7 1 2 4 2 3 7 4 5 1 9 6 8 1 4 6 9 2 8 3 5 7 6 1 2 8 7 4 5 9 3 5 7 3 6 1 9 4 8 2 4 8 9 2 3 5 6 7 1 7 2 4 5 9 3 8 1 6 8 9 1 7 4 6 2 3 5 3 6 5 1 8 2 7 4 9 ……
-
程序在处理命令行参数时,不仅能处理格式正确的参数,还能够处理各种异常的情况,如:
sudoku.exe -c abc
-
在生成数独矩阵时,左上角的第一个数为:(学号后两位相加)% 9 + 1。例如学生A学号后2位是80,则该数字为(8+0)% 9 + 1 = 9,那么生成的数独棋盘应如下(x表示满足数独规则的任意数字):
9 x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x
二、需求分析
1.功能建模
0层图:
1层图:
2.功能建模
三、解题思路
在拿到题目的时候,首先我们不难看出项目的关键在于生成1000000中不同的数独。在此之前我们应该先来了解一下数独的含义。
百度百科如下所示:
在了解完数独之后,我们就要来想一想怎么去实现这个过程。
在上网查询完资料之后,我选择了一种较为简单且较为好理解的一种算法。
根据项目要求,我们数独的第一行第一个数字是确定的。以我为例,我的学号后两位37计算所得到的首数字就为2。
首先,我们先考虑行内变换,因为数独的其中一行变换,其他的也要变换,所以我们只考虑第一行的情况就可以。因为第一行的首数字确认,所以我们对其他8个数字进行全排列,得到8!种方案,也就是40320种方案,
核心代码为:
void Permutation(int now) {
if (now == 8) {
for (int i = 0;i < 9;i++) {
firstline[num][i] = first[i];
}
num++;
}
else {
for (int i = now;i < 9;i++) {
swap(first[i], first[now]);//把要打头的数放到最开头的位置
Permutation(now + 1);
swap(first[i], first[now]);//为避免重复排序,每个数打头结束后都恢复初始排序,防止重复的方法很多,不止这一种
}
}
}
但是这还不够项目所要求的1000000种方案。那么接下来我们就可以进行行间变换,首先我们知道数独中九宫格内的数字各不相同,一个九宫格包含三行。
那么我们就可以在2-3,4-6,7-9行间进行交换。同理,我们也对他们进行全排列,这样我们就可以得到2!*3!*3!*8!=2903040种方案,远远满足项目所要求的1000000种方案,由于该全排列数目较少,所以我们可以直接列出交换顺序,如下所示:
int paixu1_3[2][3] = { {0,1,2},{0,2,1} };
int paixu4_6[6][3] = { {3,4,5},{3,5,4},{4,3,5},{4,5,3},{5,3,4},{5,4,3} };
int paixu7_9[6][3] = { {6,7,8},{6,8,7},{7,6,8},{7,8,6},{8,6,7},{8,7,6} };
这样我们就完成了数独生成方面的算法思路构造。
四、设计实现过程
在进行程序设计时,我采用的是结构化开发方法,开发语言为C++。因为vs上自带的性能分析和单元测试工具,所以我的开发平台为vs2019.
首先我设计了程序流程图如下所示:
在整理完程序的流程图之后,我们就开始结构化编程了。首先来说明一下我所定义的函数与数据结构,如下所示:
Shudo:(存放完整数独)
typedef struct {
int form[size][size];
}Shudo;
打印函数PrintSudoku:将信息打印到sudoku.txt文件中
void PrintSudoku(FILE* fp, Shudo shudo, int num)
全排列函数Permutation:对第一行进行全排列,获取第一行行间交换的所有情况
void Permutation(int now)
创造数独函数CreatSudoku:根据所需数独个数生成数独
int CreatSudoku(int count)
下面就来展示程序的具体实现过程:
首先,我们现根据设计思路自我定义一个初始的一行数据以及第一行变换所得其他行的数据变更顺序,代码如下:
int firstline[40320][9] = { 0 };//用一个二维数组来存放第一行的变换情况,也就是8!种
int first[size] = { 2,3,4,5,6,7,8,9,1 };//存放初始的第一行数字
int shunxu[9] = { 0,3,6,1,4,7,2,5,8 };//表示行间的交换顺序
int num = 0;
由于行间交换的全排列情况较少,所以我们可以提前进行定义,减少程序运行时间,代码如下:
//行间交换顺序全排列结果
int paixu1_3[2][3] = { {0,1,2},{0,2,1} };
int paixu4_6[6][3] = { {3,4,5},{3,5,4},{4,3,5},{4,5,3},{5,3,4},{5,4,3} };
int paixu7_9[6][3] = { {6,7,8},{6,8,7},{7,6,8},{7,8,6},{8,6,7},{8,7,6} };
之后我们在对打印输出函数PrintSudoku和全排列函数Permutation进行设计实现,其中全排列算法我参考了全排列算法的理解与实现(递归+字典序)这篇文章,文章讲得很好,让我完全理解了全排列算法的思想,两个函数的代码构造如下:
void PrintSudoku(FILE* fp, Shudo shudo, int num) {
if (num != 0)
fputc('\n', fp);
for (int i = 0;i < 9;i++) {
for (int j = 0;j < 9;j++) {
fprintf(fp, "%d%c", shudo.form[i][j], j == 8 ? '\n' : ' ');
}
}
}
//对第一行进行全排列
void Permutation(int now) {
if (now == 8) {
for (int i = 0;i < 9;i++) {
firstline[num][i] = first[i];
}
num++;
}
else {
for (int i = now;i < 9;i++) {
swap(first[i], first[now]);//把要打头的数放到最开头的位置
Permutation(now + 1);
swap(first[i], first[now]);//为避免重复排序,每个数打头结束后都恢复初始排序,防止重复的方法很多,不止这一种
}
}
}
之后就是我们的核心函数 CreatSudoku的设计实现了。因为我们已经获得第一行行间交换的所有情况,所以我们可以直接根据之前设定好的**shunxu[9]**中所表示的数据来直接得到其余8行的数据,之后根据外部传来的参数来决定行间交换的次数,这样就可以实现生成数独的功能了。代码如下:
//构造所需的数独
int CreatSudoku(int count) {
FILE* fp;
fp = fopen("sudoku.txt", "w");//只允许写入
int num = 0;
Shudo shudu;
for (int i = 0;i < 40320;i++) {
Shudo backup;
//为每一种第一行情况构造完整数独
for (int j = 0;j < 9;j++) {
backup.form[0][j] = firstline[i][j];
}
for (int j = 1;j < 9;j++) {
for (int k = 0;k < 9;k++) {
backup.form[j][k] = backup.form[0][(k + shunxu[j]) % 9];
}
}
shudu = backup;
//将构造好的数独进行行间交换
for (int j = 0;j < 1;j++) {
for (int k = 0;k < 6;k++) {
for (int l = 0;l < 6;l++) {
if (num < count) {
for (int m = 0;m < 3;m++) {
for (int n = 0;n < 9;n++) {
shudu.form[m][n] = backup.form[paixu1_3[j][m]][n];
}
}
for (int m = 3;m < 6;m++) {
for (int n = 0;n < 9;n++) {
shudu.form[m][n] = backup.form[paixu4_6[k][m - 3]][n];
}
}
for (int m = 6;m < 9;m++) {
for (int n = 0;n < 9;n++) {
shudu.form[m][n] = backup.form[paixu7_9[l][m - 6]][n];
}
}
PrintSudoku(fp, shudu, num);
num++;
}
else if (num == count) {
fclose(fp);
return 1;
}
else if (num > count) {
fclose(fp);
printf("程序运行错误!\n");
}
}
}
}
}
//若程序运营到这里还没有终止,表示程序出错
fclose(fp);
printf("程序运行错误!\n");
}
最后就是我们主函数main的构造:
首先我们先对命令行参数进行一系列的判断,这里就不具体描述了,详见代码。如果符合,我们就先调用全排列函数,获取第一行行间交换的所有情况,之后将需要的数独个数传入数独生成函数中,就完成了项目的要求,代码如下:
int main(int argc, char* argv[]) {
clock_t start, finish;
start = clock();
int count = 0;
//判断输入信息
if (argc !=3) {
printf("请输入正确的操作信息!\n");
return -1;
return 0;
}
if (strcmp(argv[1], "-c") == 0 ) {
int len = strlen(argv[2]);
for (int i = 0; i < len; i++)
{
if (argv[2][i] >= '0' && argv[2][i] <= '9')
{
int value = argv[2][i] - '0';
for (int j = 1; j <= len - 1; j++)
value *= 10;
count += value;
}
else
{
printf("请输入一个合法的数字!\n");
return -3;
return 0;
}
if (count > 1000000 || count <= 0) {
printf("请输入合法的范围(1——1000000)!\n");
return -4;
return 0;
}
}
//先将其初始化
if (firstline[0][0] == 0) {
num = 0;
Permutation(1);
}
CreatSudoku(count);
}
else
{
printf("请输入正确获取数独命令!\n");
return -2;
return 0;
}
//计算运行时间
finish = clock();
cout << "使用时间: " << double(finish - start) / CLOCKS_PER_SEC << "s" << endl;
return 2;
注意:其中的返回值没有特殊含义,是为了方便单元测试路径测试所设立的
五、程序正确性以及性能测试
程序正确性测试:输入范围限制在 1-1000,要求程序在 60 s 内给出结果,超时则认定运行结果无效
下图为生成1000个数独的正确性测试结果:
程序性能测试:输入范围限制在 10000-1000000,没有时间的最小要求限制
下图为生成1000000个数独的性能测试结果:
六、单元测试的设计及性能分析
在单元测试中,我们应用的是vs自带的工具,首先我们创建一个test.h的头文件,将我们要测试的模块写进去。之后创建测试项目UnitTest1。
根据程序流程图以及main函数的编写,我采用的单元测试策略为条件组合测试,一共设计了5个测试用例,测试代码如下所示:
#include "pch.h"
#include "CppUnitTest.h"
#include "../sudo/test.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace UnitTest1
{
TEST_CLASS(UnitTest1)
{
public:
TEST_METHOD(TestMethod1)
{
int argc = 2;
char* argv[2];
int a = -1;
Assert::AreEqual(a, main(argc, argv));
}
TEST_METHOD(TestMethod2)
{
int argc = 3;
char* argv[3] = { "sudoku", "-s" ,"20"};
int a = -2;
Assert::AreEqual(a, main(argc, argv));
}
TEST_METHOD(TestMethod3)
{
int argc = 3;
char* argv[3] = { "sudoku","-c","abc" };
int a = -3;
Assert::AreEqual(a, main(argc, argv));
}
TEST_METHOD(TestMethod4)
{
int argc = 3;
char* argv[3] = { "sudoku","-c","1000010" };
int a = -4;
Assert::AreEqual(a, main(argc, argv));
}
TEST_METHOD(TestMethod5)
{
int argc = 3;
char* argv[3] = { "sudoku","-c","20" };
int a = 2;
Assert::AreEqual(a, main(argc, argv));
}
};
}
VS的测试结果图片如下所示:
之后我们利用VS的性能探查器来进行性能分析
结果如下所示:
通过上图我们可以看出,程序的时间主要消耗在打印输出方面了,因此我们可以通过改进输出流来进行程序的优化,可以节约大量的时间。
七、第一阶段总结与反思
在第一阶段的任务中,我第一次感觉到了软件工程的必要性,深深地意识到软件的开发不只是编程这么简单,编程知识软件工程中的一部分。
在项目的开始,由于对各种工具缺乏认识,导致我开始的进程十分缓慢,开发过程十分没有动力,但逐渐了解这些工具之后,我深深的感受到了这些工具对我们的帮助。其中单元测试、性能分析以及需求分析过程中的建模也用到了我们课程所学的知识,让我学以致用,收获了许多!