软件工程个人项目——数独

项目地址

Github项目地址:https://github.com/euyy/MySudoku

PSP表格

PSP2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划
· Estimate· 估计这个任务需要多少时间3030
Development开发
· Analysis· 需求分析(包括学习新技术)4060
· Design Spec· 生成设计文档9060
· Design Review· 设计复审(和同事审核设计文档)00
· Coding Standard· 代码规范(为目前的开发指定合适的规范)6075
· Design· 具体设计12090
· Coding· 具体编码12001080
· Code Review· 代码复审120120
· Test· 测试(自我测试,修改代码,提交修改)120300
Reporting报告
· Test Report· 测试报告180120
· Size Measurement· 计算工作量3020
· Postmortem & Process Improvement Plan· 事后总结,并提出过程改进计划60120
合计20502075

解题思路描述

生成数独终局

首先,看到题目,我首先想到的方法是 深度遍历,暴力搜索 ,但是考虑到对于性能的要求,这显然是不可取的。

结合我之前玩过数独的经验、数独看似简单的规则以及之前两年的学习经验,我联想到了一个看似类似的题目,那就是“N皇后问题”。“N皇后问题”是一个NP问题,当初学习算法分析时,老师曾说过,当N=8时,为了提高效率,可以先在前四行随机放4个皇后,当这四个满足条件时,再利用深搜的方法进行搜索。虽然似乎比起暴力搜索来说,可能要好一些,但是效率可能还是太低。

上学期学习了最优化方法,课上讲述了启发式算法,当时最终的效果还不错,但是它需要遍历的是庞大的解空间。相比暴力搜索解空间来说,它或许效果很好,但是对于数独问题,似乎效率依然不够高。

因此,我转变了方向,决定从网络上寻求一个更好的方法来解决数独问题。我查阅了许多资料,翻看了很多的博客,了解了许多之前写过类似题目的前辈们的想法,有用多线程以提高效率的,有利用不同排列平移的……最终,我决定也使用排列平移的方式,并在此思想的基础上稍作改变。

最终,设想的做法是:
首先,生成一个九位数的排列
因为我的学号最后两位为90,按照(学号后两位相加)%9+1的算法。生成数独矩阵的左上角第一个数应该是( 9 + 0 ) % 9 + 1 = 1,则生成的一个九位数排列可以为(1,2,3,4,5,6,7,8,9)。因为第一位已经固定的原因,所以一共可以生成 8! = 40320个这样的排列。

其次,通过行交换实现生成一个数独终局
因为前三行第一个数固定的原因,只有第二行和第三行可以交换,故前三行有2种变换;中间三行可随意排列,故有 3! = 6种变换;后三行同理,有6种变换。故按照这种方法,最多可生成40320 * 2 * 6 * 6 = 2903040 个数独终局。
考虑到由于排列固定,9个平移变换后的向量也均不同,而数独的第一行不会被交换,所以把第二部分和第三部分整体交换,依然是一个正确的数独。则可以生成40320 * 2 * 6 * 6 * 2 = 5806080 个数独终局。

接下来以(1,2,3,4,5,6,7,8,9)举例说明生成数独终局的方法。
首先确定平移向量。根据之前的描述,前三行的平移数组为(0,3,6)或(0,6,3);中间三行的平移数组可为1、4、7的任意一种排列;后三行的平移数组可为2,5,8的任意一种排列。
经过分析,当9个数的排列不变时,第一行的数字均相同,且9个平移的结果也均不同。这意味着中间三行与后三行交换后,不会出现同一个数组生成两次的情况。故最多可生成40320 * 2 * 6 * 6 * 2= 5806080 个数独终局。
取平移数组为(0,3,6,1,4,7,2,5,8),则生成的对应数独终局为 :
在这里插入图片描述
然后按照三部分的不同组合的平移向量,可以生成不同的数独终局。

求解数独

采用回溯法进行深度搜索求解。
首先,读入数独,记录每行、每列、每个3*3方阵中可以填哪些数,对于每个待填方格,其可以填写的数为所在行、所在列、所在方阵都可以填写的数。
采用回溯法深搜数独的解。首先把只有一个数可以填的方格填上。然后套用回溯法搜索子集树的算法模板求解。每次填写一个数后都要检查目前数独是否合法。

设计实现过程

生成数独终局

操作流程图如下。
在这里插入图片描述

函数主体以及和其他函数的关系如下,具体的子函数代码在此不展示,功能。

void Generate_Sudoku(int finals_num)
{
    int sequence[9] = { 1,2,3,4,5,6,7,8,9 };//初始化排列数组
    rest = finals_num;//剩余终局数
    while (1) {
	Secq2Final(sequence);//由排列数组生成数独终局,同时修改剩余终局数
			    //一次最多可以生成144个数独终局,终局数足够则不再生成
			    
	if (rest == 0)break;//如果数独终局数足够,则停止生成
	
	next_permutation(sequence + 1, sequence + 8);//否则生成下一个排列数组继续生成数独终局
						    //是algorithm库中拥有的函数
    }
    Write2File();//把生成的数独终局写入文件当中,所有终局一次性输出到文件中
}

求解数独

操作流程如下。
在这里插入图片描述
为了方便记录与管理数独当前情况,我采用了下面的结构来存储数独,并利用二进制的9位数来记录可以填写哪些数,比如可以填写1、3、4,那么对应的数值就是000001101B=13。同时,也可通过row、col和matrix的值来确定每一个格子里可以填写的数(利用位操作中的与操作即可)。

下面是一些关键的结构和函数。

struct Puzzles //数独存储和管理结构
{
    int sudoku[9][9];//数独谜题
    int map[9][9];//每个格子有哪些数可以填写
    int row[9];//每一行有哪些数可以填写
    int col[9];//每一列有哪些数可以填写
    int matrix[3][3];//每一个3*3方阵有哪些数可以填写
    int empty;//还有几个未填写的格子
};


void Solve_Sudoku(char addr[])//解数独主体,在主函数中调用
{ 
    do {  
        puz = ReadPuzzle(fr); //读入一个数独 
         	      
        Solve(puz); //求解数独,采用回溯法
        
    } while ((ch = fgetc(fr)) != EOF); 
    Write2File1();//把解出的数独写入文件当中,所有终局一次性输出到文件中
}



void BackTrace(int t, Puzzles &puz) //回溯法求解数独
{  
    if (t > 80) { //找到一个解,将完成的数独赋值给result            
        result = puz;         
        return;    
    }      
    else {             
        int row = t / 9;              
        int col = t % 9;       
        if (puz.sudoku[row][col] != 0){ //如果当前方格有数,则跳至下一个方格                   
            BackTrace(t + 1, puz); 
            return;           
        }             
        vector<int> able_set;//存储当前方格可能的数的集合           
        int matr_i = row / 3;            
        int matr_j = col / 3;  
            
        //通过与运算计算出当前方格可以填哪些数        
        int able = puz.row[row] & puz.col[col] & puz.matrix[matr_i][matr_j];          
        for (int i = 0; i < 9; i++)        
        {                    
            if ((able % 2) == 1) 
                able_set.push_back(i + 1);                  
            able /= 2;              
        }    
	//按顺序填写可能的数,并继续向下搜索         
        for (int i = 0; i < able_set.size(); i++)              
        {                    
            Puzzles temp = puz;   
            //将temp数独的第row行col列填写able_set[i],并维护数独其他内容的正确性                 
            Maintain(temp, row, col, able_set[i]);               
            if (CheckSudoku(temp) == TRUE)                
                BackTrace(t + 1, temp);        
         }      
     }
 }

性能展示及优化方法

生成数独终局

下图是生成一百万个数独终局所需要的时间,在4s以内。
生成1000000个数独终局所用时间

生成数独终局中,需要注意,C++中的文件流操作要比C语言中的文件输入函数要慢很多,且一次性输出到文件中所用的时间要远小于多次输入所用的时间。
下表是我由最初第一版的程序到最终版本的优化过程中,不同版本程序生成一万个数独终局所需要的时间。

version12345说明
0.12.714s2.505s2.54s2.578s2.464s初始版本
0.22.629s2.463s2.588s2.557s2.402schar*转int改为调用函数
0.30.499s0.297s0.518s0.467s0.454s改用c中的fopen文件操作代替c++的fstream输出流操作
1.00.568s0.164s0.33s0.291s0.41s把所有的数独放在一个char[]缓冲区中,生成所有移动数组,通过一个排列生成数组时可直接查找移动数组,无需重复组合
1.10.164s0.277s0.292s0.152s0.253s修复了原来存在的一些bug

下图是优化后的几个性能图。

生成数独cpu利用率

生成数独调用树

生成数独终局

下图是求解一百万个数独所需要的时间,在80s以内。

在这里插入图片描述

计算机求解数独的过程和我们求解数独的过程有所不同。
我们求解的时候,首先填写的是只能填写一个数的格子,或是尝试填写可选择的数较少的格子。我在测试的时候尝试过在每次填数之前,都进行一次扫描,把只能填一个数的方格填上数字,然而,效果并不好。后来把所有的把只能填一个数的方格填上数字的操作都去掉,效果反而更差,时间更长。后来,又利用了随机数来实现填写只能填一个数的方格,效果依然不佳。最后,发现在每次读入数独谜题后,进行一次填写只能填一个数的方格的操作,效果较好,时间在76s左右。
回溯法中,需要遍历深度到81层,我改用empty(剩余空格数)来作为结束回溯的条件,发现效果没有变好,反而变差了,并不清楚是哪里的问题。
下表是我测试的时候性能的一个展示。

序号时间说明
186s左右每次填数之前都填写只能填一个数的方格
296s左右不进行填写只能填一个数的方格的操作
386s或更长利用随机数在每次填数前,填写只能填一个数的方格
476s左右在每次读入数独谜题后,进行一次填写只能填一个数的方格的操作
586s左右改用empty(剩余空格数)来作为结束回溯的条件

下图是优化后的几个性能图。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

可以发现,上图中消耗时间较长的一个操作是vector的new操作,所以,我把原来的用vector存储当前方格可以填写的数的结构改成了用数组存储,结果还是很理想的,结果见下图。
在这里插入图片描述

总结

本次项目虽然难度不是非常大,但是独自一人完成,依然有许多的收获和感悟。
其一,好用的不一定是效率高的。
比如读取文件,方法有很多,有fgetc、fgets、fscanf、fputc、fputs、fprintf以及文件流操作方法,但是效率却是不同的,一个一个读取或者格式化读取虽然可能方便操作,但是就效率而言却不是好的方法。文件流是C++中的文件操作方法,但是却比C中的文件操作方法速度要慢。
vector虽然好用,但是简单使用的时候,系统的开销却增大了。
其二,就目前而言,计算机的“思维”方式与人类不同。
在解数独的过程中,如果是人来处理的话,肯定是先填写可填写的数字只有一个的或者可填写的数字较少的进行尝试,但是应用到计算机解决数独的时候,反而起到了反效果,也许是我的处理方式有问题吧。
其三,未使用面向对象的思想来编写代码。
由于不熟悉使用面向对象的思想,甚至对于一些概念至今依然不是十分明了,所以给代码的可读性以及安全性都带来了不同程度的影响。今后我也会加强使用面向对象的意识,以便尽快熟练掌握面向对象的思想。

附件

需求分析

本次软件开发,就大的功能而言,有两个,其一是数独终局生成,其二是求解数独。
数独终局生成要求:
1、输入要求
在命令行中输入,输入格式为:sudoku.exe -c num(其中num是生成数独终局的个数),且能对非法输入进行异常处理。
2、输出要求
将结果保存在一个文件当中。
每行显示9个数字,数字与数字之间由空格隔开,每行最后一个数字后无空格。
每两个数独终局之间有一个空行,最后一个终局后无空行。
3、其他要求
生成的数独的左上角第一个数字是学号末两位相加模9加1。

求解数独要求:
1、输入要求
在命令行中输入,输入格式为:sudoku.exe -s absolute_path_of_puzzlefile。数独谜题的空格由0代替。
2、输出要求 将结果输出至sudoku.txt文件中。格式要求与数独终局生成的要求相同。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值