我的項目-數獨程序

1)GitHub傳送門

https://github.com/MinstrelZal/Sodokuhtml

2)PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 1 * 60 0.5 * 60
· Estimate · 估計這個任務須要多少時間 1 * 60 0.5 * 60
Development 開發 25.5 * 60 21.5 * 60
· Analysis · 需求分析 (包括學習新技術) 10 * 60 8 * 60
· Design Spec · 生成設計文檔 1.5 * 60 2 * 60
· Design Review · 設計複審 (和同事審覈設計文檔) 0.5 * 60 1 * 60
· Coding Standard · 代碼規範 (爲目前的開發制定合適的規範) 0.5 * 60 0.5 * 60
· Design · 具體設計 4 * 60 3 * 60
· Coding · 具體編碼 4 * 60 2 * 60
· Code Review · 代碼複審 2 * 60 1 * 60
· Test · 測試(自我測試,修改代碼,提交修改) 3 * 60 4 * 60
Reporting 報告 4.5 * 60 3.5 * 60
· Test Report · 測試報告 2 * 60 2 * 60
· Size Measurement · 計算工做量 0.5 * 60 0.5 * 60
· Postmortem & Process Improvement Plan · 過後總結, 並提出過程改進計劃 2 * 60 1 * 60
合計 1860 1530

3)解題思路

題目要求:
1.程序能生成不重複的數獨終局至文件;
2.程序能讀取文件內的數獨問題,求一個可行解並將結果輸出到文件;java

要解決這個問題,首先要知道數獨遊戲的規則git

數獨是源自18世紀瑞士的一種數學遊戲。是一種運用紙、筆進行演算的邏輯遊戲。玩家須要根據9×9盤面上的已知數字,推理出全部剩餘空格的數字,並知足每一行、每一列、每個粗線宮(3*3)內的數字均含1-9,不重複。數獨盤面是個九宮,每一宮又分爲九個小格。在這八十一格中給出必定的已知數字和解題條件,利用邏輯和推理,在其餘的空格上填入1-9的數字。使1-9每一個數字在每一行、每一列和每一宮中都只出現一次,因此又稱「九宮格」。———引用自《數獨_百度百科github

在瞭解了題目要求和規則之後,我立刻想到的一種算法就是回溯法:對於生成數獨終局,咱們只要按順序一個個填數字就行了,每填完一個數字都檢查它所在的行,列和宮是否知足數獨的規則,若知足則填下一個數字,若不知足則回溯。而且因爲題目要求中的第二點只要求一個可行解,所以一、2兩個要求感受實質上是同樣的。在肯定了算法之後,要解決的就是一些技術上的問題,好比,學習一下C++(捂臉)。算法

固然,我還搜索了其餘算法,一個比較不錯的算法是Dancing Links;在一些較早完成的同窗的博客中,我也看到了他們在《編程之美》這本書中找到了一個不錯的算法叫矩陣生成法編程

4)設計實現過程

一開始我直接把這個題當成了一道C語言題目,將全部函數和變量都寫在main.cpp。後來對代碼進行了重構,將一部分函數和變量封裝成了Sudoku類。Sudoku類共有6個函數,其中函數void Sudoku(int n)爲構造函數,函數int SudokuGenerate(int pos, long& count)、void SudokuSolve(char* path)爲公有函數,函數bool IsValid(int pot)、void PrintSudolu()爲私有函數。其中SudokuGenerate()函數是用來生成數獨終局的核心函數,它依次往九宮格中填數,同時調用IsValid()函數來判斷所填數字是否知足要求,若知足則遞歸地進行下一個填數,若不知足則回溯,當填完一個九宮格後,就會調用PrintSudoku()函數將該數獨終局輸出至文件。SudokuSolve()函數用來解決數獨問題,每次從文件中讀入一個數獨題目,而後調用SudokuGenerate()函數來解題並輸出至文件。另外,主函數main()中主要是判斷命令行參數的代碼,若參數正確則調用SudokuGenerate()函數或SudokuSolve()函數,不然調用PrintUsage()函數在控制檯中輸出參數的要求。總的來講,個人代碼實現比較簡單,也沒有采用什麼複雜的數據結構。另外,因爲此次命令行比較簡單,所以沒有單獨設計一個InputHander類來處理輸入。
單元測試設計
1.針對IsValid()函數設計測試(由於其是能確保生成的數獨終局正確的關鍵函數),將其聲明爲public,用該函數檢測各類錯誤的數獨終局的錯誤的位置,看其是否可以檢測出來;
2.針對命令行設計測試,主要檢測各類異常輸入,例如參數數量不對,或者參數錯誤,或者文件沒法打開,文件中的數獨題目有異常輸入(個人處理方式是忽略異常題目,繼續解下一道題)等;
3.針對生成數獨的個數以及解決數獨問題的個數進行測試;數據結構

5)改進程序性能:花費的時間;改進的思路;性能分析圖;程序中消耗最大的函數

花費的時間:一下午(大概4-5小時)
改進思路(按照時間順序):
1.從文件IO上考慮:將以前每次向sudoku.txt文件輸出一個字符改成每次輸出一個數獨終局,顯著提升了性能,生成一百萬個數獨終局的時間由原來的8分鐘左右變成了40+秒;同時,將以前每次從文件中讀入一個字符改成每次從文件中讀入一行,直接用一百萬個完整(即不須要解)的數獨做爲文件中的輸入進行測試,用時大概是120+秒;
2.在輸出時用char*而不用string,進一步加快了IO,生成一百萬個數獨終局的時間變爲30秒左右;
3.後來才知道release版本比debug版本快不少,就換成了release版的,時間又減小了三分之二,最後生成一百萬個數獨大概須要12秒,解1000道題大概須要14秒;
性能分析圖
函數

程序中消耗最大的函數:int SudokuGenerate(int pos, long& count, bool solve);性能

6)代碼(部分)說明

main.cpp單元測試

// Sudoku.cpp: 定義控制檯應用程序的入口點。
//

#include "stdafx.h"

string const USAGE = "USAGE: sudoku.exe -c N(1 <= N <= 100,0000)\n       sudoku.exe -s absolute_path_of_puzzlefile";

void PrintUsage()
{
    cout << USAGE << endl;
}

int main(int argc, char* argv[])
{
    //clock_t start, finish;
    //start = clock();
    if (argc == 3)
    {
        // -c
        if (argv[1][0] == '-' && argv[1][1] == 'c')
        {
            long n = 0;
            for (unsigned i = 0; i < string(argv[2]).length(); i++)
            {
                if (argv[2][i] < '0' || argv[2][i] > '9')
                {
                    PrintUsage();
                    return 0;
                }
                n = n * 10 + argv[2][i] - '0';
            }
            // wrong patameter
            if (n < 1 || n > 1000000) {
                PrintUsage();
                return 0;
            }
            else
            {
                Sudoku su(n);
                long count = 0;
                su.SudokuGenerate(1, count, false);
            }
        }
        // -s
        else if (argv[1][0] == '-' && argv[1][1] == 's')
        {
            Sudoku su(1);
            su.SudokuSolve(argv[2]);
        }
        // wrong parameter
        else
        {
            PrintUsage();
        }
    }
    // wrong patameter
    else
    {
        PrintUsage();
    }
    //finish = clock();
    //cout << finish - start << "/" << CLOCKS_PER_SEC << " (s) " << endl;
    return 0;
}

sudoku.h

#pragma once

extern int const GRIDSIZE = 9;
extern char const UNKNOWN = '0';
extern char const FLAGNUM = '4';  //student ID: 15061075

class Sudoku
{
public:
    Sudoku(int n);
    int SudokuGenerate(int pos, long& count, bool solve);  // solve = true <==> solve sudoku puzzle
    void SudokuSolve(char* path);
private:
    char grid[GRIDSIZE][GRIDSIZE];
    std::ofstream output;
    int n;
    char buff[163];
    bool IsValid(int pos, bool solve);
    void PrintSudoku();
};

sudoku.cpp

#include "sudoku.h"
#include "stdafx.h"

using namespace std;
string const NOSUCHFILE = "No such file: ";
string const OUTFILE = "sudoku.txt";
int const SQRTSIZE = int(sqrt(GRIDSIZE));

Sudoku::Sudoku(int n)
{
    for (int i = 0; i < GRIDSIZE; i++)
    {
        for (int j = 0; j < GRIDSIZE; j++)
        {
            grid[i][j] = UNKNOWN;
        }
    }
    grid[0][0] = FLAGNUM;
    this->n = n;
    output.open(OUTFILE);
    for (int i = 0; i < GRIDSIZE * GRIDSIZE; i++)
    {
        if ((i + 1) % 9 == 0)
        {
            buff[2 * i + 1] = '\n';
            continue;
        }
        buff[2 * i + 1] = ' ';
    }
    buff[162] = '\n';
}

int Sudoku::SudokuGenerate(int pos, long& count, bool solve)
{
    if (pos == GRIDSIZE * GRIDSIZE)
    {
        PrintSudoku();
        count++;
        if (count == n)
        {
            return 1;
        }
    }
    else
    {
        int x = pos / GRIDSIZE;
        int y = pos % GRIDSIZE;
        if (grid[x][y] == UNKNOWN)
        {
            int base = x / 3 * 3;
            for (int i = 0; i < GRIDSIZE; i++)         // try to fill the pos from 1-9
            {
                grid[x][y] = (i + base) % GRIDSIZE + 1 + '0';
                if (IsValid(pos, solve))               // if the number is valid
                {
                    if (SudokuGenerate(pos + 1, count, solve) == 1)       // try to fill next pos
                    {
                        return 1;
                    }
                }
                grid[x][y] = UNKNOWN;
            }
        }
        else
        {
            if (SudokuGenerate(pos + 1, count, solve) == 1)
            {
                return 1;
            }
        }
    }
    return 0;
}

int Sudoku::SudokuSolve(char* path)
{
    ifstream input;
    input.open(path);
    if (input)
    {
        int total = 0;
        string temp[GRIDSIZE];
        string str;
        int line = 0;
        bool exc = false;     // wrong input such as 'a','.',etc. in the input file
        while (total < 1000000 && getline(input, str))
        {
            temp[line] = str;
            line++;
            if (line == GRIDSIZE)
            {
                for (int i = 0; i < GRIDSIZE; i++)
                {
                    for (int j = 0; j < GRIDSIZE; j++)
                    {
                        grid[i][j] = temp[i][2 * j];
                        if(grid[i][j] < '0' || grid[i][j] > '9')
                        { 
                            exc = true;
                            break;
                        }
                    }
                }
                getline(input, str);
                line = 0;
                if (exc)
                {
                    exc = false;
                    continue;
                }
                total++;
                // solve sudoku
                long count = 0;
                SudokuGenerate(0, count, true);
            }
        }
        //cout << total << endl;
    }
    else
    {
        cout << NOSUCHFILE << string(path) << endl;
        return 0;
    }
    return 1;
}

bool Sudoku::IsValid(int pos, bool solve)
{
    int x = pos / GRIDSIZE;
    int y = pos % GRIDSIZE;
    int z = x / SQRTSIZE * SQRTSIZE + y / SQRTSIZE;
    int leftTop = z / SQRTSIZE * GRIDSIZE * SQRTSIZE + (z % SQRTSIZE) * SQRTSIZE;
    int rightDown = leftTop + (2 * GRIDSIZE + SQRTSIZE - 1);
    int bound = solve ? GRIDSIZE : y;
    // check row
    for (int i = 0; i < bound; i++)
    {
        if (i == y)
        {
            continue;
        }
        if (grid[x][i] == grid[x][y])
        {
            return false;
        }
    }
    // check column
    bound = solve ? GRIDSIZE : x;
    for (int i = 0; i < bound; i++)
    {
        if (i == x)
        {
            continue;
        }
        if (grid[i][y] == grid[x][y])
        {
            return false;
        }
    }   
    // check box
    int bound_x = leftTop / GRIDSIZE;
    int bound_y = leftTop % GRIDSIZE;
    if (bound_x % 3 != 0 || bound_y % 3 != 0 || bound_x > GRIDSIZE -3 || bound_y > GRIDSIZE - 3)
    {
        cout << "error" << endl;
        exit(0);
    }
    for (int i = bound_x; i < (bound_x + 3); i++)
    {
        for (int j = bound_y; j < (bound_y + 3); j++)
        {
            if (i == x && j == y)
            {
                if (solve)
                {
                    continue;
                }
                else
                {
                    return true;
                }
            }
            if (grid[i][j] == grid[x][y])
            {
                return false;
            }
        }
    }
    return true;
}

void Sudoku::PrintSudoku()
{
    for (int i = 0; i < GRIDSIZE; i++)
    {
        for (int j = 0; j < GRIDSIZE; j++)
        {
            buff[18 * i + 2 * j] = grid[i][j];
        }
    }
    output << buff;
}

感想總結

1.此次我的項目作的很是坎坷,一大把緣由就是對VS和C++都不熟悉,在性能測試和單元測試階段個人VS出了不少問題,讓我整我的心態都不太好了,索性最後都解決了(雖然還不知道爲何),但這也讓我沒時間完成附加題了。這也讓我深入體會到了「計劃趕不上變化」; 2.我以前歷來沒有想過對一個代碼進行優化,也不知道僅僅IO上的改變能對一個程序的性能形成如此大的影響,愈加讓我感受本身還須要不斷提升; 3.學習了幾種不錯的生成數獨的算法,此次雖然我本身用的是回溯法,但我但願本身能用DLX算法和矩陣生成法實現一下; 4.我以爲本身的IO還能夠繼續優化;

相關文章
相關標籤/搜索