您的位置:首页 > 其它

[软件工程基础]个人项目 数独

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.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划
· Estimate· 估计这个任务需要多少时间1015
Development开发
· Analysis· 需求分析 (包括学习新技术)120150
· Design Spec· 生成设计文档3030
· Design Review· 设计复审 (和同事审核设计文档)00
· Coding Standard· 代码规范 (为目前的开发制定合适的规范)00
· Design· 具体设计3060
· Coding· 具体编码300500
· Code Review· 代码复审60100
· Test· 测试(自我测试,修改代码,提交修改)100300
Reporting报告
· Test Report· 测试报告60100
· Size Measurement· 计算工作量3030
· Postmortem & Process Improvement Plan· 事后总结, 并提出过程改进计划6060
合计8001345

感想

这一次作业我一开始是当成一个OO作业来做的,因为和上学期的OO课作业差不多。但是这一次我开始审视自己的编码过程。

首先对于项目花费时间的估计,我就和实际的有很大偏差。能想到的原因有几个。

一个是我对于C++不太熟悉,因为之前一直写的Java,没有指针的困扰,写着也比较方便。这次写C++踩了一些坑,查了很多资料。

还有就是我在还没有仔细想好组织的时候就开始写了,边写代码边思考架构比较耗时间,因为经常会陷入到C++实现的细节里面去,同时思考不同层次的问题会比较消耗认知资源。。。

最后就是自己的时间管理还是有些问题,有时不太专注。

一个小项目还是能发现自己很多问题的,这大概就是自己选这课的目的吧,走出舒适区暴露自己的问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: