软件工程第一次作业——数独的求解与生成

代码的GitHub地址:https://github.com/Liu-SD/sudoku

personal software process stages 预估耗时 实际耗时
计划    
  估计这个任务需要多少时间  10 min 10 min
开发    
  需求分析(包括学习新技术)  180 min 190 min 
  生成设计文档  0 min(没做设计文档) 0 min 
  设计复审(和同事审核设计文档)  0 min(没有同事复审) 0 min 
  代码规范(为目前的开发制定合适的规范)  30 min 60 min 
  具体设计  180 min 180 min 
  具体编码  240 min 240 min
  代码复审  60 min 180 min 
  测试(自我测试,修改代码,提交修改)  60 min 50 min 
报告    
  测试报告  180 min 180 min 
  计算工作量  0 min(没有做这项工作) 0 min 
  事后总结,并提出过程改进计划  30 min 30 min 
合计 1070 min  1160 min 

解题思路描述

题目要求分为两个部分,一个是解数独,一个是自动生成数独。并且生成数独时对第一个数字做出了限制。所以可以认为自动生成数独是只有一个数字限制时的解数独。所以说这两者大部分的实现算法是相同的。解数独时是在找到第一个解的时候返回真,而生成数独是在找到第N个解时返回真。

因此问题转换为在R个限制条件(1<=R<=81)的情况下寻找到N(1<=N<=1000000)个解时返回真,否则返回假。解的输出可以把输出流作为参数传入,找到一个答案时将答案输出。这个问题是典型的回溯问题,在网上查资料了解到可以将问题转化为精确覆盖问题,使用DLX(dancing link)算法实现。DLX的重点在于十字链表的实现。

设计实现过程

实现算法的第一步需要使用十字链表表示稀疏矩阵。我把十字链表封装为cross_link类,这个类,类中包括build方法,建立空的矩阵。build函数在构造函数中调用。稀疏矩阵建立以后,head指向矩阵头,rows数组存放行的头指针,cols数组存放列的头指针。类中包括insert方法插入元素。delrow,delcol,recoverrow,recovercol四个方法删除或恢复整行或整列。这四个方法将是算法实现的关键。在删除行(列)时,修改每一行(列)中元素的邻居节点到另一侧,同时保留被删除行(列)的邻居指针,方便恢复。删除和恢复需要以栈的方式操作。即后删除的先恢复。

算法被封装到dlx类中,dlx类调用并维护cross_link类的实例。类中的find_one和find_many实现解数独和生成数独两个需求。find_one调用_find_one递归函数,find_many调用_find_many递归函数。

_find_one函数流程:

1. 如果head的右结点为空,即矩阵没有列,则将result数组结果输出,stack中的所有删除操作使用recover做逆操作,即恢复为最初始的矩阵。返回真。

2. 否则,找到元素最少的列min_col_index,对于这一列的每一个元素i:

  将元素i所在行压如result栈中。

  对于元素i所在行的每个元素j:

    对于元素j所在列的每个元素p:

      删除p所在的行,压如stack栈中。

    删除j所在的列,压如stack栈中。

  以stack和result栈的栈顶指针为参数递归调用自己,如果为真,返回真。

  否则,把元素i从result栈弹出。

  把这一次压如stack栈中的元素弹出并且recover函数恢复删除的行和列。

函数最后返回假。

_find_many函数和_find_one类似,在找到结果时将结果输出到文件,计数器加一,在计数器等于目标数目之前都一直返回假。

十字链表的搭建容易出错,而且增删操作对于之后的算法十分重要,因此单元测试的重点放在了十字链表类的测试。设计单元测试时,先测试十字链表的插入操作,每插入一个元素,就计算元素个数并且断言。之后测试链表的删除行(列)以及之后行(列)的复原。

程序性能改进

我在程序性能改进方面没有做很多的操作,仅仅将输出到文件的部分从逐个数字写入改为了一个数独盘的一次性写入。但这个改进大大提升了程序的性能。生成一百万个数独从之前的五分钟变为当前的十四秒。

于是,进一步的,我修改了从文件读入的方式,将之前的逐个数字读入改为逐行读入。

生成一百万个数独时,VS性能分析图:

执行次数最多的函数为:

由此可见递归调用_find_many的执行次数最多,对链表的四个操作十分频繁,对其进行优化十分重要。

上图是递归调用的函数关系图。

代码说明

十字链表类的关键代码为删除和恢复行或者列。

void cross_link::delrow(int r) {
    for (Cross i = rows[r]; i != NULL; i = i->right) { // 对于该行每一个元素
        cols[i->col]->count--; // 删除后该列的计数器减一
        if (i->up)
            i->up->down = i->down; // 在它上方存在元素的情况下,上方元素改为指向该元素下方元素
        if (i->down)
            i->down->up = i->up; // 下方元素同理
    }
}

void cross_link::recoverrow(int r) {
    for (Cross i = rows[r]; i != NULL; i = i->right) { // 对于该行每一个元素
        cols[i->col]->count++; // 恢复后该列的计数器加一
        if (i->up)
            i->up->down = i; // 在上方元素不空的情况下,将原本指向自己下方的上方元素指回自己
        if (i->down)
            i->down->up = i; // 下方元素同理
    }
}

以上是行的删除和恢复一行元素的代码,删除恢复列的情形和一上代码类似。

dlx类的关键代码为函数的递归调用。以下是_find_many的代码:

 1 bool dlx::_find_many(int stack_top, int result_pos, int N, int & n, std::ofstream &fout)
 2 {
 3     if (!head->right) { // 在链表不存在列的情况下,
 4         int matrix[9][9];
 5         for (int i = 0; i < 81; i++) { // 将result数组里面的解翻译为9*9的矩阵
 6             int j = result[i] - 1;
 7             int val = j % 9 + 1;
 8             int pos = j / 9;
 9             int row = pos / 9;
10             int col = pos % 9;
11             matrix[row][col] = val;
12         }
13         char str[19 * 9 + 2] = { 0 };
14         for (int i = 0; i < 9; i++) { // 用字符串记录矩阵并输出到文件
15             for (int j = 0; j < 9; j++) {
16                 str[i * 19 + 2 * j] = matrix[i][j] + ‘0‘;
17                 str[i * 19 + 2 * j + 1] = ‘ ‘;
18             }
19             str[i * 19 + 18] = ‘\n‘;
20         }
21         str[19 * 9] = ‘\n‘;
22         str[19 * 9 + 1] = ‘\0‘;
23         fout << str;
24         if (++n >= N) // 计数器加一,如果大于要求的数量,返回真,否则返回假
25             return true;
26         else
27             return false;
28     }
29     int min_col_count = 100;
30     int min_col_index = -1;
31     for (Cross p = head->right; p != NULL; p = p->right) { // 找到元素最少的列
32         if (min_col_count > p->count) {
33             min_col_count = p->count;
34             min_col_index = p->col;
35         }
36     }
37     for (Cross a = cols[min_col_index]->down; a != NULL; a = a->down) { // 对于该列的所有元素
38         result[result_pos++] = a->row; // 将该元素的行号压如result栈中
39         int new_stack_top = stack_top;
40         for (Cross b = rows[a->row]->right; b != NULL; b = b->right) { // 对于该元素所在行的所有元素
41             for (Cross c = cols[b->col]->down; c != NULL; c = c->down) { // 对于该元素所在列的所有元素
42                 A->delrow(c->row); // 删除该元素所在行
43                 stack[new_stack_top++] = c->row; // 并记录下删除操作(删除行时压入正的行号,删除列时压入负的列号)
44             }
45             A->delcol(b->col); // 删除该元素所在列
46             stack[new_stack_top++] = -b->col; // 记录删除操作
47         }
48         if (_find_many(new_stack_top, result_pos, N, n, fout)) // 调用下一级函数,如果找到的数独数量达到了需求,则返回真
49             return true;
50         for (int i = new_stack_top - 1; i >= stack_top; i--) { // 否则将压入stack栈中的删除操作弹出,并且做其逆操作
51             if (stack[i] > 0)
52                 A->recoverrow(stack[i]);
53             else
54                 A->recovercol(-stack[i]);
55         }
56         result_pos--; // 最后将部分解从result数组中弹出,进入下一轮循环
57     }
58     return false;在循环完所有情况还没有达到数量需求的情况下,返回假
59 }

以下是_find_one的代码:

 1 bool dlx::_find_one(int stack_top, int result_pos)
 2 {
 3     if (!head->right) { // 找到一个解时记录答案并且直接返回真
 4         for (int i = stack_top - 1; i >= 0; i--) {
 5             if (stack[i] > 0)
 6                 A->recoverrow(stack[i]);
 7             else
 8                 A->recovercol(-stack[i]);
 9         }
10         return true;
11     }
12     int min_col_count = 100;
13     int min_col_index = -1;
14     for (Cross p = head->right; p != NULL; p = p->right) {
15         if (min_col_count > p->count) {
16             min_col_count = p->count;
17             min_col_index = p->col;
18         }
19     }
26     for (Cross a = cols[min_col_index]->down; a != NULL; a = a->down) {
27         result[result_pos++] = a->row;
28         int new_stack_top = stack_top;
29         for (Cross b = rows[a->row]->right; b != NULL; b = b->right) {
30             for (Cross c = cols[b->col]->down; c != NULL; c = c->down) {
31                 A->delrow(c->row);
32                 stack[new_stack_top++] = c->row;
33             }
34             A->delcol(b->col);
35             stack[new_stack_top++] = -b->col;
36         }
37         if (_find_one(new_stack_top, result_pos))
38             return true;
39         for (int i = new_stack_top - 1; i >= stack_top; i--) {
40             if (stack[i] > 0)
41                 A->recoverrow(stack[i]);
42             else
43                 A->recovercol(-stack[i]);
44         }
45         result_pos--;
46     }
47     return false;
48 }

程序实现完成。_find_one和_find_many存在代码冗余的现象,下一步修改目标是将冗余部分抽取出来或是将两个函数合并为一个。

时间: 2024-07-29 16:28:05

软件工程第一次作业——数独的求解与生成的相关文章

软件工程第一次作业补充

软件工程第一次作业的补充 对于作业"在一周之内快速看完<构建之法>,列出你不懂的5-10个问题". 作业要求有: (1)在每个问题后面,请说明哪一章节的什么内容引起了你的提问,提供一些上下文 (2)列出一些事例或资料,支持你的提问 (3)说说你提问题的原因,你说因为自己的假设和书中的不同而提问,还是不懂书中的术语,还是对推理过程有疑问,还是书中的描述和你的经验(直接经验或间接经验)矛盾? 例如:我看了这一段文字 (引用文字),有这个问题 (提出问题):我查了资料,有这些说法

软件工程第一次作业程序开发历程

收到软件工程的作业,面对题目“......”.我先拟定了一个大概的思路,以及一些关键的函数.思路利用循环产生30道算术式,并计算答案,存储答案,在利用循环显示30道题的答案. 而关键函数我认为就是随机正整数的产生.考虑到这点,我决定用javascript来编写我的程序,其一是因为最近学习js,其二是因为对c和c++有点陌生了,然后javascript有Math.random这个函数,尽管它只能产生0与1之间的随机数.决定完语言后,我又分析了下题目,考虑到涉及真分数的问题,我将程序分成整数与真分数

2017年秋季学期软件工程第一次作业(曹洪茹)

作业一 在开始作业要求的正文之前,我先简单谈谈自此课开课以来,包括读了许多大牛写的博文之后的几点感悟和思考. 首先,作为一名有四年地方大学生活经验的军校研究生,我很激动也很庆幸在研究生阶段能遇到这么一门真正实现本科教育改革创新,以培养学生思维逻辑能力.切实达到教学目标为为目的的课程.同时,比较讽刺的是,在崇尚思想自由.开放.创新的地方本科院校没有邂逅的这种课程反倒让我在军校这个相对封闭化.教条化的环境中接触到了,这主要得益于何老师对教育的前瞻性的战略眼光.其次,我对这个课的课堂模式是持支持态度的

软件工程第一次作业(2)

上次我们只是简单的聊了聊,这次我们举例以问答的方式详谈: 一,工具类软件:QQ 问:这些软件的开发者是怎么说服你(陌生人)成为他们的用户的?他们的目标都是盈利么?他们的目标都是赚取用户的现金么?还是别的? 答:早期不清楚,但是现在,只要你是中国人,估计几乎离不开QQ了,大家打开一台新手机的第一件事估计都是安装qq吧,因为他的用户太广了,我们的爸爸妈妈,同学朋友,老师同事,甚至爷爷奶奶现在都在用,几乎成了电话之后的第二种联系方式(我觉的这个意义已经超过了电话):QQ的盈利很广,会员制度:什么黄黑钻

对三类软件(游戏,系统,工具)的分析与心得(软件工程第一次作业)

软件有很多种,如工具类软件.游戏类软件.系统类软件,它们的运行方式也各种各样,如以单机方式运行.以网站方式运行或者以APP方式运行在手机端等,请选取三种软件,分析它们各自的特点. 1   这些软件的开发者是怎么说服你(陌生人)成为他们的用户的?他们的目标都是盈利么?他们的目标都是赚取用户的现金么?还是别的?2   这些软件是如何到你手里的(邮购,下载,互相拷贝……)3   这些软件是如何处理Bug 的?又是如何更新新版本的?4   同一类型的软件之间是如何竞争的? 这一类软件的发展趋势如何?5

集美大学1414软件工程第一次作业给分细则

说明:本次四则运算的评分细则如下(总分15分,附加分2分),希望大家能按照作业要求和评分细则对照自己的项目和博客,看看不足之处和合格支出都有哪些,以做到学有所得 组成部分 标准 给分 代码 可以通过-n -r 参数控制生成等式参数和数量 2 生成过程中计算出表达式的结果给出正确和错误答案数目 2 能够根据提供的相应文件给出重复的题目数目 2 支持10000道题目的生成 1.5 有规范的readme 0.3 提交的文件符合要求无缺失且无多余 0.2 以正确格式在各个文件中保存结果 0.5 等式输出

2017秋-软件工程第一次作业

1 自我介绍 我叫翟宇豪,目前是东北师范大学2017级研究生,本科专业是计算机科学与技术专业,研究生专业是计算机技术.选择计算机专业的初衷其实非常简单,高考之后,在当时的情况下,家人.长辈的推荐下,我认为金融行业和计算机行业将是最有发展的两个领域.我希望去沿海城市.南方城市.但是因为分数限制无奈选择了哈尔滨工程大学就读.我备选专业中大部分都和金融专业相关,但是哈工程是工科类院校,所以我选择了理工类的计算机这个专业由此走上了程序猿之路. 2对计算机专业的畅想 在本科学习过程中,有很多课程是与计算机

高级软件工程第一次作业

(1)回想一下你对计算机/软件工程专业的畅想 考研时你是如何做出选择计算机/软件工程专业的决定的? 如同D的博主一般,我也是一个偏科生,在英语.语文等语言方面和政治哲学方面有严重的不足,我在我还没有考上大学的时候,我就希望我学习的专业是计算机或者智能技术.电子等类别的专业,我的大学志愿也是这般选择的,但是不一样的是,我在高中才接触了计算机,那时什么都不懂,只觉得它很神奇,很美妙.上了大学,我读了网络工程,我很认真的学习了专业的知识.慢慢的时间一点一滴的流逝了,我在大三的时候还在想,我要不要去考研

软件工程第一次作业,小学生四则运算的出题程序

一.背景 阿超有个儿子上小学二年级,老师每天让家长给孩子出30道加减法题,虽然不多,但是每天都做也算是个负担,阿超作为一个老牌程序员当然想用计算机来解决这个小问题,目前对于这个问题对于任何语言都不是问题,比如: C/C++.C#.Java.Python.VB.JavaScript.Perl…… 具体要求如下: 能自动生成小学四则运算题目(注意是给小学生用的,要是结果出现负数的话他们会迷茫的!) 除了整数外,还要支持真分数的四则运算 请大家用任何一种自己擅长的语言来编写这段程序,并把程序的介绍和自