zoukankan      html  css  js  c++  java
  • [软件工程基础]个人项目 数独

    项目地址

    https://github.com/Leext/SudokuSolver

    解题思路

    程序的需求有两个:

    1. 生成给定数量的数独终局
    2. 求解给定的数独

    对于第一个需求,我直接想到的就是,随机生成一些初始局,再求解不就是数独终局了吗。并且这种方法可以生成任意对初始局有限制的数独。所以核心的问题就转化为求解。

    关于求解的思路很容易想到,就是回溯搜索+优化剪枝。

    如果是从左往右、从上到下依次搜索每个格子,这样会使需要求解的数量变得比较大。数独每个格子的数字是被行、列、格所限制的,人在玩的时候,总是会根据其他数字的限制先填一个格子。由此可以得到一个剪枝的思路,每当要决定搜索哪个格子的时候,选择可行解最少的。直觉上来说,搜索时每一步的可行解都是最少的,每一次尝试之后,试填的格子周围的可行解数量也都会减少,使得后面的可行解数量也变小。这样就加快了搜索的速度。

    对于规定生成的终局左上角为特定数字,由于是求解生成,所以只要初始局满足要求,那生成的所有终局都是满足要求的。

    实现过程

    实现

    代码的整个 设计如下:

    SudokuBoard 类:封装了数独棋盘,方法包括:棋盘构建,寻找可行解,计算可行解数量,寻找最小可行解格子

    SudokuSolver 类:求解器类,方法包括:验证棋盘,深度优先搜索,生成棋盘,文件读取,求解数独

    核心的算法是搜索:

    1. 计算所有格子的可行解数量
    2. 如果没有可求解的,则回溯;如果获得一个解,则保存起来,达到一定数量后退出
    3. 寻找可行解最少的格子作为待解格子
    4. 获取该格子的所有可行解
    5. 对于格子的每一个可行解,设置棋盘为该可行解
    6. 递归搜索(回到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++实现的细节里面去,同时思考不同层次的问题会比较消耗认知资源。。。

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

    一个小项目还是能发现自己很多问题的,这大概就是自己选这课的目的吧,走出舒适区暴露自己的问题。

  • 相关阅读:
    解决svn Authorization failed错误
    jQuery切换事件
    jQuery学习笔记
    EAS开发
    JavaScript第二课
    JavaScript学习第一课
    EAS常用工具类
    [转]OpenBLAS项目与矩阵乘法优化
    [转]mmap和madvise的使用
    [转]C++赋值运算符重载函数(operator=)
  • 原文地址:https://www.cnblogs.com/leext/p/7597894.html
Copyright © 2011-2022 走看看