Github项目地址:https://github.com/wym990806/031702425
前言
第一次编程作业,跟自己以往大一大二所要完成的东西根本就不是一个量级的。在看到这个题目之后,我已经有了一个基本的想法,只是怎么去实现的问题。真正阻止我及时完成这一次作业的反而是一些代码以外的,比如Git的使用、命令行传入参数、性能分析、单元测试等等。这些都是我之前没有接触过的,着实伤脑筋。
1、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 45 | 45 |
Estimate | 估计这个任务需要多少时间 | 15 | 15 |
Development | 开发 | 300 | 360 |
Analysis | 需求分析 (包括学习新技术) | 180 | 240 |
Design Spec | 生成设计文档 | 100 | 80 |
Design Review | 设计复审 | 120 | 100 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 100 | 80 |
Design | 具体设计 | 200 | 220 |
Coding | 具体编码 | 360 | 400 |
Code Review | 代码复审 | 60 | 100 |
Test | 测试(自我测试,修改代码,提交修改) | 100 | 120 |
Reporting | 报告 | 60 | 60 |
Test Repor | 测试报告 | 30 | 45 |
Size Measurement | 计算工作量 | 20 | 15 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 45 |
合计 | 1720 | 1945 |
2、解题思路
当入手数独这道题目的时候,我就已经决定好自己的初方案了,那就是采取深搜对整个盘面进行递归遍历。若给出的数独盘面有解,则此方法必定可以找出一解,但是该方法是无视其余格的可能性,直接从第一个格子开始操作,这就可能导致最坏情况下,需要从第一个格子递归到最后一个格子9次(第一个格子只能填9的情况下)。
3、几个主要函数
①、legal函数(合法性检测)
数独填入数字的规则是使填入数字在每一行、每一列还有每一宫上仅出现一次。下面给出一张图来更加直观地表示。
设计一个legal函数通过遍历当前格所在行、列、宫已经有的数字来判断填入该数是否合法。
/*legal合法性函数检测输入数字是否符合数独的要求*/
/*满足输入的数字在所在行以及所在列中仅出现一次*/
/*若宫格阶数为4689,则还需判断输入数字在所在宫中仅出现一次*/
/*输入参数分别为行、列、需判断的输入数字*/
bool legal(int i, int j, int x) {
/*行合法性检测*/
for (int b = 1; b <= m; b++) {
if (x == disksurface[i][b])
return false;
}
/*列合法性检测*/
for (int a = 1; a <= m; a++) {
if (x == disksurface[a][j])
return false;
}
/*若该阶数存在宫,宫合法性检测*/
if (m == 4 || m == 6 || m == 8 || m == 9) {
int interval_i, interval_j;
int begin_i, begin_j;
/*由于阶数的不同,每个宫所占的行列数也不相同*/
switch (m)
{
case 4:
interval_i = 2;
interval_j = 2;
break;
case 6:
interval_i = 2;
interval_j = 3;
break;
case 8:
interval_i = 4;
interval_j = 2;
break;
case 9:
interval_i = 3;
interval_j = 3;
break;
}
begin_i = ((i - 1) / interval_i)*interval_i + 1;
begin_j = ((j - 1) / interval_j)*interval_j + 1;
for (int a = begin_i; a < begin_i + interval_i; a++) {
for (int b = begin_j; b < begin_j + interval_j; b++) {
if (x == disksurface[a][b])
return false;
}
}
}
return true;
}
②DFS深搜算法
核心算法大概流程由以下流程图展示
算法主要思想:从第一格开始进行深搜,若深搜到最后一格,且当前格已经有数字的话,说明当前数独盘已经填写完毕,有解。若当前格数字为0,说明未填入数字,设置一个x,从1至m(设定阶数)进行合法性判断,合法地继续深搜下一格,若下一格无可填入数字,就当当前格清零,继续判断x的循环。这样循环到最后,若给定数独有解,必定可以给出一解。
/*以深度优先算法计算数独*/
bool DFS(int a, int b) {
while (disksurface[a][b] != 0) {
if (a == m && b == m) {
return true;
}
/*对于当前位置的判断,若处于行末,则行加一,列置为一,否则列加一*/
else if (b == m) {
a++;
b = 1;
}
else {
b++;
}
}
for (int x = 1; x <= m; x++) {
if (legal(a, b, x)) {
disksurface[a][b] = x;
if (DFS(a, b)) {
return true;
}
/*若执行到这边,说明下一格无可填入数字,将当前格清0,继续判断X*/
disksurface[a][b] = 0;
}
}
return false;
}
③程序代码
/*输入文件名以命令行参数传入。*/
/*Sudoku.exe -m 9 -n 2 -i input.txt -o output.txt*/
/*m宫格阶数,n待解答盘面数,i输入文件,o输出文件*/
#include <iostream>
#include <cstdlib>
using namespace std;
/*全局变量*/
int m; /*宫格阶数*/
int disksurface[10][10];/*宫格盘面,题设最大为九宫格*/
/*若待解盘面不唯一时,需要多次使用disksurface,进行重置操作*/
void reset() {
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= m; j++) {
disksurface[i][j] = 0; /*盘面上所有数重置为0,即未输入状态*/
}
}
}
/*legal合法性函数检测输入数字是否符合数独的要求*/
/*满足输入的数字在所在行以及所在列中仅出现一次*/
/*若宫格阶数为4689,则还需判断输入数字在所在宫中仅出现一次*/
/*输入参数分别为行、列、需判断的输入数字*/
bool legal(int i, int j, int x) {
/*行合法性检测*/
for (int b = 1; b <= m; b++) {
if (x == disksurface[i][b])
return false;
}
/*列合法性检测*/
for (int a = 1; a <= m; a++) {
if (x == disksurface[a][j])
return false;
}
/*若该阶数存在宫,宫合法性检测*/
if (m == 4 || m == 6 || m == 8 || m == 9) {
int interval_i, interval_j;
int begin_i, begin_j;
switch (m)
{
case 4:
interval_i = 2;
interval_j = 2;
break;
case 6:
interval_i = 2;
interval_j = 3;
break;
case 8:
interval_i = 4;
interval_j = 2;
break;
case 9:
interval_i = 3;
interval_j = 3;
break;
}
begin_i = ((i - 1) / interval_i)*interval_i + 1;
begin_j = ((j - 1) / interval_j)*interval_j + 1;
for (int a = begin_i; a < begin_i + interval_i; a++) {
for (int b = begin_j; b < begin_j + interval_j; b++) {
if (x == disksurface[a][b])
return false;
}
}
}
return true;
}
/*以深度优先算法计算数独*/
bool DFS(int a, int b) {
while (disksurface[a][b] != 0) {
if (a == m && b == m) {
return true;
}
else if (b == m) {
a++;
b = 1;
}
else {
b++;
}
}
for (int x = 1; x <= m; x++) {
if (legal(a, b, x)) {
disksurface[a][b] = x;
if (DFS(a, b)) {
return true;
}
disksurface[a][b] = 0;
}
}
return false;
}
int main(int argc, char * argv[]) {
int n; /*待解答盘面数*/
FILE *input;
FILE *output;
/*由于argv[0]固定为程序名,输入第一个参数为argv[1],以此类推*/
m = atoi(argv[2]);
n = atoi(argv[4]);
/*以只读方式打开input文档*/
input = fopen("input.txt", "r");
if (input == NULL) {
return -1;
}
output = fopen("output.txt", "w");
if (output == NULL) {
return -1;
}
fclose(output);
while (n > 0) {
reset(); /*执行写入操作前重置盘面*/
/*输入盘面*/
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= m; j++) {
/*fscanf()函数,从一个流中输入,遇到空格或者换行时结束*/
/*若使用fscanf会报警告,故更改为fscanf_s*/
fscanf_s(input, "%d", &disksurface[i][j]);
}
}
DFS(1, 1);
/*以追写方式打开output文档*/
output = fopen("output.txt", "a");
if (output == NULL) {
return -1;
}
/*将按输入文档格式将盘面数据写入*/
for (int i = 1; i <= m; i++) {
fprintf(output, "%d", disksurface[i][1]);
for (int j = 2; j <= m; j++) {
fprintf(output, " ");
fprintf(output, "%d", disksurface[i][j]);
}
fprintf(output, "\n");
}
fprintf(output, "\n");
n--;
fclose(output);
}
fclose(input);
return 0;
}
④测试
以下给出一些测试用例。其中三-八宫格各5例,九宫格10例。
4、Code Quality Analysis工具分析
当使用fscanf函数时,vs会报警告
然而使用fscanf_s函数时并不会。
5、程序代码改进
深搜算法虽然可以给出一解,占用可能会系统过多的资源,且别一次都从第一格开始深搜,不考虑格子的可能性,会造成多余的时间浪费。改进措施:加入一个结构体,记录每一个格子可填入的数字以及合法的数字个数,这样进行深搜是时,从可能性最少的格子开始,每次都对可能性最少的格子继续深搜。这样可以有效减少程序花费的时间。
具体代码还未实现,存在bug未剔除,故不放出,debug结束后会在此进行修改添加。
5、解决(?)作业后的感想总结
在看到第一次代码作业前,有设想过会完成得很困难,但真正看到题目、真正开始做之后才更让我感觉到无从下手、举步维艰。PSP表格是什么,其中各项内容代表了什么,有些事项自己以前都没有做过,要怎么开展?Code Quality Analysis工具是什么,在哪使用,该怎么用?性能分析工具Studio Profiling Tools又该怎么用,怎样从中找出自己程序的瓶颈?如何使用Github?怎样编写单元测试代码?如何将文件名以命令行参数传入?等等问题。其中有些在作业完成过程中基本学会了,还有些至今还未学会(作业截止后仍会继续学习)。总而言之,通过这一次作业,深刻理解到软件工程实践与以往代码课的不同。接下来的第一次结队编程作业,为了不拖队友的后腿,我会尽力跟上队友的节奏,学习新的知识,尽可能地利用空闲时间去提升自己。
(还是自己大一大二不争气,不说了,学习使我快乐)