软件工程基础个人项目——Sudoku

一、Github地址

Github地址:https://github.com/ZhangWuren/SoftwareEngineering

 

二、PSP表格

PSP表格
PSP过程

预估耗时

(分钟)

实际耗时

(分钟)

Planning计划105
·Esitimate·估计耗时105
Development开发1020870
·Analysis·需求分析3020
·Design Spec·生成设计文档

120 

30
·Design Review·设计复审3010
·Coding Standard·代码规范3020
·Design·具体设计150100
·Coding·编码300300
·Code Review·代码复审6030
·Test·测试300360
Reporting报告130100
·Test Repor·测试报告6060
·Size Measurement·计算工作量1010

·Postmortem & 

 Process Improvement

 Plan

·总结,改进计划6030
 总计1160975

三、解题思路

数独是我比较熟悉的一个概念。

1. 刚看到题目后,我首先思考的是生成数独的部分,一开始我的想法是DFS,从格(1,2)开始(因为第一行第一个数确定为学号末尾两位相加模9+1)随机选择1~9中的一个数,然后用深搜一次向下排列,判断的方法则是数独规则。但是思考了一会之后,就否定了这个算法,先不谈耗费时间过长的问题,因为是要生成上万数量级的数独,对生成的不重复性无法保证。后来在网络上搜索了一番,找到了一个更普遍的算法:通过对数独第一行的平移来生成数独。

比如,假定第一行为 123456789,将其向右平移3格,得到第二行789123456;将第一行向右平移6格,得到第三行456789123。显然,因为错位的关系,第一、二、三宫都符合数独规则。同理将第一行分别平移1,4,7格可以得到第四五六行,平移2,5,8格可以得到七八九行。因为第一行第一个数为6,剩余的全排列共8!=40320种,离题目的最大要求还有所差距。这时候只需要将四五六行全排列,七八九行全排列,即可生成 8!*3!*3! = 1451520>1000000种不同的数独。

2. 解数独方面,是一个典型的DFS+回溯法的问题,因为以前做过类似的C语言题目,所以就采用了C语言方法写。

四、设计实现过程。

在我的项目里,生成数独和解数独相对独立的两个的部分(甚至分别是C++和C语言实现的)

1.生成数独

在生成数独的部分,用到了一个CRaw类

class CRow
{ //数独的一行
  public:
    CRow()//构造函数将第一行设置为6 1 2 3 4 5 7 8 9
    {
        __row[0] = 6;
        __trow[0] = 6;
        for (int i = 1; i < 9; i++)
        {
            if (i < 6)
            {
                __row[i] = i;
                __trow[i] = i;
            }
            else
            {
                __row[i] = i + 1;
                __trow[i] = i + 1;
            }
        }
    };
    void NextRow();//对__row[] 进行一次排列
    void TranslateAndPrintRow(int transNumber);//将__row[] 平移transNumber格 //结果赋值给__trow[] 并输出到文件
  private:
    unsigned short __row[9];//初始行,平移的模板
    unsigned short __trow[9];//平移后的行
};

成员函数NextRow()是用c++标准库中函数next_permutation来实现全排列,例如将1 2 3→1 3 2→2 1 3

TranslateAndPrintRow(int transNumber) 根据参数transNumber来平移行并输出。

2.解数独

解数独是用回溯算法,主体函数为递归函数int search(int sudoku[][9], int order, int number)

int search(int sudoku[][9], int order, int number)
{
    int copy[9][9];
    int *copyPoint = (int *)copy;
    int x = order / 9;
    int y = order % 9;
    for (int i = 0; i < 9; i++)
    {
        for (int j = 0; j < 9; j++)
        {
            copy[i][j] = sudoku[i][j];
        }
    }

    if (order < 0)
        searchFlag = 0;

    if (order >= 0)
    {
        copy[x][y] = number;
    }
    if (searchFlag)
    {
        return 0;
    }

    while (*(copyPoint + order))
    {
        order++;

        if (order > 80)
        {
            searchFlag = 1;
            PrintSokudu(copy);
            return 0;
        }
    }

    for (int i = 1; i <= 9; i++)
    {
        x = order / 9;
        y = order % 9;
        if (isOK(copy, x, y, i))
        {
            search(copy, order, i);
        }
    }
    return -1;
}

递归结束的标志是searchFlag=1,即下一个0的位置已经超过数独总格数81

调用了PrintSodoku()用来打印,isOK()用来判断合法性。

五、性能改进

在项目过程中,解数独的性能较为良好,需要改进的是生成数独的部分,断断续续花了大约一个星期左右的时间。

题目中输入的范围是1~1000000,在最开始的版本完成后,我尝试输出1000000个数独,结果时间却在6分钟左右。我开始寻找问题,一开始我以为是我的算法复杂度过高,但是我的计算显示我的算法复杂度应该是O(n)的,因为对于输入规模来说,我的解法是一个顺序的过程,不存在对输入规模n进行循环的地方,但是时间确实过长。

在我百思不得其解的时候,听取了同学的建议,尝试了一个新的算法,用空间换时间,将每个排列的36种模板直接用abcdefghi出来,之后只需要用对应的数字替代字母即可。但是结果仍然不如人愿,也需要5分钟左右。

后来在询问过别的同学之后,我发现是c++自带的fstream函数在文件读写的过程中耗费了大量的时间,在改用c语言FILE输出后,1000000个输出时间在5~9秒之间。

总结:我应该早点使用vs2017自带的性能分析工具!!!

下图是最终版本生成1000000个不重复数独的时间分析

最终版本性能分析

 可以看到,耗费时间的最多的地方是生成函数GenerateSudoku,其中主要是调用的TranslateAndPrintRow函数,这仍然还是输出导致的时间问题,实际上如果不需要输出,单纯生成的时间只需约0.8秒左右。

bool GenerateSudoku(char *csudokuNumber)
{
    fp = fopen("sudoku.txt", "w");//打开sudoku.txt

    //判断输入合法性
    for (int i = 0; csudokuNumber[i] != '\0'; i++)
    {
        if (csudokuNumber[i] < '0' || csudokuNumber[i] > '9')//非数字
        {
            cout << "Please enter right number" << endl;
            return false;
        }
    }

    int sudokuNumber = atoi(csudokuNumber);
    if (sudokuNumber > 1000000 || sudokuNumber <= 0)//不合法数字
    {
        cout << "Please input number between 0 and 1,000,000" << endl;
        return false;
    }

    int count = sudokuNumber; //记录还未生成的数独个数
    int TranslateArray1[3] = {1, 4, 7};//四五六行平移的格数
    int TranslateArray2[3] = {2, 5, 8};//七八九行平移的格数
    int TranslateArray3[3] = {0, 3, 6};//一二三行平移的格数
    CRow crow;

    for (int i = 0; i <= sudokuNumber / 36; i++)//每个全排列的行可以生成36个不同的数独
    {                                           //故需要循环sudokuNumber/36次
        int Times = 0;                          //Times用来判断对应全排列的行需要生成多少个不同的数独
        if (i < sudokuNumber / 36)              //除了最后一次循环未sudokuNumber%36次,别的循环都为36次
        {
            Times = 36;
        }
        else
        {
            Times = sudokuNumber % 36;
        }
        for (int j = 0; j < Times; j++)
        {
            count--;//未生成数独个数-1
            for (int k = 0; k < 3; k++)//生成一二三行
            {
                crow.TranslateAndPrintRow(TranslateArray3[k]);
                fputc('\n', fp);
            }
            for (int k = 0; k < 3; k++)//生成四五六行
            {
                crow.TranslateAndPrintRow(TranslateArray1[k]);
                fputc('\n', fp);
            }
            for (int k = 0; k < 3; k++)//生成七八九行
            {
                if (k != 2)
                {
                    crow.TranslateAndPrintRow(TranslateArray2[k]);
                    fputc('\n', fp);
                }
                else
                {
                    crow.TranslateAndPrintRow(TranslateArray2[k]);
                }
            }

            //变换 四五六 、 六七八行
            if (j % 6 == 5)
            {
                next_permutation(TranslateArray1, TranslateArray1 + 3);
            }
            next_permutation(TranslateArray2, TranslateArray2 + 3);

            if (count != 0)
            {
                fputc('\n', fp);
                fputc('\n', fp);
            }
        }
        crow.NextRow();
    }

    cout << "Already Generate right sudoku in sudoku.txt" << endl;
    return true;
}

六、代码说明。

1.生成数独

生成数独部分,关键类为CRow,关键函数为GenerateSudoku()

class CRow
{ //数独的一行
  public:
    CRow()//构造函数将第一行设置为6 1 2 3 4 5 7 8 9
    {
        __row[0] = 6;
        __trow[0] = 6;
        for (int i = 1; i < 9; i++)
        {
            if (i < 6)
            {
                __row[i] = i;
                __trow[i] = i;
            }
            else
            {
                __row[i] = i + 1;
                __trow[i] = i + 1;
            }
        }
    };
    void NextRow();//对__row[] 进行一次排列
    void TranslateAndPrintRow(int transNumber);//将__row[] 平移transNumber格 //结果赋值给__trow[] 并输出到文件
  private:
    unsigned short __row[9];//初始行,平移的模板
    unsigned short __trow[9];//平移后的行
};
bool GenerateSudoku(char *csudokuNumber)
{
    fp = fopen("sudoku.txt", "w");//打开sudoku.txt

    //判断输入合法性
    for (int i = 0; csudokuNumber[i] != '\0'; i++)
    {
        if (csudokuNumber[i] < '0' || csudokuNumber[i] > '9')//非数字
        {
            cout << "Please enter right number" << endl;
            return false;
        }
    }

    int sudokuNumber = atoi(csudokuNumber);
    if (sudokuNumber > 1000000 || sudokuNumber <= 0)//不合法数字
    {
        cout << "Please input number between 0 and 1,000,000" << endl;
        return false;
    }

    int count = sudokuNumber; //记录还未生成的数独个数
    int TranslateArray1[3] = {1, 4, 7};//四五六行平移的格数
    int TranslateArray2[3] = {2, 5, 8};//七八九行平移的格数
    int TranslateArray3[3] = {0, 3, 6};//一二三行平移的格数
    CRow crow;

    for (int i = 0; i <= sudokuNumber / 36; i++)//每个全排列的行可以生成36个不同的数独
    {                                           //故需要循环sudokuNumber/36次
        int Times = 0;                          //Times用来判断对应全排列的行需要生成多少个不同的数独
        if (i < sudokuNumber / 36)              //除了最后一次循环未sudokuNumber%36次,别的循环都为36次
        {
            Times = 36;
        }
        else
        {
            Times = sudokuNumber % 36;
        }
        for (int j = 0; j < Times; j++)
        {
            count--;//未生成数独个数-1
            for (int k = 0; k < 3; k++)//生成一二三行
            {
                crow.TranslateAndPrintRow(TranslateArray3[k]);
                fputc('\n', fp);
            }
            for (int k = 0; k < 3; k++)//生成四五六行
            {
                crow.TranslateAndPrintRow(TranslateArray1[k]);
                fputc('\n', fp);
            }
            for (int k = 0; k < 3; k++)//生成七八九行
            {
                if (k != 2)
                {
                    crow.TranslateAndPrintRow(TranslateArray2[k]);
                    fputc('\n', fp);
                }
                else
                {
                    crow.TranslateAndPrintRow(TranslateArray2[k]);
                }
            }

            //变换 四五六 、 六七八行
            if (j % 6 == 5)
            {
                next_permutation(TranslateArray1, TranslateArray1 + 3);
            }
            next_permutation(TranslateArray2, TranslateArray2 + 3);

            if (count != 0)
            {
                fputc('\n', fp);
                fputc('\n', fp);
            }
        }
        crow.NextRow();
    }

    cout << "Already Generate right sudoku in sudoku.txt" << endl;
    return true;
}

说明见注解

2.解数独

主要函数为递归函数search(),说明见注解

int search(int sudoku[][9], int order, int number)
{
    //sudoku[][9]为递归的数组
    //order为当前要设置的格子在数独中的顺序
    //number为要对当前格子填下的数字

    //递归函数
    int csudoku[9][9]; //辅助数组,递归操作改变的是这个辅助数组,使得回溯时不需要再变动原数组,只需要将这个数组释放
    int *csudokuPoint = (int *)csudoku;
    int x = order / 9; //根据顺序转化为坐标
    int y = order % 9;
    for (int i = 0; i < 9; i++) //将递归的数组赋值给辅助数组
    {
        for (int j = 0; j < 9; j++)
        {
            csudoku[i][j] = sudoku[i][j];
        }
    }

    if (order < 0)      //初始化,开始递归
        searchFlag = 0; //结束标志,0未结束,1结束

    if (order >= 0) //将number赋值到csudoku中的对应位置
    {
        csudoku[x][y] = number;
    }
    if (searchFlag) //判断是否结束递归
    {
        return 0;
    }

    //搜索下一个为0的格子的顺序,即目标格
    while (*(csudokuPoint + order) != 0)
    {
        order++;
        if (order > 80)//如果顺序大于80,则表示所有格子都已经填满,设置searchFlag为1,打印结果
        {
            searchFlag = 1;
            PrintSudoku(csudoku);
            return 0;
        }
    }

    for (int i = 1; i <= 9; i++)//对目标格填1~9
    {
        x = order / 9;//根据order获得目标格坐标
        y = order % 9;
        if (isOK(csudoku, x, y, i))//判断当前填法是否合法
        {
            search(csudoku, order, i);//如果合法,则递归目标格子
        }
    }
    return -1;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值