软件工程基础-个人项目-数独的生成与求解
Github项目地址:https://github.com/CarloFz/OOOOOLD
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 60 |
Estimate | 估计这个任务需要多少时间 | 10 |
Development | 开发 | 600 |
Analysis | 需求分析(包括学习新技术) | 60 |
Design Spec | 生成设计文档 | 60 |
Design Review | 设计复审(和同事审核设计文档) | 20 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 20 |
Design | 具体设计 | 60 |
Coding | 具体编码 | 600 |
Code Review | 代码复审 | 20 |
Test | 测试(自我测试,修改代码,提交修改) | 120 |
Reporting | 报告 | 60 |
Test Report | 测试报告 | 30 |
Size Measurement | 计算工作量 | 20 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 20 |
合计 | 1760 |
二、解题思路
- 终局生成
题目要求最多生成100w个不重复数独终局,其中左上角的数字不能改变
经过查阅资料,我决定使用一个已经写好的数独模板,然后对这个模板的第一行进行全排列(除第一个数字)。但是显然这样做还不能达到100w的复杂度,于是我们可以通过对于模板进行行列的交换,再配合上每个交换后的模板的第一行的全排列,就能够达到100w的复杂度。 - 数独求解
这部分计划使用回溯法求数独的解,并且通过对题目进行预处理减少回溯法的时间复杂度
三、设计实现过程
这个项目的代码文件主要分成四部分
第一部分为header.h:主要负责相关库的引入和所有函数的声明
#pragma once
#include <stdio.h>
#include <iostream>
#include <string.h>
#include <string>
#include <fstream>
#include <ctime>
#include <vector>
using namespace std;
#define MAX_CREATE 1000000
#define MAX_BUFLEN 10000
#define CREATE_FILENAME "./createSudoku.txt"
#define SOLVE_FILENAME "./solveSudoku.txt"int inputProcess(int argc, char** argv);
int create(int count);
int solve(char *path);void swapChar(char* a, char* b);//交换某两个字符的位置
void write(char* buf[], int buflen, bool fin);//将缓冲的内容写入文件void initPossibleSet(int matrix[9][9], vector<int> possibleSet[9][9]);//初始化所有位置的可能的答案的集合
void updatePossibleSet(int matrix[9][9], vector<int> possibleSet[9][9], int row, int col);//更新某一个位置的可能的答案的集合
void checkPossibleSet(int matrix[9][9], vector<int> possibleSet[9][9]);//检查是否有能够唯一确定的空位
bool checkTrue(int matrix[9][9]);//检查数独解是否正确(DEBUG用)
bool backTrace(int row, int col, int matrix[9][9], vector<int> possibleSet[9][9], int nextPos[9][9][2]);//回溯法求解数独
第二部分为main.cpp:是程序的主要框架,负责调用各个部分的函数
第三部分为input.cpp:这个文件中包含检测输入的合法性的相关函数的实现
第四部分为create.cpp: 这个文件中包含生成数独终局所需要的相关函数的实现
下图为我使用的数独终局模板
第五部分为solve.cpp:这个文件中包含解决数独问题所需要的相关函数的实现
四、性能分析
创建数独:sudoku.exe -c num
-c 1 | -c 1000 | -c 10000 | -c 1000000 |
---|---|---|---|
0.001s | 0.031s | 0.105s | 10.255s |
关于创建数独的部分,主要的时间花费在通过多级循环遍历所有模板的交换可能,但是在写入文件的部分应该还有提升空间。
写入文件时,我设计了一个比较大的缓冲区,每次生成一个终局之后就把这个终局添加到缓冲区中,当缓冲区满之后,再把缓冲区的内容一次性写入文件中。
因此缓冲区的大小是IO部分的时间复杂度的关键
求解数独:sudoku.exe -s path
-s 1 | -s 1000 | -s 10000 | -s 1000000 |
---|---|---|---|
0.001s | 0.095s | 0.733s | 73.216s |
关于求解数独的部分,我通过网上查阅资料,发现使用c++的ifstream和ofstream可以直接把文件全部读入内存或把缓冲区的内容全部写入文件,于是读写文件的时间几乎可以不考虑优化。
这部分最占用时间的是对每个数独进行递归的回溯法求解。使用比较简单的回溯法处理1e6的数据大概需要140多秒,显然还有很大优化空间。
于是我采用对数独题目进行预处理的方式,在每个题目中,都会有很多的空白是可以通过同行、同列、同九宫格的已有数字推断出唯一解的,我们可以在使用回溯法求解时把这些空先解答出来,这样就能很大程度上减少递归的层数,能有效地减少回溯法的时间复杂度。
具体的实现方法是我通过possibleSet来存放每一个空格的所有可能解,当某个空格的可能解只有一个时,我们就可以把这个空格填上这个数字,然后对于这个空格所有同行、同列、同九宫格的空格更新他们的possibleSet,再重复检查每个空的解空间大小,当没有空格能求出唯一解时,我们进入回溯法进行求解。
同样的,在回溯法中我也考虑过使用这样的方法减少回溯的复杂度,即在每次尝试对一个空填入可行解时都更新其他空格的可行解,并对有唯一解的空格进行解答。但是这样以来就使得回溯法每一次都需要大量的计算来对possibleSet进行更新,也使得回溯法对于数独矩阵的还原过程变得很困难,因为我额外对数独矩阵做了很大的修改。这些问题使得我在这样实现代码时让处理1e6的数据的时间增加到190多秒,于是这个方法被弃用。
但是我们仍然可以使用possibleSet对回溯法进行一些优化。在回溯法中,我们对于每个空都要用9个数字分别尝试填入,这意味这有很多次重复查询。于是我们在尝试填入之前先更新这个空格的possibleSet,然后将possibleSet中的解尝试填入空格中,这样能很显著的减少回溯时间。
除此之外,原始的回溯法中,每一此递归调用都是调用当前处理的元素的下一个元素,也就是说并不是空格的元素也会使用一层递归调用,于是我想通过只对于空格使用回溯法减少递归层数。最终我通过nextPos[9][9][2]的数组存放每个数独矩阵元素的下一个空格的位置,这个数组在取出一个数独题目并进行过预处理后就可以得出,然后在回溯法时每次递归调用只需要通过查表调用当前数组元素的下一个空格位置即可。
经过预处理、减少查询时间、减少递归层数的方式进行优化过后,对于1e6的数独题目只需要70多秒的时间即可解答。但是需要注意的是,我这这里使用的1e6的测试数据都是我通过对终局进行随机挖空得到的,空格的个数都在60-30之间,可能与实际的数独题目有一些差距。而且当之前使用比较简单的数独题目时,解题时间会有很大的缩短,只需要10多秒就能解决1e6的数据。所以说实际的运行效率与题目的难易程度有很大关系,这主要是因为简单的题目空格较少,回溯法层数很小,而且很多空格都可以通过预处理解决,而困难的题目会导致预处理效果不明显,回溯法搜索时很费时间。
五、代码说明
main.cpp
int main(int argc, char** argv)
{clock_t start = clock();//输入处理int type = inputProcess(argc, argv);if (type == -1){cout << "参数错误,请重新输入" << endl;return 0;}//任务处理if (type == 1){int count = atoi(argv[2]);create(count);}else if (type == 2){solve(argv[2]);}clock_t end = clock();cout << "time : " << ((double)end - start) / CLOCKS_PER_SEC << "s\n" << endl;return 0;
}
input.cpp
int inputProcess(int argc, char** argv)
{//验证参数个数if (argc != 3){return -1;}//验证第一个参数-c/-sint type = 0; //-c = 1; -s = 2;if (strcmp(argv[1],"-c") == 0) {type = 1;}else if (strcmp(argv[1], "-s") == 0){type = 2;}else {type = -1;//错误参数}if (type == -1){return -1;}//验证第二个参数if (type == 1)//-c{string countS = argv[2];for (unsigned int i = 0; i < countS.length(); i++){if (countS[i] > '9' || countS[i] < '0'){return -1;}}}else if (type == 2) {//-sFILE* p = NULL;fopen_s(&p, argv[2], "r");if (p == NULL){return -1;}fclose(p);}return type;
}
create.cpp
int create(int count)
{//覆盖之前的文件FILE* p = NULL;fopen_s(&p, CREATE_FILENAME, "w");if (p != 0) {fclose(p);}char** buf;buf = (char**)malloc(sizeof(char*) * MAX_BUFLEN);int buflen = 0;int countRes = 0;if (count > MAX_CREATE){cout << "请求生成的数独终局过多" << endl;return 0;}//数独终局的原始模板,其他的终局在此基础上变化而成char sudoTemplate[10][10] = { "abcghidef","defabcghi","ghidefabc","bcahigefd","efdbcahig","higefdbca","cabighfde","fdecabigh","ighfdecab" };char matrix[9][9];for (int i = 0; i < 9; i++){for (int j = 0; j < 9; j++){matrix[i][j] = sudoTemplate[i][j];}}//六层循环遍历每一种模板的交换情况//行for(int row1 = 0; row1 < 2; row1++){//12行交换for (int i = 0; i < 9; i++){swapChar(&matrix[1][i], &matrix[2][i]);}for (int row2 = 0; row2 < 6; row2++){if (row2 % 2 == 0){//34行交换for (int i = 0; i < 9; i++){swapChar(&matrix[3][i], &matrix[4][i]);}}else {//45行交换for (int i = 0; i < 9; i++){swapChar(&matrix[5][i], &matrix[4][i]);}}for (int row3 = 0; row3 < 6; row3++){if (row3 % 2 == 0){//67行交换for (int i = 0; i < 9; i++){swapChar(&matrix[6][i], &matrix[7][i]);}}else {//78行交换for (int i = 0; i < 9; i++){swapChar(&matrix[7][i], &matrix[8][i]);}}//列for (int col1 = 0; col1 < 2; col1++){//12列交换for (int i = 0; i < 9; i++){swapChar(&matrix[i][1], &matrix[i][2]);}for (int col2 = 0; col2 < 6; col2++){if (col2 % 2 == 0){//34列交换for (int i = 0; i < 9; i++){swapChar(&matrix[i][3], &matrix[i][4]);}}else {//45列交换for (int i = 0; i < 9; i++){swapChar(&matrix[i][4], &matrix[i][5]);}}for (int col3 = 0; col3 < 6; col3++){if (col2 % 2 == 0){//67列交换for (int i = 0; i < 9; i++){swapChar(&matrix[i][6], &matrix[i][7]);}}else {//78列交换for (int i = 0; i < 9; i++){swapChar(&matrix[i][7], &matrix[i][8]);}}//开始生成全排列int index[8] = { -1,-1,-1,-1,-1,-1,-1,-1 };int pos[9];for (pos[1] = 0; pos[1] < 8; pos[1]++){for (pos[2] = 0; pos[2] < 7; pos[2]++){for (pos[3] = 0; pos[3] < 6; pos[3]++){for (pos[4] = 0; pos[4] < 5; pos[4]++){for (pos[5] = 0; pos[5] < 4; pos[5]++){for (pos[6] = 0; pos[6] < 3; pos[6]++){for (pos[7] = 0; pos[7] < 2; pos[7]++){for (pos[8] = 0; pos[8] < 1; pos[8]++){//计算排列//映射信息存在index中index[pos[1]] = 1;for (int i = 2; i <= 8; i++) {int countPos = 0;for (int j = 0; j < 8; j++){if (index[j] == -1){countPos++;}if (countPos - 1 == pos[i]) {index[j] = i + 1;break;}}}//生成一个终局endchar* end = (char*)malloc(sizeof(char) * 81);for (int i = 0; i < 9; i++){for (int j = 0; j < 9; j++) {if (matrix[i][j] == 'a') {if (end != 0) {end[i * 9 + j] = '2';}}else {if (end != 0) {end[i * 9 + j] = index[matrix[i][j] - 'a' - 1] + '1' - 1;}}}}//将终局存在IO缓冲中if (buf != 0) {buf[buflen++] = end;}countRes++;if (countRes == count) {if (buflen != 0) {write(buf, buflen, true);buflen = 0;}return 0;}if (buflen >= MAX_BUFLEN) {write(buf, buflen, false);buflen = 0;}for (int i = 0; i < 8; i++){index[i] = -1;}}}}}}}}}//}}}}}}return 0;
}
void swapChar(char* a, char* b)
{char temp = *a;*a = *b;*b = temp;
}
void write(char* buf[], int bufLen, bool fin)
{FILE* p = NULL;fopen_s(&p, CREATE_FILENAME, "a+");for (int i = 0; i < bufLen; i++){for (int j = 0; j < 9; j++){for (int k = 0; k < 9; k++){if (p != 0) {fwrite(&buf[i][j * 9 + k], sizeof(char), 1, p);if (k != 8) {fprintf(p, " ");}}}if (!(j == 8 && fin && i == bufLen - 1)) {if (p != 0) {fprintf(p, "\n");}}}if (!fin || i != bufLen - 1) {if (p != 0) {fprintf(p, "\n");}}free(buf[i]);}if (p != 0) {fclose(p);}
}
solve.cpp
int solve(char* path)
{//读文件ifstream fin(path, std::ios::binary);int bufLen = static_cast<unsigned int>(fin.seekg(0, std::ios::end).tellg());vector<char> buf(bufLen);fin.seekg(0, std::ios::beg).read(&buf[0], static_cast<std::streamsize>(buf.size()));fin.close();int bufPoint = 0;while (bufPoint < bufLen){int bufPointStart = bufPoint;//取出一个数独题目int matrix[9][9];vector<int> possibleSet[9][9];for (int i = 0; i < 9; i++){for (int j = 0; j < 9; j++){matrix[i][j] = buf[bufPoint] - '0';bufPoint += 2;possibleSet[i][j].clear();}}//先把答案唯一的空位填上,降低递归的复杂度initPossibleSet(matrix, possibleSet);checkPossibleSet(matrix, possibleSet);//将空位的位置存成表,加快查询速度int nextPos[9][9][2];int startPos[2] = { -1, -1 };for (int i = 8; i >= 0; i--){for (int j = 8; j >= 0; j--){nextPos[i][j][0] = startPos[0];nextPos[i][j][1] = startPos[1];if (matrix[i][j] == 0) {startPos[0] = i;startPos[1] = j;}}}//回溯法求解backTrace(startPos[0], startPos[1], matrix, possibleSet, nextPos);if (bufPoint != bufLen - 1) {bufPoint++;}//存储解for (int i = 0; i < 9; i++){for (int j = 0; j < 9; j++){buf[bufPointStart] = matrix[i][j] + '0';bufPointStart += 2;}}}//写文件ofstream fout(SOLVE_FILENAME, std::ios::binary);fout.seekp(0).write(&buf[0], bufLen);fout.close();return 0;
}
//初始化所有位置的可能的答案的集合
void initPossibleSet(int matrix[9][9], vector<int> possibleSet[9][9])
{for (int i = 0; i < 9; i++){for (int j = 0; j < 9; j++) {updatePossibleSet(matrix, possibleSet, i, j);}}
}
//更新某一个位置的可能的答案的集合
void updatePossibleSet(int matrix[9][9], vector<int> possibleSet[9][9], int row, int col)
{possibleSet[row][col].clear();if (matrix[row][col] == 0){int exist[9];memset(exist, 0, sizeof(exist));for (int k = 0; k < 9; k++){if (matrix[row][k] != 0) {exist[matrix[row][k] - 1] = 1;}if (matrix[k][col] != 0) {exist[matrix[k][col] - 1] = 1;}if (matrix[row / 3 * 3 + k / 3][col / 3 * 3 + k % 3] != 0){exist[matrix[row / 3 * 3 + k / 3][col / 3 * 3 + k % 3] - 1] = 1;}}for (int k = 0; k < 9; k++) {if (exist[k] == 0) {possibleSet[row][col].push_back(k + 1);}}}
}
//检查是否有能够唯一确定的空位
void checkPossibleSet(int matrix[9][9], vector<int> possibleSet[9][9])
{bool stepOut = true;while (stepOut){stepOut = false;for (int i = 0; i < 9; i++){for (int j = 0; j < 9; j++) {if (possibleSet[i][j].size() == 1) {stepOut = true;matrix[i][j] = possibleSet[i][j][0];possibleSet[i][j].clear();for (int k = 0; k < 9; k++){updatePossibleSet(matrix, possibleSet, i, k);updatePossibleSet(matrix, possibleSet, k, j);updatePossibleSet(matrix, possibleSet, i/3*3+k/3, j/3*3+k%3);}}}}}
}
//检查数独解是否正确(DEBUG用)
bool checkTrue(int matrix[9][9])
{for (int i = 0; i < 9; i++) {int existR[9];int existC[9];int existB[9];memset(existR, 0, sizeof(existR));memset(existC, 0, sizeof(existC));memset(existB, 0, sizeof(existB));int baseR = 0, baseC = 0;switch (i){case 0:baseR = 0; baseC = 0;break;case 1:baseR = 0; baseC = 3;break;case 2:baseR = 0; baseC = 6;break;case 3:baseR = 3; baseC = 0;break;case 4:baseR = 3; baseC = 3;break;case 5:baseR = 3; baseC = 6;break;case 6:baseR = 6; baseC = 0;break;case 7:baseR = 6; baseC = 3;break;case 8:baseR = 6; baseC = 6;break;default:break;}for (int j = 0; j < 9; j++) {if (matrix[i][j] <= 0 || matrix[i][j] >= 10 ||matrix[j][i] <= 0 || matrix[j][i] >= 10 ||matrix[baseR + j / 3][baseC + j % 3] <= 0 || matrix[baseR + j / 3][baseC + j % 3] >= 10){return false;}if (existC[matrix[j][i] - 1] == 1 || existR[matrix[i][j] - 1] == 1 || existB[matrix[baseR + j / 3][baseC + j % 3] - 1] == 1) {return false;}else {existC[matrix[j][i] - 1] = 1;existR[matrix[i][j] - 1] = 1;existB[matrix[baseR + j / 3][baseC + j % 3] - 1] = 1;}}}return true;
}
//回溯法求解数独
bool backTrace(int row, int col, int matrix[9][9], vector<int> possibleSet[9][9], int nextPos[9][9][2]) {if (row == -1 && col == -1) {//已经成功了,打印数组即可return true;}updatePossibleSet(matrix, possibleSet, row, col);for (int k = 0; k < possibleSet[row][col].size(); k++) {//判断给i行j列放1-9中的任意一个数是否能满足规则//将该值赋给该空格,然后进入下一个空格matrix[row][col] = possibleSet[row][col][k];if (backTrace(nextPos[row][col][0], nextPos[row][col][1], matrix, possibleSet, nextPos)){return true;}//初始化该空格matrix[row][col] = 0;}return false;
}
以上代码都在visual stdio 2019自带的代码分析器中消除了所有警告
六、实际花费的时间
PSP2.1 | Personal Software Process Stages | 实际耗时(分钟) |
---|---|---|
Planning | 计划 | 60 |
Estimate | 估计这个任务需要多少时间 | 10 |
Development | 开发 | 1200 |
Analysis | 需求分析(包括学习新技术) | 60 |
Design Spec | 生成设计文档 | 60 |
Design Review | 设计复审(和同事审核设计文档) | 20 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 20 |
Design | 具体设计 | 60 |
Coding | 具体编码 | 1800 |
Code Review | 代码复审 | 20 |
Test | 测试(自我测试,修改代码,提交修改) | 180 |
Reporting | 报告 | 80 |
Test Report | 测试报告 | 30 |
Size Measurement | 计算工作量 | 20 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 20 |
合计 | 3640 |
七、总结
这此的数独项目中我印象最深的时解数独的优化过程,有很多的优化想法,但是因为设计时考虑不周,导致出现了很多次的负优化。程序效率的优化部分我还有很多需要改进的地方。
软件工程基础-个人项目-数独的生成与求解相关推荐
- 软件工程基础个人项目——数独终局生成求解
目录 1.源代码的GitHub链接: 2.PSP表格(预估): 3.题目要求: 4.解题思路: 1)数独游戏规则 2)生成数独终局 2)求解数独 5.设计实现过程: 第一部分:sudoku类的构建 第 ...
- 软件工程基础-个人项目-数独游戏
软件工程基础-个人项目-数独游戏 ----------------------------------------------------------------------------------- ...
- 软件工程基础个人项目——数独(5)
软件工程基础个人项目--数独 点击这里可看github上的具体代码 本次个人项目关于数独的生成与求解 PSP表格 PSP2.1 Personal Software Process Stages 预估耗 ...
- 软件工程基础——个人项目——数独(1)
软件工程基础--个人项目--数独(1) 一.实现目标 1.生成数独终局 命令行输入如下: sudoku.exe -c 20 sudoku.exe为最终实现程序,-c确定活动为生成数独终局,20为生成结 ...
- [软件工程基础]结对项目 数独程序扩展
(1)在文章开头给出Github项目地址.(1') 项目地址:https://github.com/JerryYouxin/sudoku (2)在开始实现程序之前,在下述PSP表格记录下你估计将在程序 ...
- [软件工程基础]个人项目——数独
目录 一.Github项目地址 二.PSP估计 三.解题思路描述 3.1. 生成终局 3.1.1.暴力搜索--回溯法 3.1.2.模板法 3.2.求解数独 3.2.1.暴力搜索--回溯法 3.2.2. ...
- 软件工程基础-个人项目-数独
个人项目–数独 目录) 个人项目--数独 1 项目地址 2 PSP表格 3 思路描述 3.1 数独终局生成 3.1.1. 暴力法 3.1.2. 全排列及行变换 3.2 功能实现思路 3.2.1. 数独 ...
- 软件工程基础大项目——数独问题
Github项目地址:https://github.com/WX78yyj/sudoku(由于自己有不爱命名的坏习惯,所以忘记命名了,结果提交的文件名称是未命名3,发现还改不了,郁闷) PSP2.1 ...
- 数独问题流程图_软件工程基础大项目——数独问题
Github项目地址:https://github.com/WX78yyj/sudoku(由于自己有不爱命名的坏习惯,所以忘记命名了,结果提交的文件名称是未命名3,发现还改不了,郁闷) PSP2.1 ...
- 数独_软件工程基础个人项目
一.Github项目地址 https://github.com/maoshuo1754/sudoku 二.预估时间 预估时间 PSP2.1 Personai software process stag ...
最新文章
- LOJ504「LibreOJ β Round」ZQC 的手办
- Java类型FloatDouble
- 【js】获得项目路径
- 【EXLIBRIS】纸版书目整理 -- 大书架 上 【292 种】【327册】
- java e.getmessage() null,浅谈Java异常的Exception e中的egetMessage()和toString()方法的区别...
- 抽奖系统概率设计_《微博抽奖玄学理论·养号攻略XI》
- 数据库基础知识(学习笔记)
- 云计算技术与应用(高职组)赛题库 2019 年全国职业院校技能大赛题库
- .value和.innerHTML
- 观后感《没事别看哲学书!》
- 了解C语言中的pipe()系统调用
- 7个流行的强化学习算法及代码实现
- 解决TortoiseSVN或者TortoiseGit拉取的文件夹不能完整显示绿色打钩、黄色、红色感叹号、蓝色加号等小图标的问题
- 欲与青龙重得水,来年再战不周山
- 理解递归的返回——递归查询地区表
- SQL Sever报错,无法连接到服务器
- 2K20安卓修改器服务器到期,nba2k20手机版修改器
- 【相机】(1)——Intent调相机的2种方式以及那些你知道的和不知道的坑
- 狂神说docker笔记(一)
- 腾讯绝不会开放客户端QQ
热门文章
- java调用公安接口_src 公安部PGIS在交警系统的应用,包括 的各种API 以及mysql对空间数据的支持 GIS program 261万源代码下载- www.pudn.com...
- 大数据项目实施工作流程及大数据运维的日常工作流程
- 第七版(谢希仁)计算机网络 知识点总结
- 《码出高效 Java开发手册》书籍源码及相关代码示例
- 关于中标麒麟系统出现“网络管理器未响应”这件事的解决办法
- 免费服装收银系统哪个好?
- 你好2019,我是全新的CPDA数据分析师课程
- js ajax上传file文件上传,使用ajaxfileupload.js实现上传文件功能
- TamronOS IPTV系统任意用户添加修改
- ICEM 二维块的拉伸