2017BUAA软工个人项目之数独

1.项目GitHub地址:https://github.com/ZiJiaW/Soduko

(由于一开始把sudoku看成了soduko,于是名字建错了,读起来可能有点奇怪…)

2.项目PSP表格如下:


PSP2.1


Personal Software Process Stages


预估耗时


实际耗时


Planning


计划


0.5h


0.5h


.Estimate


.估计这个任务需要多少时间


0.5h


0.5h


Development


开发


20.5h


21.5


.Analysis


.需求分析(包括学习新技术)


3h


2h


.Design Spec


.生成设计文档


0h


0h


.Design Review


.设计复审(和同事设计审核设计文档)


0h


0h


.Coding Standard


.代码规范(为目前的开发指定合适的规范)


0h


0h


.Design


.具体设计


3h


2.5h


.Coding


.具体编码


8h


6h


.Code Review


.代码复审


0h


0h


.Test


.测试(自我测试,修改代码,提交修改)


2h


6h


Reporting


报告


3h


4h


.Test Reprot


.测试报告


1.5h


1h


.Size Measurement


.计算工作量


0.5h


0h


.Postmortem & Process Improvement Plan


.事后总结,并提出过程改进计划


1h


0.5h


合计


24h


23h

3.解题思路

3.1 任务需求

编写命令行程序(sudoku.exe),支持下列指令:1. sudoku.exe –c N  2. sudoku.exe  -s absolute_path_of_puzzlefile;

指令1实现程序生成不重复的N个数独终局至同目录下文件sudoku.txt,数独矩阵的左上角数字为确定的(9+6)%9+1=7;指令2将绝对路径下的数独题目解出并生成答案于同目录下的sudoku.txt。

3.2 思路分析

直觉告诉我解数独比较简单,所以我先想的是怎么解数独。查了下资料发现主要就两种方法,最简单的就是递归回溯,另外一种就是Dancing Links算法,将数独转化成精确覆盖问题求解,其实也是需要回溯的,但是使用的数据结构比较方便。因为我先想的也是递归填数,而且比较简单,所以就选择第一种方法了。思路很简单,将读取到的数独题目保存在9*9的二维数组中:

  1. 从第一个格子开始填数,如果该格子已经填过,那么处理下一个格子;
  2. 如果当前格子是空的,尝试从1-9中选择数字填入,并判断是否符合数独规则,符合则填入;
  3. 若最终没有找到合适的数填入,说明在之前填的某一个数字不对,进行回溯。
  4. 若填的是最后一个格子且满足规则,说明找到了数独的一个解,程序结束。

解决了解数独的问题,再来看怎么生成数独,其实从上面的角度很容易就想到,把初始的数独矩阵全部置零,按照解数独的方法即可生成数独了,只不过需要生成的数独数量较多,相当于求全零数独的前N个解,那么只要将解数独的算法步骤稍作修改,当生成的解是第N个,输出该解让程序结束,当生成的解不足N个时,输出该解并回溯,这样就能够保证已经生成的解不会二次生成,因为回溯后必然改变之前的某个值。至于左上角规则,只需要在预先放置好需要的数字,然后从第二个格子开始处理即可。

4.实现过程

分析需求,我设计了以下函数和类:

4.1输入

vector<int(*)[9]> SodukoInput(char * filename);

读取文件中的数独题目,每一个题目为一个2维矩阵,返回各题目矩阵指针的vector;

4.2输出

void SudokuOutput(char *ret, int maxnum, char *ret2);

将得到的保存所有数独解的数组设置格式(空格和回车),然后返回之,使用fputs输出。

4.3数独求解模块

class SudokuSolve {
public:
    bool Solve(int r, int l);//递归填数
    bool check(int r, int l, int num);//测试同行同宫同列是否已有num
    void ProblemInit(int p[9][9]);//初始化
    int(*getSolution())[9];//返回解
private:
    int problem[9][9];
};

每一个需要求解的数独初始化一个类,Solve函数即为对problem[r][l]的处理试填,check函数判断在problem[r][l]处填入num是否符合规则,getSolution用于在解决数独后返回填完的problem的指针,用于输出。

4.3数独生成模块

class SudokuMaker {
public:
    bool fill(int r, int l, char *ret);//递归填数
    bool check(int r, int l, int num);//判断在[r,l]处放入num是否符合数独规则
    void RequestInit(int n);//初始化需求
private:
    int maxnum;//需要生成的数独终局数
    int count;//当前已生成的数独终局数
    int M[9][9];//维护的数独棋盘
};

5.关键代码说明

下面给出数独求解的Solve函数进行说明:

bool SudokuSolve::Solve(int r, int l)
{
    int nr = l == 8 ? r + 1 : r;
    int nl = l == 8 ? 0 : l + 1;
    if (problem[r][l] != 0 && nr < 9)//(r, l) already has a number
    {
        if (SudokuSolve::Solve(nr, nl))
            return true;
        else
            return false;
    }
    else if (problem[r][l] != 0 && nr >= 9)//problem solved
        return true;
    // now problem[i][j] == 0, try to fill it.
    for (int k = 1; k < 10; ++k)
    {
        if (!SudokuSolve::check(r, l, k))
            continue;
        problem[r][l] = k;
        if (r == 8 && l == 8)//problem solved
            return true;
        if (SudokuSolve::Solve(nr, nl))
            return true;
        else
            problem[r][l] = 0;//k is bad, try k+1.
    }
    return false;//can‘t find a k.
}

实际调用的时候,首先初始化数独题目,即函数中的problem,而后调用Solve(0,0)即可将problem解出;上面的函数首先计算当前处理位置的下一位置,若当前位置已经有值,则直接处理下一个,若恰好在最后一个位置有值,则说明此时数独已经解好了,可以结束递归;在当前位置为空时,我们就要尝试填数,对每一个判断是否符合规则,找到一个合适的值后,若填的是最后一个位置,同样说明数独解决;否则填值后处理下一个位置,若下一个位置的处理失败,说明当前位置的填值不合适,尝试下一个数;在尝试所有数后,若没有合适的,说明之前位置填值有误,需要恢复当前位置的空状态并回溯。注意恢复problem[r][l]=0是必要的,否则回溯到上一个位置时会对check函数(判断同行同列同宫是否存在k)的结果有影响,导致少解。

对这个函数的单元测试函数如下:

TEST_METHOD(TestMethod1)
        {
            int p[9][9] = { {8,0,0,0,0,0,0,0,0},{0,0,3,6,0,0,0,0,0},{0,7,0,0,9,0,2,0,0},
            {0,5,0,0,0,7,0,0,0},{0,0,0,0,4,5,7,0,0},{0,0,0,1,0,0,0,3,0},
            {0,0,1,0,0,0,0,6,8},{0,0,8,5,0,0,0,1,0},{0,9,0,0,0,0,4,0,0} };
            SudokuSolve s;
            s.ProblemInit(p);
            s.Solve(0, 0);
            int(*q)[9] = s.getSolution();
            bool r = true;
            for (int i = 0; i < 9; ++i)
            {
                for (int j = 0; j < 9; ++j)
                    r &= s.check(i, j, q[i][j]);
            }
            Assert::AreEqual(r, true);
        }

使用的数独题目为号称最难的芬兰题,在实际运行中使用clock计时得到求解时间为245ms,测试中给出数独的解并测试其是否合法,测试结果如下:

相似的思路处理生成数独的问题,给出SudokuMaker::fill函数如下:

bool SudokuMaker::fill(int r, int l, char *ret)
{
    int nr = l == 8 ? r + 1 : r;
    int nl = l == 8 ? 0 : l + 1;
    for (int k = 1; k < 10; ++k)
    {
        if (!SudokuMaker::check(r, l, k))
            continue;
        M[r][l] = k;
        if (r == 8 && l == 8)//到达最后一个位置
        {
            count++;
            if (count == maxnum)//若已生成要求数目的数独终局,则输出终局并结束递归
            {
                for (int i = 0; i < 9; ++i)
                {
                    for (int j = 0; j < 9; ++j)
                    {
                        ret[9 * i + j + 81 * (count-1)] = char(M[i][j] + ‘0‘);
                    }
                }
                return true;
            }
            else
            {
                //生成数目不够,则输出并恢复[r,l]处的值,并试填下一个
                for (int i = 0; i < 9; ++i)
                {
                    for (int j = 0; j < 9; ++j)
                    {
                        ret[9 * i + j + 81 * (count-1)] = char(M[i][j] + ‘0‘);
                    }
                }
                M[r][l] = 0;
                continue;
            }
        }
        else
        {
            if (SudokuMaker::fill(nr, nl, ret))//递归求解下一个位置
                return true;
            else
            {
                M[r][l] = 0;
                continue;
            }
        }
    }
    return false;
}

可以看到两个函数布局差不多,实际上和之前分析的一样,初始化数组M的左上角,从位置(0,1)开始求解,两者的区别在于前者只要得到一个可行解就可以输出结束递归,而后者需要生成maxnum个数独终局,因此在生成足够数目的数独前,函数一律在输出后继续尝试,尝试所有的值后回溯。下面给出代码覆盖率测试结果,分两块,一块是生成数独,一块是解数独:

因为一次只能设置一个命令行参数,所以都不是100%,但是可以看到两个模块分别都是覆盖率很高的。

6.性能分析和改进

由于选择的是暴力回溯,瓶颈在那里,所以数独生成的速度肯定不会很快…但是按照以上的思路编译通过后第一次性能分析,我选择的是生成1000个数独终局,运行时间达到了惊人的38秒,我瞬间有了跳崖的冲动。仔细查看性能分析结果,我发现程序98%以上的时间在做文件IO(最初版本性能测试没有截图),于是我仔细查看了我的IO函数,实际上我的IO是在每生成一个数组的时候进行输出,由于写的比较快,就直接在函数内进行文件的打开和关闭了,所以输出1000个数独需要开闭文件1000次,我将文件流传入函数,在外面开闭文件,速度就上去了。此时100000个数独需要1分钟左右,仍然很慢,下面是这时的输出函数。

void SudokuOutput(int p[9][9], bool flag, fstream &file)
{
    if (!file.is_open())
        cerr << "fail to open file!" << endl;
    else
    {
        for (int i = 0; i < 9; ++i)
        {
            for (int j = 0; j < 9; ++j)
            {
                if (j == 8)
                    file << p[i][j] << endl;
                else
                    file << p[i][j] << ‘ ‘;
            }
        }
        if (flag)
            file << endl;
    }
}

再次进行性能分析,发现程序运行时间依然是文件IO占了大头,这是突然想到这里是直接将数字以整型输出到文件,如果我把它改成字符输出呢?当即把p[i][j]改成char(p[i][j]+’0’),发现输出快了十几秒。继续分析发现操作符<<和endl的耗时很长,查阅资料,这里endl的flush作用是不需要的,所以将endl改成file.put(‘\n’),前者同理,到这里再运行,100000级的时间是14秒。但是还是很慢。由于我选择的算法十分的暴力,所以我预期百万级的测试在1分钟内完成,十万级就耗时14秒是不行的。机缘巧合之下又看到了这篇文章,于是试用了freopen重定向和putchar的组合,运行时间优化到9秒,确实有效果。这时又看到微信群里罗老师建议保存答案到最后一起输出,于是我尝试建了一个全局字符数组,将所有终局都存进去,在最后的时候直接用fputs全部输出,这时运行100000级输出运行时间为:5.121s,百万级为:49.553s(用clock计时)。满足预期了……到这里Output函数已经面目全非,被我改成给输出的数组添加空格和回车的函数了,就不贴了,但是之前贴的关键函数都是最终版。下面是性能分析结果(-c 100000):

可以看到在优化IO后,IO占的时间很少了,现在最耗时的在于每次试填都要使用的check函数,用于判断同行同宫同列是否符合需求,我尝试过维护专门的数组来记录每行每列每宫已填数的信息,这样check函数就只需要查询这些数组了,但是实际测试下来和最简单的直接遍历行列宫相差无几,因为维护这些数组同样需要时间成本,所以最终按照原方案。

7.PSP各模块实际花费时间(略,见1)

8.感想

俗话说得好,不作死就不会死,暴力回溯生成数独终局确实是挺慢的,比不上各种取巧的方法,但是用来解数独我倒觉得是最实用也是最简单的方法,因为解数独是无法避免回溯和试填的。在写这个程序之前,说实话我没怎么用C++写过程序,计院的面向对象也还没上过,撑死了用C++解过几十道LeetCode,只能说略懂C++的语法而已,可以说是相当的菜了。前面写的优化其实只是对IO作了优化,对大佬们来说可以说是相当trivial了,但是对我来说,之前确实没有过处理这么多数据的情况,所以其实收获还是蛮大的,因为很多东西都是第一次用,包括VS和GitHub。最后,图简单暴力解题我觉得我大概要倒数了吧……

时间: 2024-10-08 03:00:15

2017BUAA软工个人项目之数独的相关文章

软工团队项目个人总结

经过了一个学期的软工课程学习,以及长期的团队开发,收获有下. 用户:创新就是极致的用户体验.在开发我们的这款游戏的开始阶段,我们与校内很多同学交流了一下他们对这款游戏的看法,并与他们在线下对游戏进行试玩,然后他们也对我们提出了很多意见,包括有些时候觉得我们某些地方设置的太傻了,随机性太大,博弈性不够等问题.而且有时候交流还会出现一些问题,但总的来说,我们还是从中挖掘了很多可以改进的点,分析了用户的需要,改进了挺多地方的规则的.然后,秉承着从软工课程上学到的,能让用户少点一下,绝不多点一下的类似的

软工2019_MucMuc项目个人总结

MucMuc项目个人总结 1.相关链接 原型界面设计链接 UML设计链接 github项目链接 2.项目个人分工 项目总体的部分设计 后端项目总体构建, 代码实现, 以及测试 阿里云后端服务器的配置和项目部署 3.开发过程 开始 在项目最初的阶段, 整个组对于要做怎样的工作并没有清晰的想法. 不知道如何开始工作, 从何做起, 开发工具为何, 是面临的最大难题. 因为没有任何有对于web开发有经验的成员. 从前后端开发工具的选择上, 到前后端通信的具体流程, 都没有一个较好的认知. 这也直接导致了

软工个人项目WC(Python实现)

一.github地址:https://github.com/1371272989/WC.exe 实现功能: 1.-c:统计字符数: 2.-w:统计单词数: 3.-l:统计行数: 4.-a:统计复杂数据(空行.代码行和注释行): 5.-s:递归处理目录下符合条件的文件: 通配符没有全面,只能辨别后缀. 6.-x:通过图形界面选择文件: 可以通过图形界面选择文件,但输出还是在cmd上显示. 二.PSP PSP Personal Software Process Stages 预估耗时(分钟) 实际耗

[2017BUAA软工]第一次个人项目 数独的生成与求解

零.Github链接 https://github.com/xxr5566833/sudo 一.PSP表格 PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟) Planning 计划     · Estimate · 估计这个任务需要多少时间 10   Development 开发     · Analysis · 需求分析 (包括学习新技术)  120   · Design Spec · 生成设计文档  60   · Design

BIT软工个人项目-数独

GitHub项目地址: https://github.com/FounDerSquare/SE-sudoku 一.任务 实现一个能够生成数独终局并且能求解数独问题的控制台程序. 提交的代码要求经过代码质量分析工具的分析并消除所有的警告. 对项目的首个版本使用性能分析工具找出性能瓶颈并改进. 及时维护仓库与博客. 二.PSP表格 PSP2.1 Personal Software Process Stages 预估耗时 (分钟) 实际耗时 (分钟) Planning 计划 30 l  Estimat

[2017BUAA软工]第0次个人作业

第一部分:结缘计算机 1.你为什么选择计算机专业?你认为你的条件如何?和这些博主比呢? 我觉得我选择计算机系完全是误打误撞吧.当时我的分数上北航是没问题的,所以填专业时就是机械,电气,自动化,计算机等专业一个一个报,反正都不了解,更不用说感兴趣吧.应该是计算机专业排在第一个然后就被录取了...不过我觉得我真的很幸运,来到这里才知道北航的计算机系是相当不错的. 大学之前没有接触过编程知识.我觉得高中时数学的程序框图可能是我接触的唯一的和程序有关的东西...在我们学校听说过物理竞赛,化学竞赛, 数学

软工实践项目课程的自我目标

对实践项目完成后学习到的能力的预期 组长说,攻坚安卓方向,那就希望首先懂得安卓这门语言吧 然后就是了解安卓应用的开发过程吧 对项目课程的期望 但愿难度不要太大,虽然越难越锻炼人,但我还是不希望难 有一定的补救机会就更好了 对项目的愿景规划 不懂,好好学习,天天向上!

[2017BUAA软工]第零次作业

第一部分:结缘计算机 你为什么选择计算机专业?你认为你的条件如何?和这些博主比呢?(必答) 我当初选择计算机,是因为:1.北航的前辈对北航计算机专业评价非常高:2.我也喜欢通过编程来代替我完成一些繁琐的任务(如,用按键精灵写简单的脚本来打游戏)3.我觉得计算机很神奇,怎么靠电路完成了这么多神奇的操作(如,IO.计算.存储等) 我觉得我的条件:1.肯定在dalao们面前毫无竞争力,不过也不至于就因此无法弥补差距2.在学习两年后,感觉当初dalao们的一些很666的知识,现在看来其实也不算太难掌握(

软工团队项目之项目选择

项目基础:考试练习系统APP 项目扩展:能让客户坚持每天做题的APP 需求分析:首先,用户会选择这个APP,那么必然有做题的必要和需求,也许是为了即将来临的考试也可能是真的为了做题给自己填补漏洞,那么不管是哪种情况,大量的刷题是免不了的,那么我们这个APP就有了市场需求. 对市面上已有的类似的APP分析:其实在市场上已经有了很多成熟的类似的APP,那么我们所要制作的就必须有我们的特色.传统的考试系统或是练习系统,基本上都是能从数据库中抽取一定量的题,组合成一套试卷,来供用户使用,而在用户提交答案