zoukankan      html  css  js  c++  java
  • 个人项目-数独程序

    1)GitHub传送门

    https://github.com/MinstrelZal/Sodoku

    2)PSP表格

    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

    3)解题思路

    题目要求:
    1.程序能生成不重复的数独终局至文件;
    2.程序能读取文件内的数独问题,求一个可行解并将结果输出到文件;

    要解决这个问题,首先要知道数独游戏的规则

    数独是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。———引用自《数独_百度百科

    在了解了题目要求和规则以后,我马上想到的一种算法就是回溯法:对于生成数独终局,我们只要按顺序一个个填数字就好了,每填完一个数字都检查它所在的行,列和宫是否满足数独的规则,若满足则填下一个数字,若不满足则回溯。并且由于题目要求中的第二点只要求一个可行解,因此1、2两个要求感觉实质上是一样的。在确定了算法以后,要解决的就是一些技术上的问题,比如,学习一下C++(捂脸)。

    当然,我还搜索了其他算法,一个比较不错的算法是Dancing Links;在一些较早完成的同学的博客中,我也看到了他们在《编程之美》这本书中找到了一个不错的算法叫矩阵生成法

    4)设计实现过程

    一开始我直接把这个题当成了一道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.针对生成数独的个数以及解决数独问题的个数进行测试;

    5)改进程序性能:花费的时间;改进的思路;性能分析图;程序中消耗最大的函数

    花费的时间:一下午(大概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);

    6)代码(部分)说明

    main.cpp

    // Sudoku.cpp: 定义控制台应用程序的入口点。
    //
    
    #include "stdafx.h"
    
    string const USAGE = "USAGE: sudoku.exe -c N(1 <= N <= 100,0000)
           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] = '
    ';
    			continue;
    		}
    		buff[2 * i + 1] = ' ';
    	}
    	buff[162] = '
    ';
    }
    
    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还可以继续优化;

  • 相关阅读:
    zabbix监控部署(三)
    zabbix监控部署(二)
    zabbix监控部署(一)
    Docker安装mysql5.7
    Docker修改容器默认存储路径
    MySQL全量+增量备份脚本
    企业级Dokcer镜像仓库Harbor部署
    Linux环境pyhon3安装pyinstaller
    配置Hive元数据数据库为PostgreSQL
    Oracle 11gR2 RAC 添加节点
  • 原文地址:https://www.cnblogs.com/Minstrel/p/7583804.html
Copyright © 2011-2022 走看看