我是靠谱客的博主 优秀黄豆,最近开发中收集的这篇文章主要介绍软件工程基础 大作业,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

本文所有内容基于markdown,如果不能正常显示,建议卸载浏览器并安装最新版本。

原创内容,转载请注明来源https://www.cnblogs.com/zhber/p/10016909.html

  1. 仓库 https://github.com/zhber/Sudoku/

  2. 设计PSP表格(见7)

  3. 解题思路描述

    代码要实现的功能分两部分,一是计算给定数独局面的解,二是生成(T)不重复的数独解,(T)的规模(1e6)

    • 对于第一部分,注意到给定局面的一些位置可以由同一行/列/块已确定的数字推出来,因此先暴力松弛,即枚举每个位置,遇到可以确定的位置就填上去,并重新开始一轮松弛。如果都没有可以确定的位置了,松弛环节结束。显然松弛操作不会超过(81)次。接下来的位置填法不唯一,没有太好做法,直接(dfs)爆搜所有可能情况。

      • 另解:考虑问题转化为精确覆盖问题,即:给出一个二维的只包含(01)元素的矩阵,选出一些行的集合,使得集合中每一列有且恰好有一个(1),如可以一起选(0101)(1010),但不能一起选(0101)(1110/0010)。那么可以把这个问题转化一下变成(729)(364)列的(01)矩阵的精确覆盖问题,然后套一个dancing link模板即可。因为精确覆盖问题是要每一列都有恰好(1)(1),令前(81)列表示数独(81)个位置必须都填上,再(81)列表示数独(9)行的(9)个数字都要用上,再(81)列表示数独(9)列的(9)个数字都要用上,再(81)列表示数独(9)块的(9)个数字都要用上。对于每个位置的每一种填法建一行,把这个位置的这种填法对应的位置、对应行的数字、对应列的数字、对应块的数字的那(4)列填(1),其他位置填(0),跑一遍dancing link即可出解。
    • 对于第二部分,要求生成的是很多不重复的数独解,输出很多个 不重复解是问题所在。

      注意到对于一个合法的数独解,我们把任意一对数字的位置互换,比如把所有数字(2)和数字(9)的位置互换,就可以得到一个新的数独解。那么一个解实际上给出了一种分配数字的方案,打乱编号顺序能构造出的不重复解有(9!=362880)个。再考虑合法解旋转(90°)(180°)(270°)后同样是合法解(但不一定不重复),以及交换同一块内的两行或两列同样是合法解(也不一定不重复),能构造出的解有(9!*4*(A_3^2)^6=67722117120)种,远大于(1e6)。当然,这里面肯定有不少重复情况,但拿出(1e6)种不重复情况绰绰有余。因此问题只在于生成初始的合法数独解。可以考虑随机生成几个残局,然后用第一部分的算法跑几个合法解来使用,无解就多跑几次,直到有解。也可以考虑打表,预处理几个合法解放在配置文件里,运行时从里面读取。

  4. 设计实现过程

    第一部分,套个DLX模板即可。DLX不是很方便,最好能将这部分封装好,然后直接调用接口。

    第二部分,代码里预留一个写死了的合法数独解,通过调换把(2)翻到左上角,然后把(13456789)几个数字轮换,外面再套一个四到六行和四到六列的任意调换,就是(8!*(A_3^2)^2=1451520)已经够了。

    详细的需求分析、设计可参阅仓库里的 设计文档.docx

    此处列举一部分设计内容,仅供参考

    一、主模块:实现程序的基本功能。
    1、   init()函数
    介绍:为实现功能,程序需要一个初始化函数,用于处理和检测输入、判定操作类型等。
    原型:int init(int argc,char** argv);
    参数说明:把主函数的参数原样传入,以处理输入。
    返回值说明:返回值为0,代表执行一个’-c’操作。返回值为1,代表执行一个’-s’操作。
    2、   work_c()函数
    介绍:处理’-c’操作的代码部分,用函数封装,而不用在主函数中堆砌。
    原型:void work_c(int n);
    参数说明:传入数据是一个数字n,表示要生成的数独个数。
    返回值说明:无返回值(void)。
    3、   work_s()函数
    介绍:处理’-s’操作的代码部分,用函数封装,而不用在主函数中堆砌。
    原型:void work_s();
    参数说明:无输入。由于输入输出改版,文件路径不需要再作为参数传入。
    返回值说明:无返回值(void)。
    4、   class Sudoku 数独类型
    介绍:用于存储单个数独信息的结构体。
    原型:
    class Sudoku{
    public:
     int d[9][9];
     Sudoku(){};
     void init();
     void out1();
     void out2(); 
    };
    成员说明:
    public int d[9][9]; 一个存储数独内数字的二维矩阵,取值范围为0~9。1~9表示已经填入对应数字,0表示未确定此处的数字
    public Sudoku(){} 构造函数,数独创建时自动把一个已经填入数字的样例数独赋值给它
    public void init(); 初始化函数(从文件中输入)。用于从文件中读取给定的数独局面。由于输入输出改版,文件路径不需要作为参数传入
    public void out_s(); 输出函数,适用于’-s’操作的输出函数
    public void out_c(); 输出函数,适用于‘-c’操作的输出函数
  5. 记录在改进程序性能上所花费的时间,描述你改进的思路,展示性能分析图,并展示你程序中消耗最大的函数。

    ​在改进程序性能上花费了很多时间。

    12.13:添加了fastIO来优化,同时代码大修,加了很多static/register以及某些常数优化来加速。

    12.16:尝试学一下汇编+底层优化,失败了,怎么都过不了编译……

    12.17:这几天陆陆续续改好了第二版fastIO,删除了几乎所有冗余的代码,感觉没有可以优化的了。

    12.28:在最后审阅的时候发现原来都是用VS的Debug模式生成可执行文件,换成Release后发现快了很多……

    以下为第一版代码的性能分析图(函数视图,按采样样本数从大到小排序):

    性能分析0

    性能分析1

    [ucrtbased.dll]不知是什么,应该是某个系统函数的动态链接库。但是可以看到Sudoku::out函数即输出函数的占用率极高,达到90%以上。这说明输出较慢时一个很重要的瓶颈。

    因此考虑优化主要以加速I/O为主。

    • 为了加速输入输出,可以考虑把fprintf的格式化输出数字替换成fputc输出单个单个的字符。
      • 结果:有一点效果,但是还是比较慢。
    • 考虑使用更底层的fwrite/fread实现I/O,那么要自己写一套缓冲区的输入输出。参考了鄙队的ACM模板写出第一版fastIO.hpp。
      • 结果:果然快了很多。
    • 然后陆陆续续加了一些常数优化,比如一些局部变量移出来变成全局变量再加static,不能移出来的加register,嵌套for里定义的变量移出来,把一些if用:?替换,一些加法尽量用位运算替换,等等。
      • 结果:感觉不明显。
    • 陆陆续续搞出第二版fastIO.hpp,删掉了很多没用的细节。
      • 结果:快了大约30%,但是没有第一版一下快那么多那么舒服了。
    • 令人窒息的操作:把Debug切换到Release
      • 结果:握草快了好多啊!感觉时间只有原来的四分之一了
  6. 代码说明

    此处选择一些有代表性的代码。

    void Sudoku::init()//初始化
    {
     register int i, j;
     for (i = 0; i < 9; i++)
         for (j = 0; j < 9; j++)
         {
             if (read(d[i][j]), IOerror)
             {
                 if (!i && !j && ~IOerror) return;
                 else
                 {
                     puts("Error : not valid input.");
                     exit(23333);
                 }
             }
         }
    }

    此代码用来从文件中读取一个完整的数独。

    首先局部变量从for嵌套中提出来而且加register,不用解释。

    函数调用了fastIO::read读取了81次数据,然后保存在二维数组Sudoku::d中。

    逗号运算符的返回值为后者,如(a,b,c)的返回值为c。因此在if中判断的就是fastIO::IOerror。这里纯属装X写法。当出现读入错误时,根据fastIO的实现,会有两种情况:一是读到了非法字符,即({0...9,' ','backslash t','backslash n'})以外的字符。另外一种情况是读到了文件尾EOF。第一种情况fastIO=-1,不进入后面的第二个if判断。唯一的合法情况只有读到文件尾时刚好读完,此时应该是恰好下一个数独的第一个位置读取就报错,即(i=j=0),并且fastIO=1.此时进入if判断,会直接退出函数。否则直接报错并且程序报异常退出。返回值23333意义请见设计文档的约定。

    inline void work_c(int n)//处理参数是-c的情况
    {
     refreshOut("sudoku.txt");
     if (n <= 0)return;
     Sudoku now;
     std::swap(a[1], a[now.d[0][0]]);
     std::swap(a[1], a[2]);
     register int i, j, k;
     for (i = 1; i <= 6; i++)
     {
         for (j = 1; j <= 6; j++)
         {
             for (k = 1; k <= 40320; k++)
             {
                 now.out_c();
                 if(!(--n))return;else print('n');
                 std::next_permutation(a + 2, a + 10);
             }
             std::next_permutation(col + 3, col + 6);
         }
         std::next_permutation(row + 3, row + 6);    
     }
    }
    

    此代码用来执行-c操作,参数为(n)

    fastIO::refreshOut("sudoku.txt")用于将输出流重定向到一个相对路径sudoku.txt里。

    if (n <= 0)return 这句意义不大,因为init()初始化已经判定不合法的n退出。

    Sudoku now 初始化一个数独,构造函数将会把写死的一个模板复制上去。

    初始(a_i=i),两个std::swap的作用是把学号对应的数字(2)换到数独左上角首位,同时保证剩下的(8)个数字恰好在(a_2...a_9)

    此后三个循环,for irow数组控制数独第4到第6行的轮换,for jcol数组控制数独第4到第6列的轮换,for ka数组控制数独剩下8个可移动数字的轮换。std::next_permutation是得到下一个排列。在循环内部,每次执行n--和输出结果,变换结果可以根据a/row/col数组直接得到,即原来输出d[i][j]此时直接输出a[d[row[i]][col[j]]]。当n=0时直接退出。

  7. 完成PSP表格

    PSP 2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
    Planning计划
    · Estimate· 估计这个任务需要多少时间23
    Development开发
    · Analysis· 需求分析(包括学习新技术)6045
    · Design Spec· 生成设计文档120240
    · Design Review· 设计复审(和同事审核设计文档)6010
    · Coding Standard· 代码规范(为目前的开发制定合适的规范)1010
    · Design· 具体设计12090
    · Coding· 具体编码7201200
    · Code Review· 代码复审12060
    · Test· 测试(自我修改,修改代码,提交修改)720480
    Reporting报告
    · Test Report· 测试报告180180
    · Size Management· 计算工作量2010
    · Postmortem & Process Improvement Plan· 事后总结,并提出过程改进计划12030
    合计22522358
    一些感悟
    • 软件工程确实是一门很难的学科啊……感觉写代码和优化很简单,但是写文档真是要了我的命,尤其是画图
    • 我猜这是个敏捷开发过程,因为我的需求一直在变,想到啥加啥(逃
    • 感受到设计文档的好处,虽然这是第一版出来之后补上的。确定了函数接口有助于检查代码安全性和防止溢出,但是之后不能随便乱改了,算是有利有弊吧
    • 感到Windows跑这种运行时间很长的代码不是很友善啊……大概是因为内核调度的原因每次跑出来时间都不一样,而且波动很大啊,我应该取一个平均数吗?
    • 造数据的时候,又来了——“这代码怎么会还有问题?”——“握草我又把自己代码X掉了”,感觉还是要学习一个
    • Visual Studio真辣鸡!Mingw大法好!
    更新日志
    2018.11.25
    • 更新一个补全数独的demo,过了poj2676,正确性应该没问题了吧?
    2018.11.26
    • 发现并没有那么简单……原来要先做需求分析……心态爆炸
    • 推倒重写
    • 我真的好菜啊
    • 补上psp表格,填了个看上去很合理的预期时间(应该算合理吧?)
    • (设计文档怎么写啊?我不会啊?)
    • 完成解题思路描述
    • todo:
      • 设计文档 不会写也要硬着头皮写
      • 解题思路写得太长,要砍掉一点
    2018.11.27~12.02(补)
    • 忙着各种事,啥也没干
    • 我真的好菜啊
    2018.12.03
    • 我真的好菜啊……时间真的来得及么
    • 赶紧发挥人类智慧弄了一个看上去还行的技术文档
    • 设计复审是什么啊?就是给别人看一眼吗?
    • 把dancing links封装到dlx.hpp里面
    • 然而被文件路径的判断制裁了……并没有拼出来一个能跑的.exe
    • 感觉判断文件路径是否合法好难啊?不会啊
    2018.12.04
    • 试着用regex搞一发文件路径判断,感觉似乎很有道理
    • 怼出来一个能跑的exe了……终于敢commit一下push了
    • 测了下-c的极限数据,感觉读写实在太慢
    • 我好菜啊
    • todo:
      • 太慢了 弄个fastIO上去
      • 测一下-s的极限数据是不是很爆炸
    2018.12.05~12.13
    • 摸鱼,并且忘记写文档辣
    • fastIO.hpp 断断续续弄好了,感觉跑得飞快 -c 1e6 只要6.5s
    • 感觉dancing link还是慢啊……不知道该怎么办啊 -s的满数据还是170秒
    • todo
      • 用mmap再改一改fastIO(我要跑的比香港记者都快
      • 试下IDA*爆搜或者其他什么解数独的算法,如果比dancing link 更快就换了吧
      • 尝试着写一下各种文档
    2018.12.14~12.30(补)
    • 汇编优化失败
    • 感觉剪枝爆搜并不会比dancing link更快,有点难受了
    • 开发fastIO 2.0完成,感觉快了一小些
    • 把Debug换成Release,发现怎么时间变成原来四分之一啊?
    • 我好菜啊
    • 改了改设计文档
    • 构造测试用例

转载于:https://www.cnblogs.com/zhber/p/10016909.html

最后

以上就是优秀黄豆为你收集整理的软件工程基础 大作业的全部内容,希望文章能够帮你解决软件工程基础 大作业所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部