• 软件工程基础 大作业


    本文所有内容基于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,' ','ackslash t','ackslash 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('
      ');
      				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.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
      Planning 计划
      · Estimate · 估计这个任务需要多少时间 2 3
      Development 开发
      · Analysis · 需求分析(包括学习新技术) 60 45
      · Design Spec · 生成设计文档 120 240
      · Design Review · 设计复审(和同事审核设计文档) 60 10
      · Coding Standard · 代码规范(为目前的开发制定合适的规范) 10 10
      · Design · 具体设计 120 90
      · Coding · 具体编码 720 1200
      · Code Review · 代码复审 120 60
      · Test · 测试(自我修改,修改代码,提交修改) 720 480
      Reporting 报告
      · Test Report · 测试报告 180 180
      · Size Management · 计算工作量 20 10
      · Postmortem & Process Improvement Plan · 事后总结,并提出过程改进计划 120 30
      合计 2252 2358

    一些感悟

    • 软件工程确实是一门很难的学科啊……感觉写代码和优化很简单,但是写文档真是要了我的命,尤其是画图
    • 我猜这是个敏捷开发过程,因为我的需求一直在变,想到啥加啥(逃
    • 感受到设计文档的好处,虽然这是第一版出来之后补上的。确定了函数接口有助于检查代码安全性和防止溢出,但是之后不能随便乱改了,算是有利有弊吧
    • 感到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,发现怎么时间变成原来四分之一啊?
    • 我好菜啊
    • 改了改设计文档
    • 构造测试用例
  • 相关阅读:
    第四章——64位软件逆向技术-基本语法(上)
    第三章——静态分析技术-IDA的简单操作
    第二章——动态分析技术-OD常见问题
    工厂模式及其抽象工厂
    设计模式-原则
    设计模式-简单工厂模式
    设计模式-桥接模式
    设计模式
    Linux下安装软件心得
    光驱挂载和下载
  • 原文地址:https://www.cnblogs.com/zhber/p/10016909.html
Copyright © 2020-2023  润新知