• [2017北航软工]第1次个人项目——求解数独与生成数独终局


    [2017北航软工]第1次个人项目

    ——求解数独与生成数独终局

    项目GitHub地址 https://github.com/Hesitater/Sudoku

    解题思路

    这次的数独问题主要可分为求解数独与生成终局两大部分。最开始拿到题目的时候,我决定先完成生成终局,再求解数独,然而找到的大部分生成数独的算法都无法满足生成1000000个不同终局的要求。虽然初始决策的错误让我绕了一些弯路,但也让我拜识不同生成终局的算法,在这里将找到的终局生成算法贴出来:

    前两个算法用的都是对已有终局进行行、列、单元格交换的生成方法,最后一个算法比较有特色,虽然也需要终局,但相较前两者加入了一定随机性。

    求解数独

    在发现先生成终局这条路走不通后,我改变了策略,选择先完成求解数独的功能。在谷歌求解算法的过程中,翻到了Ladit同学的这篇博客:深度优先搜索和回溯法生成数独。我最后采用了博客中推荐的DLX算法来求解数独。由于使用指针操作以及算法解法的巧妙,DLX算法算是数独求解算法当中效率较高的,以下是对DLX算法讲解得比较全面而又易懂的两篇博客:

    生成终局

    生成终局同样利用求解数独的DLX算法就能完成。如果将DLX算法看做一个函数,求解数独的过程就是把挖了空的终局作为参数传给函数,让函数把空位填满;生成终局的过程就是把没有数字的空数独(除了第一个格子被填上指定数字)作为参数传给函数,让函数把空位填满。

    这篇博客也提供了一个DLX和深度优先搜索结合的方式生成数独。这个算法给数独生成增加了一定随机性而又能保证生成的终局数量,值得参考——产生数独谜题

    整体设计

    类结构:

    一共有8各类,可分为两大部分:

    • DLX:

        1. DLXNode——DLX节点类
        1. ColumnHead——DLXNode子类,列头类
        1. CommonNode——DLXNode子类,普通节点类
        1. DLXGenerator——DLX十字链表创建类
        1. DLXSolver——DLX问题求解类
    • Sudoku:

        1. SudokuLoader——文件存取操作类
        1. SudokuSolver——数独求解类,以DLXSolver为核心
        1. SudokuGenerator——终局生成类

    代码组织:

    数独的求解与终局生成都是建立在DLX算法的基础上的,因此为DLX算法单独设立了5个类来管理DLX算法,明确各部分分工。

    单元测试:

    贴出程序的两个重要方法的测试
    SudokuSolver::solveSudoku:测试解出的数独是否与答案相符

    TEST_METHOD(TestSolveSudoku) {
        vector<int> puzzleVector;
        vector<int> solutionVector;
        int puzzle[81] = {
            0,1,2,3,4,0,7,8,9,
                    7,8,9,5,1,2,3,4,6,
                    3,4,6,7,8,9,5,1,2,
                    2,5,1,0,3,4,9,6,7,
                    6,9,7,2,5,1,8,3,4,
                    8,3,4,6,9,7,2,5,0,
                    1,2,5,4,7,3,6,9,8,
                    9,6,8,0,2,5,4,7,3,
                    4,7,3,9,6,8,1,2,0 };
        vector solution[81] = {
            5,1,2,3,4,6,7,8,9,
                    7,8,9,5,1,2,3,4,6,
                    3,4,6,7,8,9,5,1,2,
                    2,5,1,8,3,4,9,6,7,
                    6,9,7,2,5,1,8,3,4,
                    8,3,4,6,9,7,2,5,1,
                    1,2,5,4,7,3,6,9,8,
                    9,6,8,1,2,5,4,7,3,
                    4,7,3,9,6,8,1,2,5
        };
        for (int i = 0; i < 81; i++) {
            puzzleVector.push_back(puzzle[i]);
        }
        SudokuSolver solver;
        DLXNode* listHead = new DLXNode();
        solver.solveSudoku(listHead, puzzleVector, solutionVector);
        for (int j = 0; j < 81; ++j) {
            Assert::AreEqual(solutionVector[i], solution[i]);
        }
    }
    

    SudokuGenerator::generate: 测试生成的数独是否满足数独的约束与格式

    TEST_METHOD(TestGenerateSudokus) {
            SudokuSolver solver;
            DLXNode* listHead = new DLXNode();
            vector<vector<int>> sudokus;
            vector<int> answers;
            SudokuGenerator generator;
            generator.generateSudokus(10);
            for (int i = 0; i < 10; ++i) {
                Assert::AreEqual(solver.solveSudoku(listHead, sudokus[i], answers),true);
            }
    }
    

    性能分析


    上图为VS生成的本次项目的样本分析报告

    从上面的调用树可以看出,整个程序占用资源最多的方法是SudokuSolver::solveWithMultiAnswers,这个方法主要负责利用DLX算法完成求解工作,暂时未想到性能改进的办法。

    代码说明

    DLX部分:

    • DLXGenerator::appendLine:增加一行元素到DLX十字链表底部
    void DLXGenerator::appendLine(vector<ColumnHead*> columnHeads, vector<int> elementSubscripts, int rowIndex){
        CommonNode* lastHorizontalNode = NULL;
        CommonNode* firstHorizontalNode = NULL;
    
        for (int i = 0; i < elementSubscripts.size(); ++i) {
            int subscript = elementSubscripts[i];
            ColumnHead* columnHead = columnHeads[subscript];
            CommonNode* currentNode = new CommonNode(rowIndex, columnHead);
            DLXNode *lastVerticalNode = columnHead->upNode;
    
            //Link vertical nodes
            lastVerticalNode->appendDownNode(currentNode);
            currentNode->appendDownNode(columnHead);
    
            //Link horizontal nodes
            if (i == 0) {
                firstHorizontalNode = currentNode;
                lastHorizontalNode = currentNode;
            } else {
                lastHorizontalNode->appendRightNode(currentNode);
                lastHorizontalNode = currentNode;
            }
    
            currentNode->columnIndex = columnHead->columnIndex;
            columnHead->numberOfOne++;
        }
    
        lastHorizontalNode->appendRightNode(firstHorizontalNode);
    }
    
    • DLXSolver::solveWithOneAnswer:求解DLX十字链(只求一个解),参数listHead为DLX十字链链头,solution中存储所有解的行号。
    bool DLXSolver::solveWithOneAnswer(DLXNode *listHead, vector<int>& solution, int depth) {
        if (listHead->rightNode == listHead) { //Solution found
            /*for (int i = 0; i < depth; ++i) { //Debugging code
                cout << solution[i] <<endl;
            }*/
            return true;
        }
    
        //Select column with least one's
        ColumnHead* columnHead = selectColumn(listHead);
        cover(columnHead);
    
        bool solutionFound = false; //Usage: return type
    
        //Loop rows with one in column below columnHead
        for (DLXNode* node = columnHead->downNode; node != columnHead; node = node->downNode) {
            solution.push_back(((CommonNode*)node)->rowIndex); //Add temporary tempSolution node
    
            for (DLXNode* node2 = node->rightNode; node2 != node; node2 = node2->rightNode) {
                cover(((CommonNode*)node2)->columnHead);
            }
    
            depth++;
            solutionFound = solveWithOneAnswer(listHead, solution, depth); //Enter next recursion level
            depth--;
    
            for (DLXNode* node2 = node->leftNode; node2 != node; node2 = node2->leftNode) {
                uncover(((CommonNode*)node2)->columnHead);
            }
    
            if (solutionFound){ //Solution found, jump out loop
                break;
            }
    
            solution.pop_back(); //Delete temporary tempSolution node
        }
        uncover(columnHead);
    
        return solutionFound;
    }
    
    • DLXSolver:: solveWithCertainAnswers:对一个DLX十字链表,求多(answerCount)个解
    void DLXSolver:: solveWithCertainAnswers(DLXNode *listHead, vector<int>& tempSolution, vector<vector<int>>& lastSolution,
                                             int answerCount, int depth) {
        if (listHead->rightNode == listHead) { //One solution found
            /*for (int i = 0; i < depth; ++i) { //Debugging code
                cout << tempSolution[i] <<endl;
            }*/
            lastSolution.push_back(tempSolution);
            return;
        }
    
        ColumnHead* columnHead = selectColumn(listHead);
        cover(columnHead);
    
        for (DLXNode* node = columnHead->downNode; node != columnHead; node = node->downNode) {
            tempSolution.push_back(((CommonNode*)node)->rowIndex); //Add temporary tempSolution node
    
            for (DLXNode* node2 = node->rightNode; node2 != node; node2 = node2->rightNode) {
                cover(((CommonNode*)node2)->columnHead);
            }
    
            depth++;
            solveWithCertainAnswers(listHead, tempSolution, lastSolution, answerCount, depth); //Enter next recursion level
            depth--;
    
            for (DLXNode* node2 = node->leftNode; node2 != node; node2 = node2->leftNode) {
                uncover(((CommonNode*)node2)->columnHead);
            }
    
            if (lastSolution.size() == answerCount) { //Solution count achieved get out of recursion
                break;
            }
    
            tempSolution.pop_back(); //Delete temporary tempSolution node
        }
        uncover(columnHead);
        return;
    }
    

    Sudoku部分:

    • SudokuSolver::solveSudoku:得到所给数独题的一个解,传到answer中
    void SudokuSolver::solveSudoku(DLXNode *listHead, vector<int> &sudoku, vector<int> &answer) {
        transformToList(sudoku, listHead);
        DLXSolver dlxSolver = DLXSolver();
        vector<int> solution;
        dlxSolver.solveWithOneAnswer(listHead, solution, 0); //Got DLX answer
    
        solutionToAnswer(solution, answer); //Answer got
    }
    
    • SudokuSolver::solveWithMultiAnswers:对一个数独题求多(answercount)个解
    * void SudokuSolver::solveWithMultiAnswers(DLXNode *listHead, vector<int>& sudoku, vector<vector<int>>& answers, int answerCount) {
        transformToList(sudoku, listHead);
        DLXSolver dlxSolver = DLXSolver();
        vector<int> tempSolution;
        vector<vector<int>> lastSolution;
        dlxSolver.solveWithCertainAnswers(listHead, tempSolution, lastSolution, answerCount, 0); //Got DLX answer
    
        //Get answers from lastSolution
        for (int i = 0; i < answerCount; ++i) {
            vector<int> answer;
            solutionToAnswer(lastSolution[i], answer);
            answers.push_back(answer);
        }
    }
    
    • SudokuGenerator:: generateSudokus:创建sudokuCount个不重复的终局
    vector<vector<int>> SudokuGenerator:: generateSudokus(int sudokuCount) {
        vector<vector<int>> answers;
    
        //Create an sudoku with all zero
        vector<int> originalSudoku;
        for (int j = 0; j < sudokuSize; ++j) {
            if (j == 0) { //The first one must be 5
                originalSudoku.push_back(5);
            }else {
                originalSudoku.push_back(0);
            }
        }
    
        //Solve the zero sudoku to get sudoku outcomes
        DLXGenerator generator = DLXGenerator();
        DLXNode* listHead = new DLXNode();
        SudokuSolver solver = SudokuSolver();
        solver.solveWithMultiAnswers(listHead, originalSudoku, answers, sudokuCount);
        return answers;
    }
    

    PSP表格

    PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
    Planning 计划 5 7
    · Estimate · 估计这个任务需要多少时间 5 7
    Development 开发 430 1020
    · Analysis · 需求分析 (包括学习新技术) 120 600
    · Design Spec · 生成设计文档 30 0
    · Design Review · 设计复审 (和同事审核设计文档) 0 0
    · Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 0
    · Design · 具体设计 20 120
    · Coding · 具体编码 120 120
    · Code Review · 代码复审 10 0
    · Test · 测试(自我测试,修改代码,提交修改) 120 180
    Reporting 报告 70 160
    · Test Report · 测试报告 30 30
    · Size Measurement · 计算工作量 10 5
    · Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 125
    合计 505 1207

    小结

    万事开头难。由于对语法、算法和各种工具的生疏,这次的项目花了不少时间,Deadline前才勉强把作业的基本要求达成。这一周的练习算是让我头次真正体会到高效学习的重要性,打算在下次项目尝试指定阶段性目标,可把一周的项目周期切割成三份,每个项目制定周期定为2~4天,希望能对效率提高有所帮助。

  • 相关阅读:
    重新认识布局:html和body元素
    重新认识布局:3d空间中的css盒子
    重新认识布局:百分比单位
    重新认识布局:标准流,浮动,定位的关系
    Redis(1.7)Redis高可用架构与数据库交互(理论篇)
    C++: 模块定义文件声明(.def)的使用
    HttpListener supports SSL only for localhost? install certificate
    跨域请求引起的 OPTIONS request
    html 浏览器自动加上 标签的详解
    c# HttpServer 的使用
  • 原文地址:https://www.cnblogs.com/captainYi/p/7594568.html
Copyright © 2020-2023  润新知