博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【20170926】【软工】第一次个人项目——数独
阅读量:4932 次
发布时间:2019-06-11

本文共 9122 字,大约阅读时间需要 30 分钟。

一、项目源码的github链接

(因为使用github不熟练-_-,在sudoku仓库关联了项目文件夹,导致大小接近100M。因此新开sudoku_onlycode仓库来最终提交。为保留提交历史,sudoku仓库未删除)

二、需求分析

  • 生成数独
    命令:
    sudoku.exe -c n
    要求:
    1.输出到sudoku.txt
    2.生成的数独不重复
    3.1<=n<=1000000,支持前置零以及前置加号,不支持运算式
    4.可以处理异常情况,如:sudoku.exe -c abc
    5.左上角第一个数为(学号后两位相加)% 9 + 1=(8+9)%9+1=9
    6.参数不合法输出异常信息到控制台
  • 求解数独
    命令:
    sudoku.exe -s absolute_path_of_puzzlefile
    要求:
    1.输出到sudoku.txt
    2.数独中的0代表空格
    3.文件中数独题目数为n,1<=n<=1000000
    4.保证格式正确
    5.对每个数独题目求出一个可行解
    6.参数不合法输出异常信息到控制台

三、解题思路

刚拿到项目时,自然地将其分为生成终局和求解数独两部分。

生成终局

开始时对生成终局没什么思路,通过查阅资料找到这样一篇,由博主的方法以及和室友们的热烈讨论,发现可以生成1-9中八个数的全排列,暂且将其定义为种子数组,用数组索引替换的形式,将一个已知数独变换成另外8!-1个不同的数独,因为[0,0]元素确定,所以是8!-1。这样距离我们要求的1000000个数独还有很大差距,还是经过查阅资料后,找到了一篇,其中介绍了基于矩阵变换的数独生成方法,这里我只采用了行变换的思路,需要保证的是:交换只发生在前三行,中间三行,最后三行之间。而不能越界交换,比如第一行和第四行交换就是不允许的。但是这种方式随机性较差,而且每次运行程序的原始矩阵都相同,所以我进行了一些改进: 在进行索引替换前,对原始的种子数组进行一次索引替换,生成一个初始种子,用这个初始种子对祖先数独进行索引替换得到初始数独。对这个初始数独进行索引变换加行变换。这样可以保证每次开始的初始数独不一样。

求解数独

暴力回溯的想法很成熟,但是还是比较关心有没有更好的方法,依然是查阅资料后,找到了DLX算法,即将问题转化为精确覆盖问题求解。但是考虑到自己的水平不高,实现了DLX可能没有时间进行充分的优化,而未优化的DLX会比暴力慢好多。思前想后还是决定写暴力。。。在空白格的表示方法以及去除候选数的方法上,参照了。主要想法是将数字用类似二进制的方式表示,即1=000000001,2=000000010,……, 9=100000000, 0=111111111,即每一位代表这个位置取值的可能性。取出一个数字可能性可以用取反做位与的方式,例如这个空白位置不能取2,则有111111111 & (111111101)=111111101,这样就去除了2的可能。

四、设计实现过程

我使用了两个类来进行数据的组织以及方法的实现。现详细说明:

EndProducer类

  • private属性:

int _ancestorMartix[81]: 祖先数独

int _originalMartix[81]: 原始数独
int _seed[9]: 原始种子数组
int _initialSeed[9]: 初始种子数组
int Nums: 要生成的数独数量

  • public方法:

void Swap(int* a, int* b):交换方法

int* ROWijk(int* a):行变换方法(i,j,k的排列方式代表交换方式),传入的是数独数组首地址,返回的是交换后的数组地址。

int* getInitialSeed():得到随机的初始种子

bool DuplicateCheck(int* a, int aim, int count):重复性检查方法。检查地址为a的数组中是否存在aim元素,count表示当前的数组长度。

bool IndexSubstitution(int* seed, int* a, int* b, int len):索引替换方法。对长度为len的a数组进行以seed为种子的索引替换得到b。

void MainOperation():主操作方法。生成数独。

SolveSudoku类

  • private属性:

int _puzzle[81]:数独谜题

int _flag[81]:标记数组,如果这个位置的数是唯一的,那么相应的标记数组位置为1,否则为0.

  • public方法:

void setPuzzle(int a[]):顾名思义,将_puzzle赋值为a

int* getPuzzle():返回_puzzle的副本

void setFlag(int a[]):顾名思义,将_flag赋值为a

int NumTranfor(int a):将a转换成用到的数字。

int RemoveCandidates(int index):去掉index位置的不合要求的候选数

bool Fill(int index):在index位置填数

流程图

输入参数为'-c':

1220932-20170926180652184-1326151492.png

输入参数为‘-s’:

1220932-20170926193108825-527908512.png

单元测试设计

TEST_METHOD(End_Test_ROW987)        {            EndProducer EP(12);            int arr[81] = { 0 };            memset(arr + 54, 7, sizeof(int) * 9);            memset(arr + 63, 8, sizeof(int) * 9);            memset(arr + 72, 9, sizeof(int) * 9);            int Real[81] = { 0 };            memcpy(Real, EP.Row987(arr), sizeof(int) * 81);            memset(arr + 54, 7, sizeof(int) * 9);            memset(arr + 63, 8, sizeof(int) * 9);            memset(arr + 72, 9, sizeof(int) * 9);            for (int i = 54; i < 63; i++) {                Assert::AreEqual(Real[i], arr[i + 18]);                Assert::AreEqual(Real[i + 9], arr[i + 9]);                Assert::AreEqual(Real[i + 18], arr[i]);            }        }        TEST_METHOD(End_Test_DuplicateCheck)         {            EndProducer es(12);            int arr[10] = { 1,2,3,4,5,5,5,3,2,0 };            int x = 6;            int y = 1;            int Count = 9;            bool expect_in = es.DuplicateCheck(arr, y, Count);            bool expect_out = es.DuplicateCheck(arr, x, Count);            Assert::AreEqual(expect_in, true);            Assert::AreEqual(expect_out, false);        }        TEST_METHOD(End_Test_getInitialSeed)         {            EndProducer ep(12);            int arr[9] = { 1,2,3,4,5,6,7,8,9 };            int* arr1 = NULL;             arr1 = ep.getInitialSeed();            for (int i = 0; i < 9; i++) {                Assert::AreNotEqual(*(arr1 + i), arr[i]);            }        }        TEST_METHOD(End_Test_IndexIndexSubstitution)         {            EndProducer ep(12);            int arr1[9] = { 4,6,3,2,1,8,9,5,7 };            int test_seed[9] = { 9,2,4,6,5,1,7,8,3 };            int arr2[9] = { 0 };            ep.IndexSubstitution(test_seed, arr1, arr2, 9);            int expect_arr2[9] = { 6,1,4,2,9,8,3,5,7 };            for (int i = 0; i < 9; i++) {                Assert::AreEqual(arr2[i], expect_arr2[i]);            }        }        TEST_METHOD(Sol_Test_setPuzzle)         {            SolveSudoku test_ss;            int x[81] = { 0 };            x[1] = 9;            x[5] = 1;            x[4] = 2;            test_ss.setPuzzle(x);            int* y = test_ss.getPuzzle();            Assert::AreEqual(x[1], y[1]);            Assert::AreEqual(x[4], y[4]);            Assert::AreEqual(x[5], y[5]);        }        TEST_METHOD(Sol_Test_NumTransfor)        {            SolveSudoku test_ss;            int x1 = 1;            int x2 = 4;            int x3 = 9;            Assert::AreEqual(test_ss.NumTransfor(x1), 0x1);            Assert::AreEqual(test_ss.NumTransfor(x2), 0x8);            Assert::AreEqual(test_ss.NumTransfor(x3), 0x100);        }

关于单元测试,我是对每个类中的每个方法单独进行了进行了测试,具体代码可参看顶部的github链接。

1220932-20170926181412653-1290188411.png

覆盖率

这是参数为‘-c’时EndProducer类的覆盖率:

1220932-20170926181423122-2119248834.png

这是参数为‘-s’时SolveSudoku类的覆盖率:

1220932-20170926193134622-1530801870.png

五、性能分析

最开始的版本生成1000000个数独需要263秒左右的时间,启动性能分析,查看生成数独部分的调用关系树

1220932-20170926181451481-166326515.png

发现开销最大的函数是这个out<<,找到这部分的代码,发现我们是每次向文件中输出一个字符,又因为c++中IO会占用较长时间

因此改用fputs,一次输出一个终盘。修改后的时间达到了3.76s。

1220932-20170926181506903-947097628.png

解题方面,性能分析发现开销最大的函数是Fill(int index),意料之中。而且RemoveCandidates方法以及NumTransfor方法均在Fill中被调用,才造成开销大的问题。

1220932-20170926181524184-871037070.png

六、代码分析

行变换函数,仅以978变换为例

int* EndProducer::Row978(int* a){    int top = (7 - 1)*NUM_ROW;    int end = (9 - 1)*NUM_ROW;    for (int i = 0; i < NUM_ROW; i++) {        Swap(&a[top + i], &a[end + i]);    }    top = (8 - 1)*NUM_ROW;    end = (9 - 1)*NUM_ROW;    for (int i = 0; i < NUM_ROW; i++) {        Swap(&a[top + i], &a[end + i]);    }    return a;}

索引替换方法

void EndProducer::IndexSubstitution(int* seed, int* a, int* b, int len){        int i;    //int result[81] = { 0 };        for( i = 0 ; i < len ; i++ )    {        int temp = a[i];        b[i] = seed[temp - 1];    }}

谜题数字转换,其中NUM1~NUM9均在头文件中定义

int SolveSudoku::NumTransfor(int a) {    int temp = 0;    switch (a)    {    case 1:        temp = NUM1;        break;    case 2:        temp = NUM2;        break;    case 3:        temp = NUM3;        break;    case 4:        temp = NUM4;        break;    case 5:        temp = NUM5;        break;    case 6:        temp = NUM6;        break;    case 7:        temp = NUM7;        break;    case 8:        temp = NUM8;        break;    case 9:        temp = NUM9;        break;    default:        cout << "Error4" << endl;        exit(1);        break;    }    return temp;}

去除index位置中不符合要求的候选数。

int SolveSudoku::RemoveCandidates(int index) {    if (_flag[index] == 1)    {        return _puzzle[index];    }    int row = index / 9;    int col = index % 9;    int m = row / 3;    int n = col / 3;    int i1;    int j1;    int pointCopy = _puzzle[index];    //Palace candidates removal    for (i1 = m * 3; i1 < m * 3 + 3; i1++)    {        for (j1 = n * 3; j1 < n * 3 + 3; j1++)        {            if (_flag[i1 * 9 + j1] == 1)             {                pointCopy = pointCopy & (~NumTransfor(_puzzle[i1 * 9 + j1]));            }        }    }    //row and col candidates removal    int i;    for (i = 0; i < 9; i++)     {        if (_flag[row * 9 + i] == 1)         {            pointCopy = pointCopy & (~NumTransfor(_puzzle[row * 9 + i]));        }        if(_flag[i * 9 + col] == 1)        {            pointCopy = pointCopy & (~NumTransfor(_puzzle[i * 9 + col]));        }        else            ;    }    return  pointCopy;}

回溯求解数独的Fill方法

bool SolveSudoku::Fill(int index) {       if (index >= 81)     {        return true;    }    int shiftvalue = RemoveCandidates(index);    int pointCopy = _puzzle[index];        if(_flag[index] == 0)    {        for (int i = 1 ; shiftvalue > 0 ; i++)         {            if (shiftvalue % 2 != 0)            {                _puzzle[index] = i;                _flag[index] = 1;                if (Fill(index + 1))                {                    return true;                }            }            shiftvalue = shiftvalue >> 1;        }    }    else    {        return Fill(index + 1);    }    _puzzle[index] = pointCopy;    _flag[index] = 0;    return false;}

七、PSP图

1220932-20170926181533434-2122156197.png

八、总结

这次作业应该是我第一次文档指挥编码的作业,结果还是不太成功,实现过程中还是出现了该文档的问题,由此暴露出的问题有几个。

一个是对C++不熟悉,以至于编码前先去看了几节C++面向对象的网课,但感觉效果不好,还不如在编码过程中学到的多。
还有就是这次太重视设计了,以至于有时候想的偏离了正确轨道,意识到的时候发现和项目好像没啥关系。
最后还是C的基础不扎实,经常会写着写着去查百度,很多知识没有熟练掌握,这大幅增加了编码时间。

2017.9.29修改

  • 将Rowjik方法进行了整合,修改为RowSwap函数,github已更新。主要思路为将目标序列与升序序列比较,若相对位置不同,则替换两者所对应行,同时替换两者位置,直到与升序序列相等时停止。

  • 今天看到作业结果居然生成了重复的终局,心态炸穿,在EndProducer.cpp中的MainOperation函数中发现了问题,是个老生常谈的错误了,因为我的行变换函数是直接修改地址,因此在对原始矩阵进行变换的时候要保留一个副本,我想到了这个问题,实现的时候却犯了想当然的错误!我新建了一个int* OriMartixCopy来保留副本,同时新建了一个int* OriMarRowCopy来用于行变换,结果在赋值时直接写成了:
OriMarRowCopy = OriMartixCopy;

相当于将两个指针同时指向原数组,失去了原本的意义。现在改为:

for(int i = 0 ; i < 81 ; i++){        OriMarRowCopy[i] = OriMartixCopy[i];}

相应的,单元测试也进行了修改。但是在解题时居然会题目与解的数目不一,有些费解,以后再查。

  • 这个问题也已经找到,还是对C++流处理的理解不够,写入出现问题:
fstream outfile("sudoku.txt");

初步试验后发现,写成ofstream就可以,

ofstream outfile("sudoku.txt");

查阅了C++ Reference关于和的内容后发现,fstream有一个成员常数out,这个out会使内部缓冲流支持文件写入操作,而在ofstream中,这个out是默认设置的。所以ofstream可以而fstream不行,也可以在构造时对fstream进行手动添加成员常量out,即:

fstream outfile("sudoku.txt", std::fstream::out);

总结一下,还是懒,其实花少量的时间写一个测试程序就可以解决的。这次就引以为戒,在结对项目中绝对绝对不能这么马虎了。

转载于:https://www.cnblogs.com/LuoboLiam/p/7596142.html

你可能感兴趣的文章
socket 通讯 端口绑定 问题 解答
查看>>
关于用户需求的调查
查看>>
云计算时代对传统软件工程的冲击
查看>>
Mahout--(三)相似性度量
查看>>
CodeForces 980 C Posterized
查看>>
C#泛型列表List<T>基本用法总结
查看>>
Drug side effect extraction from clinical narratives of psychiatry and psychology patients
查看>>
linux定时备份mysql并同步到其它服务器
查看>>
浅谈分布式事务
查看>>
Spring MVC专题
查看>>
linux grep命令详解(转)
查看>>
Java下获取可用CPU数
查看>>
JAVA学习笔记
查看>>
可编程硬件Arduino初探(1)-开发环境搭建
查看>>
Django静态文件配置
查看>>
关于C#中怎样实现点ENTER键就相当于按下确认输出按钮
查看>>
jqGrid 是一个用来显示网格数据的jQuery插件
查看>>
windows 下 gcc/g++ 的安装
查看>>
登陆后跳转到指定页
查看>>
[前端插件]为自己的博客增加打赏功能
查看>>