[软件工程基础]个人项目 数独
2017-09-26 17:29
141 查看
项目地址
https://github.com/Leext/SudokuSolver解题思路
程序的需求有两个:生成给定数量的数独终局
求解给定的数独
对于第一个需求,我直接想到的就是,随机生成一些初始局,再求解不就是数独终局了吗。并且这种方法可以生成任意对初始局有限制的数独。所以核心的问题就转化为求解。
关于求解的思路很容易想到,就是回溯搜索+优化剪枝。
如果是从左往右、从上到下依次搜索每个格子,这样会使需要求解的数量变得比较大。数独每个格子的数字是被行、列、格所限制的,人在玩的时候,总是会根据其他数字的限制先填一个格子。由此可以得到一个剪枝的思路,每当要决定搜索哪个格子的时候,选择可行解最少的。直觉上来说,搜索时每一步的可行解都是最少的,每一次尝试之后,试填的格子周围的可行解数量也都会减少,使得后面的可行解数量也变小。这样就加快了搜索的速度。
对于规定生成的终局左上角为特定数字,由于是求解生成,所以只要初始局满足要求,那生成的所有终局都是满足要求的。
实现过程
实现
代码的整个 设计如下:SudokuBoard 类:封装了数独棋盘,方法包括:棋盘构建,寻找可行解,计算可行解数量,寻找最小可行解格子
SudokuSolver 类:求解器类,方法包括:验证棋盘,深度优先搜索,生成棋盘,文件读取,求解数独
核心的算法是搜索:
计算所有格子的可行解数量
如果没有可求解的,则回溯;如果获得一个解,则保存起来,达到一定数量后退出
寻找可行解最少的格子作为待解格子
获取该格子的所有可行解
对于格子的每一个可行解,设置棋盘为该可行解
递归搜索(回到1)
对于题目中要求的左上角数字,
单元测试
测试代码如下:void test() { SudokuBoard board = SudokuBoard(std::string("012345678000000000000000000000000000000000000000000000000000000000000000000000000")); assert((1 << 8) == (board.getFeasible(0, 0) >> 1)); assert(1 == board.countFeasible(0, 0)); board = SudokuBoard(std::string("012345678900000000000000000000000000000000000000000000000000000000000000000000000")); assert(0 == board.countFeasible(0, 0)); board = SudokuBoard(std::string("012300000400000000507000000000000000000000000600000000000000000600000000000000000")); assert(2 == board.countFeasible(0, 0)); auto p = board.findFewest(); assert(0 == p.first && 0 == p.second); board = SudokuBoard(std::string("000000010400000000020000000000050407008000300001090000300400200050100000000806000")); SudokuSolver solver; SudokuBoard *b = solver.solve(board); assert(solver.check(*b)); board = SudokuBoard(std::string("000000010400000000020000000000050604008000300001090000300400200050100000000807000")); b = solver.solve(board); assert(solver.check(*b)); board = SudokuBoard(std::string("000000012003600000000007000410020000000500300700000600280000040000300500000000000")); b = solver.solve(board); assert(solver.check(*b)); board = SudokuBoard(std::string("000000012008030000000000040120500000000004700060000000507000300000620000000100000")); b = solver.solve(board); assert(solver.check(*b)); board = SudokuBoard(std::string("000000012040050000000009000070600400000100000000000050000087500601000300200000000")); b = solver.solve(board); assert(solver.check(*b)); board = SudokuBoard(std::string("000000012050400000000000030700600400001000000000080000920000800000510700000003000")); b = solver.solve(board); assert(solver.check(*b)); board = SudokuBoard(std::string("000000013000030080070000200000206000030000900000010000600500204000400700100000000")); b = solver.solve(board); assert(b == NULL); solver.generate(board); solver.generateN(3, board); copeSolve("a.txt"); copeGenerate("10"); }
测试包括几个构造好的样例来测试可行解有关的函数,然后测试代码是否能正确解题,最后是测试处理命令行时调用函数的正确性。
代码也都全部覆盖(未覆盖的是对于文件读入的异常提示,因为这次作业不会出现这种情况,就没有测试)
性能改进
在初步完成代码以后,我进行了性能分析。以下是生成100万个数独终局时,程序所用的时间分布。由于我的算法生成和求解是等价的,所以生成时的性能可以体现求解的性能。从中可以发现getBanArray这个函数耗费的时间比较多。
bool *SudokuBoard::getBanArray(int x, int y) { bool *banArray = new bool[10]; for (int i = 0; i < 10; i++) banArray[i] = false; for (int i = 0; i < 9; i++) banArray[_board[i][y]] = true; for (int j = 0; j < 9; j++) banArray[_board[x][j]] = true; int start_x = x / 3 * 3; int start_y = y / 3 * 3; for (int i = start_x; i < start_x + 3; i++) for (int j = start_y; j < start_y + 3; j++) banArray[_board[i][j]] = true; return banArray; } std::vector<int>& SudokuBoard::getSolveVector(int x, int y) { bool *banArray = getBanArray(x, y); std::vector<int>* rtn = new std::vector<int>; for (int i = 1; i < 10; i++) if (!banArray[i]) rtn->push_back(i); delete banArray; return *rtn; }
这个函数是我用来获取某个格子的可行解情况的,它使用布尔数组来储存。在另一个函数中还要利用这个布尔数组生成可行解的vector。这个过程非常繁琐。由于我的算法需要大量调用这个函数,所以非常耗时。我改进了这个过程,使用一个int来表示可行解。用int的低位来表示某个数字是否可行。
改进之后这个过程的代码:
int SudokuBoard::getFeasible(int x, int y) { int bit = 0; const int complete = 0x3fe; for (int i = 0; i < 9; i++) bit |= 1 << _board[i][y]; for (int j = 0; j < 9; j++) bit |= 1 << _board[x][j]; int start_x = x / 3 * 3; int start_y = y / 3 * 3; for (int i = start_x; i < start_x + 3; i++) for (int j = start_y; j < start_y + 3; j++) bit |= 1 << _board[i][j]; return bit^complete; } int SudokuBoard::countFeasible(int x, int y) { // _board[x][y] must be 0 int bit = getFeasible(x, y) >> 1; int count = 0; while (bit) { bit &= (bit - 1); count++; } return count; }
改进之后的性能分析:
可以看到花费的时间从21.6s减少到了7.6秒,性能提升了60%以上。
进一步可以看到fprintf,即写文件的函数,占用了五分之一的时间。因此我尝试另外开一个线程来完成写入文件,但是不知道是不是我自己实现的问题,这个改进并没有加快速度。
代码说明
关键的代码是搜索求解的函数:bool SudokuSolver::dfs(SudokuBoard& board) { std::pair<int, int>& target = board.findFewest(); if (target.first == -1) // end { _solveCount++; solutions->push_back(board.toString()); return _solveCount >= _solveLimit; } if (target.second == -1) // no solution return false; int feasible = board.getFeasible(target.first, target.second); for (int i = 1; i <= 10; i++) { if ((feasible >> i) & 1) { board.set(target, i); if (dfs(board)) return true; } } board.set(target, 0); return false; }
首先获得可行解最少的格子,
findFewest的结果可以作为是否找到解和解不存在的标识。继续搜索则获取该格子的可行解。for循环遍历每个可行解。for循环结束后,把当前格子置为0。
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 10 | 15 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 120 | 150 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 0 | 0 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 0 | 0 |
· Design | · 具体设计 | 30 | 60 |
· Coding | · 具体编码 | 300 | 500 |
· Code Review | · 代码复审 | 60 | 100 |
· Test | · 测试(自我测试,修改代码,提交修改) | 100 | 300 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 60 | 100 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 800 | 1345 |
感想
这一次作业我一开始是当成一个OO作业来做的,因为和上学期的OO课作业差不多。但是这一次我开始审视自己的编码过程。首先对于项目花费时间的估计,我就和实际的有很大偏差。能想到的原因有几个。
一个是我对于C++不太熟悉,因为之前一直写的Java,没有指针的困扰,写着也比较方便。这次写C++踩了一些坑,查了很多资料。
还有就是我在还没有仔细想好组织的时候就开始写了,边写代码边思考架构比较耗时间,因为经常会陷入到C++实现的细节里面去,同时思考不同层次的问题会比较消耗认知资源。。。
最后就是自己的时间管理还是有些问题,有时不太专注。
一个小项目还是能发现自己很多问题的,这大概就是自己选这课的目的吧,走出舒适区暴露自己的问题。
相关文章推荐
- 软件工程基础个人项目开发——数独
- 【软件工程基础】个人数独项目介绍及制作流程
- [软件工程基础]个人项目 数独
- 软件工程基础/个人项目1
- 软件工程基础课-个人项目纪实
- [软件工程基础]结对项目 数独程序扩展
- 软件工程基础课-个人项目-代码风格规范
- [软件工程基础]结对项目 数独程序扩展
- 软件工程基础作业-个人项目代码复审
- 项目管理与软件工程基础—软件生命周期(SDLC)
- 最佳的项目一定建立在最佳的软件工程基础上的
- [软件工程基础]第 1 次个人作业
- [软件工程基础]第 1 次个人作业
- 软件工程个人项目--Word frequency program
- 软件工程基础课-个人技术博客说明
- [软件工程基础]第 3 次个人作业
- [软件工程基础]第 0 次个人作业
- 软件工程个人项目——买书的最低价格
- [软件工程基础]第 2 次个人作业