https://github.com/MinstrelZal/Sodokuhtml
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 1 * 60 | 0.5 * 60 |
· Estimate | · 估计这个任务须要多少时间 | 1 * 60 | 0.5 * 60 |
Development | 开发 | 25.5 * 60 | 21.5 * 60 |
· Analysis | · 需求分析 (包括学习新技术) | 10 * 60 | 8 * 60 |
· Design Spec | · 生成设计文档 | 1.5 * 60 | 2 * 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 0.5 * 60 | 1 * 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 0.5 * 60 | 0.5 * 60 |
· Design | · 具体设计 | 4 * 60 | 3 * 60 |
· Coding | · 具体编码 | 4 * 60 | 2 * 60 |
· Code Review | · 代码复审 | 2 * 60 | 1 * 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 3 * 60 | 4 * 60 |
Reporting | 报告 | 4.5 * 60 | 3.5 * 60 |
· Test Report | · 测试报告 | 2 * 60 | 2 * 60 |
· Size Measurement | · 计算工做量 | 0.5 * 60 | 0.5 * 60 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 2 * 60 | 1 * 60 |
合计 | 1860 | 1530 |
题目要求:
1.程序能生成不重复的数独终局至文件;
2.程序能读取文件内的数独问题,求一个可行解并将结果输出到文件;java
要解决这个问题,首先要知道数独游戏的规则git
数独是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家须要根据9×9盘面上的已知数字,推理出全部剩余空格的数字,并知足每一行、每一列、每个粗线宫(3*3)内的数字均含1-9,不重复。数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出必定的已知数字和解题条件,利用逻辑和推理,在其余的空格上填入1-9的数字。使1-9每一个数字在每一行、每一列和每一宫中都只出现一次,因此又称“九宫格”。———引用自《数独_百度百科》github
在了解了题目要求和规则之后,我立刻想到的一种算法就是回溯法:对于生成数独终局,咱们只要按顺序一个个填数字就行了,每填完一个数字都检查它所在的行,列和宫是否知足数独的规则,若知足则填下一个数字,若不知足则回溯。而且因为题目要求中的第二点只要求一个可行解,所以一、2两个要求感受实质上是同样的。在肯定了算法之后,要解决的就是一些技术上的问题,好比,学习一下C++(捂脸)。算法
固然,我还搜索了其余算法,一个比较不错的算法是Dancing Links;在一些较早完成的同窗的博客中,我也看到了他们在《编程之美》这本书中找到了一个不错的算法叫矩阵生成法。编程
一开始我直接把这个题当成了一道C语言题目,将全部函数和变量都写在main.cpp。后来对代码进行了重构,将一部分函数和变量封装成了Sudoku类。Sudoku类共有6个函数,其中函数void Sudoku(int n)为构造函数,函数int SudokuGenerate(int pos, long& count)、void SudokuSolve(char* path)为公有函数,函数bool IsValid(int pot)、void PrintSudolu()为私有函数。其中SudokuGenerate()函数是用来生成数独终局的核心函数,它依次往九宫格中填数,同时调用IsValid()函数来判断所填数字是否知足要求,若知足则递归地进行下一个填数,若不知足则回溯,当填完一个九宫格后,就会调用PrintSudoku()函数将该数独终局输出至文件。SudokuSolve()函数用来解决数独问题,每次从文件中读入一个数独题目,而后调用SudokuGenerate()函数来解题并输出至文件。另外,主函数main()中主要是判断命令行参数的代码,若参数正确则调用SudokuGenerate()函数或SudokuSolve()函数,不然调用PrintUsage()函数在控制台中输出参数的要求。总的来讲,个人代码实现比较简单,也没有采用什么复杂的数据结构。另外,因为此次命令行比较简单,所以没有单独设计一个InputHander类来处理输入。
单元测试设计:
1.针对IsValid()函数设计测试(由于其是能确保生成的数独终局正确的关键函数),将其声明为public,用该函数检测各类错误的数独终局的错误的位置,看其是否可以检测出来;
2.针对命令行设计测试,主要检测各类异常输入,例如参数数量不对,或者参数错误,或者文件没法打开,文件中的数独题目有异常输入(个人处理方式是忽略异常题目,继续解下一道题)等;
3.针对生成数独的个数以及解决数独问题的个数进行测试;数据结构
花费的时间:一下午(大概4-5小时)
改进思路(按照时间顺序):
1.从文件IO上考虑:将以前每次向sudoku.txt文件输出一个字符改成每次输出一个数独终局,显著提升了性能,生成一百万个数独终局的时间由原来的8分钟左右变成了40+秒;同时,将以前每次从文件中读入一个字符改成每次从文件中读入一行,直接用一百万个完整(即不须要解)的数独做为文件中的输入进行测试,用时大概是120+秒;
2.在输出时用char*而不用string,进一步加快了IO,生成一百万个数独终局的时间变为30秒左右;
3.后来才知道release版本比debug版本快不少,就换成了release版的,时间又减小了三分之二,最后生成一百万个数独大概须要12秒,解1000道题大概须要14秒;
性能分析图:
函数
程序中消耗最大的函数:int SudokuGenerate(int pos, long& count, bool solve);性能
main.cpp:单元测试
// Sudoku.cpp: 定义控制台应用程序的入口点。 // #include "stdafx.h" string const USAGE = "USAGE: sudoku.exe -c N(1 <= N <= 100,0000)\n sudoku.exe -s absolute_path_of_puzzlefile"; void PrintUsage() { cout << USAGE << endl; } int main(int argc, char* argv[]) { //clock_t start, finish; //start = clock(); if (argc == 3) { // -c if (argv[1][0] == '-' && argv[1][1] == 'c') { long n = 0; for (unsigned i = 0; i < string(argv[2]).length(); i++) { if (argv[2][i] < '0' || argv[2][i] > '9') { PrintUsage(); return 0; } n = n * 10 + argv[2][i] - '0'; } // wrong patameter if (n < 1 || n > 1000000) { PrintUsage(); return 0; } else { Sudoku su(n); long count = 0; su.SudokuGenerate(1, count, false); } } // -s else if (argv[1][0] == '-' && argv[1][1] == 's') { Sudoku su(1); su.SudokuSolve(argv[2]); } // wrong parameter else { PrintUsage(); } } // wrong patameter else { PrintUsage(); } //finish = clock(); //cout << finish - start << "/" << CLOCKS_PER_SEC << " (s) " << endl; return 0; }
sudoku.h:
#pragma once extern int const GRIDSIZE = 9; extern char const UNKNOWN = '0'; extern char const FLAGNUM = '4'; //student ID: 15061075 class Sudoku { public: Sudoku(int n); int SudokuGenerate(int pos, long& count, bool solve); // solve = true <==> solve sudoku puzzle void SudokuSolve(char* path); private: char grid[GRIDSIZE][GRIDSIZE]; std::ofstream output; int n; char buff[163]; bool IsValid(int pos, bool solve); void PrintSudoku(); };
sudoku.cpp:
#include "sudoku.h" #include "stdafx.h" using namespace std; string const NOSUCHFILE = "No such file: "; string const OUTFILE = "sudoku.txt"; int const SQRTSIZE = int(sqrt(GRIDSIZE)); Sudoku::Sudoku(int n) { for (int i = 0; i < GRIDSIZE; i++) { for (int j = 0; j < GRIDSIZE; j++) { grid[i][j] = UNKNOWN; } } grid[0][0] = FLAGNUM; this->n = n; output.open(OUTFILE); for (int i = 0; i < GRIDSIZE * GRIDSIZE; i++) { if ((i + 1) % 9 == 0) { buff[2 * i + 1] = '\n'; continue; } buff[2 * i + 1] = ' '; } buff[162] = '\n'; } int Sudoku::SudokuGenerate(int pos, long& count, bool solve) { if (pos == GRIDSIZE * GRIDSIZE) { PrintSudoku(); count++; if (count == n) { return 1; } } else { int x = pos / GRIDSIZE; int y = pos % GRIDSIZE; if (grid[x][y] == UNKNOWN) { int base = x / 3 * 3; for (int i = 0; i < GRIDSIZE; i++) // try to fill the pos from 1-9 { grid[x][y] = (i + base) % GRIDSIZE + 1 + '0'; if (IsValid(pos, solve)) // if the number is valid { if (SudokuGenerate(pos + 1, count, solve) == 1) // try to fill next pos { return 1; } } grid[x][y] = UNKNOWN; } } else { if (SudokuGenerate(pos + 1, count, solve) == 1) { return 1; } } } return 0; } int Sudoku::SudokuSolve(char* path) { ifstream input; input.open(path); if (input) { int total = 0; string temp[GRIDSIZE]; string str; int line = 0; bool exc = false; // wrong input such as 'a','.',etc. in the input file while (total < 1000000 && getline(input, str)) { temp[line] = str; line++; if (line == GRIDSIZE) { for (int i = 0; i < GRIDSIZE; i++) { for (int j = 0; j < GRIDSIZE; j++) { grid[i][j] = temp[i][2 * j]; if(grid[i][j] < '0' || grid[i][j] > '9') { exc = true; break; } } } getline(input, str); line = 0; if (exc) { exc = false; continue; } total++; // solve sudoku long count = 0; SudokuGenerate(0, count, true); } } //cout << total << endl; } else { cout << NOSUCHFILE << string(path) << endl; return 0; } return 1; } bool Sudoku::IsValid(int pos, bool solve) { int x = pos / GRIDSIZE; int y = pos % GRIDSIZE; int z = x / SQRTSIZE * SQRTSIZE + y / SQRTSIZE; int leftTop = z / SQRTSIZE * GRIDSIZE * SQRTSIZE + (z % SQRTSIZE) * SQRTSIZE; int rightDown = leftTop + (2 * GRIDSIZE + SQRTSIZE - 1); int bound = solve ? GRIDSIZE : y; // check row for (int i = 0; i < bound; i++) { if (i == y) { continue; } if (grid[x][i] == grid[x][y]) { return false; } } // check column bound = solve ? GRIDSIZE : x; for (int i = 0; i < bound; i++) { if (i == x) { continue; } if (grid[i][y] == grid[x][y]) { return false; } } // check box int bound_x = leftTop / GRIDSIZE; int bound_y = leftTop % GRIDSIZE; if (bound_x % 3 != 0 || bound_y % 3 != 0 || bound_x > GRIDSIZE -3 || bound_y > GRIDSIZE - 3) { cout << "error" << endl; exit(0); } for (int i = bound_x; i < (bound_x + 3); i++) { for (int j = bound_y; j < (bound_y + 3); j++) { if (i == x && j == y) { if (solve) { continue; } else { return true; } } if (grid[i][j] == grid[x][y]) { return false; } } } return true; } void Sudoku::PrintSudoku() { for (int i = 0; i < GRIDSIZE; i++) { for (int j = 0; j < GRIDSIZE; j++) { buff[18 * i + 2 * j] = grid[i][j]; } } output << buff; }
1.此次我的项目作的很是坎坷,一大把缘由就是对VS和C++都不熟悉,在性能测试和单元测试阶段个人VS出了不少问题,让我整我的心态都不太好了,索性最后都解决了(虽然还不知道为何),但这也让我没时间完成附加题了。这也让我深入体会到了“计划赶不上变化”; 2.我以前历来没有想过对一个代码进行优化,也不知道仅仅IO上的改变能对一个程序的性能形成如此大的影响,愈加让我感受本身还须要不断提升; 3.学习了几种不错的生成数独的算法,此次虽然我本身用的是回溯法,但我但愿本身能用DLX算法和矩阵生成法实现一下; 4.我以为本身的IO还能够继续优化;