我的Github项目地址,使用工具VS2017社区版 / DevC++5.11,开发语言为C语言

基础题要求如下,附加题不会做就不贴出来了...

项目需求
利用程序随机构造出 N 个已解答的数独棋盘 。
输入
数独棋盘题目个数 N
输出
随机生成 N不重复的已解答完毕的数独棋盘,并输出到 sudoku.txt中

--引用自《第二次作业——个人项目实战


一、心路历程

说实话拿个这个作业,不对,应该叫做小项目,我是被吓了一跳。看着这满屏的字,加上Deadline 9月10日,本来基础就差的我顿时慌得要死(还好后来加时3天松了口气)。首先我没有《构建之法》这本书。无奈,火速跑到京东订购一本,然后开始研读项目需求。

我通读了一遍,就知道附加题的GUI界面开发的分我是别想要了,生成终盘数独和唯一解可填充数独还可以拼一拼。但是问题来了,什么是数独呢......我听说过,但是不确定。于是我又跑去百度,证实我的记忆是正确的。确认无误后,我开始想,怎么生成呢,怎么生成呢......坐在电脑前良久没有思路,我就在阳台转啊转。突然我灵光一闪,想到:得出的终盘是随机的,他每个格子的数字都要满足在行、列、宫内不重复出现,也就是说要随机生成数字,一个格子一个格子去填,然后加以条件判断,满足则填入,再下一个。这样,我脑子里飞速的转动,代码的轮廓开始涌现。于是我飞似的回到座位,先填好PSP表格,再打开DevC++,开始编码。

因为只是一个想法,在编写了5个函数(分别是一个接口,和判断行、列、宫数字是否重复的函数,以及一个打印函数)后,我先用的是DevC++来试验一下我的功能模块是否正确(万万没想到的是这居然和《构建之法》提到的单元测试不谋而合)

判断比较简单,下面只展示一下我的接口函数(之所以是a[10][10],是因为我想使用a[1]~a[9]来表示第1行到第9行)

//    行、列、宫判断函数均为bool型,数字有重复返回false,否则返回true

void sudoku_algorithm(int a[10][10])        //  生成数独的算法
{
    srand((unsigned)time(NULL));
    for (int row = 1; row <= 9; row++)
    {
        for (int col = 1; col <= 9; )
        {
            int  rand_num = rand() % 9 + 1;

            if(!sudoku_row(a, row, col, rand_num))   
                continue;              //    判断行                       
            if(!sudoku_col(a, row, col, rand_num))    
                continue;              //    判断列                          
            if(!sudoku_mod(a, row, col, rand_num))  
                continue;              //    判断宫                          

            a[row][col++] = rand_num;
             //printf("%d ", a[row][i]);
        }
        for(int i = 1; i <= 9; i++)
            printf("%d ", a[row][i]);
      //putchar('\n');
    }
}

然后我在main()函数 int a[10][10] = {0},再调用这个函数,结果出现了下面这张图的情况

1213675-20170909161206366-872495099.png

没错,它卡住了......我检查了前面的数字,发现都满足数独条件,但是到第6行就卡住了......我又尝试了几次输出,发现有时可以完整地输出一个数独!但是有时就和这个一样卡死在某一行。我在心想:不对啊,就这张图来说,我在第6行第一个数字填入4或7是没有问题的啊,为什么???马萨卡......

于是我把打印语句换在了里面,每找到一个数字就打印出来,第二个循环外再加个换行。结果和我想的一样,真实的情况应该是这样的

1213675-20170909162238132-954929554.png

也就是说在某个格子里,rand_num赋值数字1~9填入均不符合数独的条件,这样就会陷入死循环

怎么办呢?我看着屏幕前的代码,思绪再次打转。我冥思苦想了半天,觉得这样那样也不是办法,索性再加一个test函数,先把1~9的数字全部试一遍,看这个格子能不能填,不能填的话,索性把这一行重置为0,从头开始。说干就干,不一会儿这个函数就写好了,我把它放在了sudoku_algorithm()函数的int rand_num的下面


bool sudoku_test(int a[10][10], int row, int col)               //  判断这个格子能否填入 1~9
{
    int count = 0;

    for (int i = 1; i <= 9; i++)
    {
        bool flag1 = sudoku_row(a, row, col, i);
        bool flag2 = sudoku_col(a, row, col, i);
        bool flag3 = sudoku_mod(a, row, col, i);

        if (flag1 && flag2 && flag3)            //  说明数 i 可以填入格子            
            break;
        else
            count++;
    }

    if (count == 9)                     //  说明 1~9 都不能填入
    {
        for(int i = 1; i <= col; i++)
            a[row][i] = 0;      
        return false;
    }  
    else
        return true;
}

再次运行之前,我心里并没有底。但令人欣慰的是,这次生成了完整的数独

高兴之余,我并没有忘记多测几次,以防刚才那种情况。在我把生成的数独数量调到10时,我发现我输出的10个数独时一模一样的!WTF?虽然我每次点运行生成的数独不一样,但数量大于1时所有的数独都一模一样了。我以前见过这种情况,就是没有设置随机数种子或是种子一样所导致的,可我明明设置了啊......我人傻了,这个百度也解决不了,我开始向大神们求助,得到了统一回复就是“我还没做呢,不是还早嘛......”。果然大神们敲这种简单的题目都是几个小时搞定的。我长叹一口气,又试着保存数独的第一个数,并与新生成的数独的第一个数做比较,相同则不输出,重新开始。

我还是蛮高兴这样做的话我生成了不同的数独。但是在控制台,几乎是每隔1秒才输出一个数独,这和之前比简直差远了。但是每隔1秒一个数独让我猜想会不会是随机种子是每隔1秒刷新一次,而我生成数独速度太快导致输出一样的数独。我到CSDN发了一贴,得到了如下回复

srand在哪里?
不能放在循环体内以及循环体内的函数内,因为代码执行速度很快,即便你是在循环体内执行srand(time(0))时间种子基本不会改变导致伪随机数是一样的。 ---By sdghchj

--引用自《随机生成多个数独的程序问题

这样我的疑问得到解决,就是把srand()函数放到主函数,也就是接口的前面去......(换个位置问题就解决了,这真的是......)

然而事情不可能会那么顺利的,尤其是对我这种菜鸡来说。大概我运行.exe每10次,就会出现最开始那个情况的卡死。我用最开始的方式测试,这次却没有出现卡死在中间的情况,都是卡死在了每一行的第一个格子。然而令我不解的是,明明同样的,我能在这个格子里填入数字,他就是过不去。

一个大胆的想法出现在我的脑海,会不会9!== 362 880 种排列方式在这一行都行不通?我没法一种一种去验证,因为如果是这样我只能说我这个算法实在是烂。于是我只能认为是这样,就给int了一个变量fail_times = 0,每次出现重置行的操作都自加1,当超出一个定值(自己设定,先设置为2000)时,就把上一行也全部重置。这个fail_times设在第一个循环和第二个循环中,以下代码加在第二个循环里

if (fail_times > 2000 && row > 2)
{
    for (int i = 1; i <= 9; i++)
    {
        a[row][i] = 0;
        a[row - 1][i] = 0;
        row -= 2;
        break;
    }
}

之后经过无数次测试,发现都能生成正确的结果了,我开始把代码放到VS上。按照VS的特性稍作修改,再加入读写文件的函数,开始测试。

我使用DEBUG模式下的性能分析,这样每个函数的调用次数都会显示出来,但结果让我非常不满意。我生成1000个数独耗时是71秒钟性能分析报告和截图被我误删了),这大大超出了题目要求的“1000个不超过60秒,否则0分”。我将视图调到“函数”模块,发现sudoku_test()调用的次数最多,达到了百万次。而每个test()中在最差的情况下会调用row(),col(),mod()个9次,共计27次,这直接导致了这三个函数调用次数也非常大。

我开始着手修改代码。我想:判断格子能否填数是肯定要判断的,不然之前写的都白费了。但是......嗯,如果这个格子能填数,那么即使随机产生的rand_num不符合要求,那下次也不必再调用sudoku_test()了。于是我bool了一个flag = false,若test()为真,即能填数,则把flag赋值为true,再在test()函数前加一个判断语句,如此即可实现我的想法。这样跑下来,使用性能分析得出的时间是11秒左右。

此外,fail_times上限值对时间也有影响。在if(fail_times>2000)的情况下,程序跑了11秒;在if(fail_times>500)的情况下,程序跑了30秒;在if(fail_times>100)的情况下,却只有6秒。次数再少一些,比如50,时间只有一点点的减少,但是会出现卡死的情况。因此最后我把fail_times的上限值定为了100。

以上展示的代码是编程过程中的思路,完整代码请到我的Sudoku项目下载查看

这之后的工作便是一些修修补补了,比如减少文件打开关闭次数,以及为命令行而修改的版本,如此可以降到5秒,而如果使用DevC++或是命令行执行,生成1000个数独仅仅需要零点几秒的时间。虽然和生成100万个只要30几秒的大神没法比,但能从71秒降到6秒我也很满意了。这些修改过程我都以注释的方式记录在了main.cpp中。

/* 9月11日凌晨更新,修改行、列、宫判断任意一个不满足即执行,而不是全部判断完再执行。如此生成1000个数独,性能分析从5.9秒到4.4秒;命令行生成从0.139秒到0.105秒。观察性能分析的函数表发现,变化较大的是:行判断row()增加了10万次,列判断col()减少了80万次,宫判断减少了150万次(原来全部调用了220万次) */

/* 9月25日早上更新,虽然因为没有认识的人的原因退出了实践课程,但是作业还是按要求修改了,数独左上角的数字已经固定 */


二、测试运行

为了避免打印数独、system("pause")、scanf_s()之类函数对时间的影响,在测试阶段,默认生成1000个数独,且只输出提示信息和打开文件、得到数独、写入数独、关闭文件的时间

结果如下:
1213675-20170909183008741-1057608042.png

1213675-20170909183229476-663267915.png


三、性能分析与PSP

1213675-20170909183656476-1402579731.png
1213675-20170910105532007-1803261253.png
1213675-20170909183707694-109085837.png

可以看到的是sudoku_test()依然是占了大头,不过我也想不到什么好方法进行优化了,就暂时先搁一搁吧。

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

三、总结

从发布作业的那一刻开始,我就知道暑假已经结束了(虽然我也没回家)。其实如果有思路的话,编码工作其实很快就能完成,即使是我这种所有编程课都在及格线飘过的人。我大部分的时间都花在了熟悉GIT的使用和新的IDE(VS)及其工具上(我还是不知道C怎么用单元测试)。在一次一次的错误和失败中,我用了最笨的方法(打印输出)去找原因,以后或许得学着用DEBUG?在一次一次的代码优化中,切切实实地看到了程序时间的减少。虽然我没法和100万个数独只用30几秒的大佬相比,但对我自己来说,或许就是一种进步了吧。

至于附加题的GUI,实在超出了我的能力。而生成唯一解的未填充数独,我有了思路,就是写一个解数独的模块,然后挖格子,用程序返回他的解的个数,不为1就继续挖。但是遗憾的是我发现我的程序随着格子越来越多,解也越来越多......我问大佬,大佬自己也很忙,故就此作罢了。

第N周新增代码(行)累计代码(行)本周学习耗时(小时)累计学习耗时(小时)重要成长
02602601111学会调试、优化方法

转载于:https://www.cnblogs.com/binghuangxuewu/p/7498378.html

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐