个人项目作业-数独生成与求解
项目地址
https://github.com/FelixCaae/SudokuGen
解题思路
一开始打算自己设计一个算法,因为之前也做过数独题目,对解题思路有一定的了解,打算用排除法加搜索来做,初步的思路是这样的,首先初始化一个空数独表格,给每个格子内填入可能的解,在没有任何约束条件的情况下就是1-9。然后将题目上的数字依次填入,每填入一个数字就依据约束条件对可能性做一些修改,比如(0,0)的位置填入了1,那么就从第一个宫内的其他数字单元和该行该列的其他数字单元中移除可能解1.如此反复,将题目填入完毕后,依次遍历可能解最少的单元来进行解的搜索,每次确定一个解就进一步压缩限制条件直到所有数字都被填完。
这个算法没有来得及实施,因为感觉很可能会性能不足,所以就上百度搜索了一下数独的算法求解,了解到一种以宫为单位进行填写的算法而不是以格为单位进行填写的回溯法和一种以数独的同质性为基础的交换算法(用于生成而不是求解)。Wiki上列举的各种方法更为详细,但是没有细看,决定先试试回溯法,并且用python写了一个版本用于验证。
数独入门解题技巧 http://ask.kedo.gov.cn/resource/stars/823575.shtml
回溯法介绍 http://blog.sina.com.cn/s/blog_a28e3dd90101e1i2.html
设计过程
这部分参考了之前OO的经验,把功能分给了4个类,参数处理类(ArgumentHandler)用于处理输入的参数,文件处理类(FileHandler)用于打开关闭文件和读写数独,表类(Table)用于记录数独数据并且提供生成和求解的方法,数独缓冲类(SdkBuffer)用于临时存储生成的数独。
类确定后,首先是设计公有方法,确定类的协同关系以及确保覆盖全部功能,其中比较重要的Table类试着使用了形式化的方法
1 class Table 2 3 { 4 5 private: 6 7 int cells[9][9]; 8 9 int total; 10 11 int top; 12 13 SdkBuffer* pCurrentBuffer; 14 15 public: 16 17 Table(); 18 19 /*@Modifies:this 20 21 Set all elements to zero ,which stands for not being filled. 22 23 */ 24 25 void SetZero(); 26 27 28 29 /*@Requires:0<=row,col<9 and 1<=num<=9 30 31 @Modifies:this 32 33 Set one element to the value given 34 35 */ 36 37 void Set(int row, int col, int num); 38 39 40 41 /*@Requires:length(num)>=9 and any element i in num[][] ==> 1<=i<=9 42 43 @Modifies:this 44 45 Set the table with the two-dimension array given. 46 47 */ 48 49 void Set(int num[][9]); 50 51 52 53 /*@Requires:total>0 and total <=100000 and sdb!=null and total<sdb->GetCapacity() 54 55 @Modifies:sdb 56 57 Generate sudoku solution and write to a file. 58 59 */ 60 61 void Generate(unsigned int total, FileHandler* fh); 62 63 64 65 /*@Requires:sdb!=nulls 66 67 @Effects:/result==true <==> We can find at least one solution 68 69 @Effects:/result==false <==> We can`t find any solution 70 71 Solve the problem in the table 72 73 */ 74 75 bool Solve(SdkBuffer* sdb); 76 77 78 79 /*Solve the problems in src buffer and write the answers to the dst buffer 80 81 Notice:it will clear the dst buffer firtst and clear src buffer as an effect 82 83 */ 84 85 void Solve(SdkBuffer * sdbSrc, SdkBuffer * sdbDst); 86 87 88 89 /*@Requires:total>0 and total <=100000 and sdb!=null 90 91 @Modifies:sdb 92 93 Generate sudoku solution to the sdb 94 95 */ 96 97 void Generate(unsigned int total, SdkBuffer* sdb); 98 99 100 101 /*@Requires:src!=null,dst!=null 102 103 @Modifies:src,dst 104 105 Solve the problem set given by the src and output the result to dst. 106 107 */ 108 109 void Solve(FileHandler* src, FileHandler*dst); 110 111 ~Table(); 112 113 private: 114 115 /*@Requires : 0<rst, cst<3 1 <= num <= 9 116 117 @Effects : / result == null <= = > there is one number in Palace(rst, cst) equals to num 118 119 @Effects : / result == {-1 } <= = > there is no suitable place for num in Palace(rst, cst) 120 121 @Effects : len(/ result) >= 1 && / result != {-1} <= = > there is one or more suitable place for num in Palace(rst, cst)*/ 122 123 int* lookUp(int rst, int cst, int num); 124 125 126 127 void solve(int subt, int num); 128 129 }
其他类的方法设计比较简单,缓冲区采用了栈的设计,参数处理器使用一个方法接受输入参数,并返回一个枚举型变量,文件读写类是对文件操作做了一个简单的封装。
单元测试及性能优化
这部分没有来得及做,确实时间没有安排好,直到周六才开始编码,而且周末组队的事情耽误了一些时间,这个作业其实还是很勉强完成的,之后得补上测试,优化的话可能得结对编程的时候再说了。
代码展示
关键算法
int* Table::lookUp(int rst, int cst, int num) { int *result=new int[10]; int index=0; int ron=0,con=0; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { ron = i + rst * 3; con = j + cst * 3; if (cells[ron][con] == num) { return NULL; } if (cells[ron][con] != 0) continue; bool pass=true; for (int t = 0; t < 9; t++) { if (cells[ron][t] == num || cells[t][con] == num) { pass = false; break; } } if (pass) { result[index++] = ron*9+con; } } } // printf("%d", index); result[index] = -1; return result; } void Table::solve(int subt, int num) { //this function works recursively to fill number 1-9 one by one to all the 9 sub tables //subt index subtable ,which is a 3x3 palace. It`s index starts from 0 to 8 //num (1-9) is the current number we try to fiil in // printf("%d %d ", subt, num); //if we get enough solutions ,just exit if (total == top) return; //It signals that all numbers are filled. else if (num == 10) { total += 1; pCurrentBuffer->Fill(cells); return; } //we should try the next number while that all palaces are reached else if (subt == 9) { solve(0, num + 1); return; } //Row or Column of SubTable int rst = subt / 3; int cst = subt % 3; //suitable cells int * suitcells = lookUp(rst, cst, num); if (suitcells == NULL) { solve(subt + 1, num); return; } int index = 0; //Row or Column of Number int ron = 0; int con = 0; while (suitcells[index] != -1 && total!=top) { ron = suitcells[index] / 9; con = suitcells[index] % 9; cells[ron][con] = num; solve(subt + 1, num); cells[ron][con] = 0; index++; } delete[] suitcells; }
lookUp()函数用于在某一个宫内为一指定的数寻找合适的位置,返回NULL代表该数已存在,空数组代表没有合适的位置等等。
solve()函数递归调用自己,依次填入1-9到九个宫里,直到发现无解或解的个数达到设定值为止。
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
Planning |
计划 |
20 |
15 |
· Estimate |
· 估计这个任务需要多少时间 |
20 |
15 |
Development |
开发 |
665 |
570 |
· Analysis |
· 需求分析 (包括学习新技术) |
200 |
120 |
· Design Spec |
· 生成设计文档 |
60 |
90 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
30 |
0 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
5 |
30 |
· Design |
· 具体设计 |
100 |
0 |
· Coding |
· 具体编码 |
120 |
300 |
· Code Review |
· 代码复审 |
30 |
30 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
120 |
45 |
Reporting |
报告 |
80 |
80 |
· Test Report |
· 测试报告 |
20 |
10 |
· Size Measurement |
· 计算工作量 |
20 |
10 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
40 |
60 |
合计 |
765 |
665 |
总结:
这次作业完成的比较失败吧,自己感觉,没有一开始就认真着手这些工作导致时间不充裕,容错率低是一个很大的原因。
其次是查询资料的环节,由于偷懒没有仔细比较各种算法的优劣,而是快速地凭印象选择,如果不是看到其他人的交流很可能不知道那些比较有效率的算法。
设计阶段没有很到位,只是花了几分钟画了一个草图,可能之后会表现出来一些问题吧。
代码复审,嫌麻烦,时间短,没有做,结果在调试上浪费了很多时间在一些小细节上.. 主要的算法明明先用python实现了一遍,几乎一模一样的写到VS里,本以为这块会是出错率最低的地方,实际上花了最多的时间来调。如果仔细审查一遍会好很多吧。
单元测试..没有做,所以可能还有很多潜在的漏洞。设计的时候就没有设计单元测试,真是一团糟..