17秋 軟件工程 第二次作業 sudoku


2017年秋季 軟件工程 作業2:個人項目 sudoku

Github Project

Github Project at Wasdns/sudoku.

PSP Table

PSP2.1 Personal Software Process Stages 預估耗時(分鍾) 實際耗時(分鍾)
Planning 計划 10 10
Estimate 估計這個任務需要多少時間 10 10
Development 開發 340 350
Analysis 需求分析 (包括學習新技術) 30 30
Design Spec 生成設計文檔 10 5
Design Review 設計復審 (和同事審核設計文檔) 10 5
Coding Standard 代碼規范 (為目前的開發制定合適的規范) 10 10
Design 具體設計 30 60
Coding 具體編碼 120 120
PSP2.1 Personal Software Process Stages 預估耗時(分鍾) 實際耗時(分鍾)
Code Review 代碼復審 30 40
Test 測試(自我測試,修改代碼,提交修改) 100 80
Reporting 報告 60 35
Test Report 測試報告 20 15
Size Measurement 計算工作量 10 5
Design Review 設計復審 (和同事審核設計文檔) 10 5
Postmortem & Process Improvement Plan 事后總結, 並提出過程改進計划 20 10
合計 410 395

解題思路

需求:

  • 1.利用程序隨機構造出N個已解答的數獨棋盤;
  • 2.在生成數獨矩陣時,左上角的第一個數為:(學號后兩位相加)% 9 + 1;

上述需求可以分解為以下幾點:

  • 1.構造出合法的數獨解,即滿足 行/列/格 的約束條件;
  • 2.根據用戶的輸入進行IO處理;
  • 3.數獨矩陣第一個數為(0+9)%9+1=1。

那么很容易就想到以下的基本求解方法:

1.初始化一個數獨棋盤(以二維數組表示),初始位置為sudoku[0][0],填入1,進入步驟2;

2.遍歷下一個格子,進入步驟3;

3.在滿足依賴條件的前提下選出所有可能的數字,如果有進入步驟4,沒有返回步驟1;

4.隨機選取一個滿足條件的數字,填入該空格,進入步驟5;

5.如果該空格是最后一個空格,則終止程序返回數獨解;如果不是最后一個空格,返回步驟2。

實現也很簡單(brach: legacy),但是很快就發現了問題:上述求解過程中,步驟3的情況是很容易出現的(15格=>20格之間),即遍歷到某一個空格時,發現1-9所有的數字都不滿足數獨解的合法約束(行/列/格)。在上述的算法中,很簡單的就進行了處理(重新開始),但是計算出單個解的時間是無法估計的。

於是發現了一個規律:當遍歷至一個單元格時,如果發現無解,則說明前一個單元格所選擇的數字不符合要求。那么只要重新回到上一步,將上一次選擇的數字標記為非法,再進行選擇試探即可。那么最終生成的算法為:

1.初始化一個數獨棋盤(以二維數組表示),以及一個用於記錄當前單元格所有潛在數字的緩存數組,初始位置為sudoku[0][0],填入1,進入步驟2;

2.遍歷下一個格子,進入步驟3;

3.在滿足依賴條件的前提下選出所有可能的數字,如果有進入步驟4;沒有,清空當前單元格的緩存數組,將上一個單元格選擇的數字從上一個單元格的緩存數組中剔除,返回步驟2;

4.隨機選取一個滿足條件的數字,填入該空格,進入步驟5;

5.如果該空格是最后一個空格,則終止程序返回數獨解;如果不是最后一個空格,返回步驟2。

設計實現

最開始時,確定了完成該項目所需要的類:

  • SudokuJudger: 用於測試生成的數獨解是否合法(滿足行/列/格的約束限制);
  • SudokuGenerator: 用於生成數獨的解;
  • SudokuIOer: 用於接收來自用戶的輸入(命令行形式,解析參數);
  • SudokuExceptionInspector: 用於異常處理,如輸入非法字符;
  • SudokuPrinter: 打印數獨解。

其中,SudokuJudger包含以下方法:

bool SudokuisSolved(int input[9][9]);

說明:將數獨解作為輸入,判斷是否是正確解答,返回True|False。

SudokuGenerator包含以下數據結構:

int solution[9][9];

說明:用於存放數獨解。

SudokuGenerator包含以下方法:

int generateNumber(int inputAvailable[10], int index);

說明:將當前單元格的緩存數組、當前單元格的格號(或者說相對(0, 0)的偏移量)作為輸入,根據程序依賴條件得出所有潛在數字,若不存在返回-1,若存在則基於隨機數選舉出一個數字,並返回。

void increaseRandomSeed();

說明:修改隨機數種子,保證隨機性。

bool Generator();

說明:生成合理數獨解,如果正常執行,將解存放在solution[9][9]中,返回True;若發生異常,則返回False。

SudokuIOer包括以下方法:

void outputFile(int solution[9][9], ofstream& sudokuFile);

說明:輸入數獨的解、文件流,將數獨的解輸出到該文件流中。

SudokuExceptionInspector包括以下方法:

bool isNumber(char number[]);

說明:將用戶輸入作為該函數的輸入,判斷輸入的數字的每一位是否在0-9之間,返回True|False。

int parser(char number[]) throw(ParserException);

說明:將用戶輸入作為該函數的輸入,判斷輸入合法性,若非法拋出異常,若合法返回對應的整數值。

SudokuPrinter包括以下方法:

void Printer(int solution[9][9]);

說明:將輸入的數獨解打印出來。

依賴關系:

表述為:方法名 => 被依賴方法名。

Class SudokuGenerator:

  • Generator => generateNumber;
  • Generator => increaseRandomSeed;

Class SudokuExceptionInspector:

  • parser => isNumber.

關鍵代碼說明

函數main():

int main(int argc, char *argv[]) {
    // 判斷用戶輸入參數個數,若小於3則報錯
    if (argc < 3) {
        cout << "Error occurs when parsing arguments." << endl;
        cout << "Usage: sudoku.exe -c [N: a number]" << endl;
        return 1;
    }
    
    // 解析用戶輸入的參數,判斷是否輸入異常,出現異常進行異常處理
    int solutionNumber;
    SudokuExceptionInspector sudokuExceptionInspector;
    try {
        solutionNumber = sudokuExceptionInspector.parser(argv[2]);
    } catch(ParserException) {
        cout << "Error occurs when parsing arguments." << endl;
        cout << "Usage: sudoku.exe -c [N: a number]" << endl;
        cout << "Please check your input number." << endl;
        return 1;
    }

    SudokuGenerator sudokuGenerator;
    SudokuIOer sudokuIOer;

    // 打開文件 sudoku.txt
    ofstream sudokuFile("sudoku.txt", ios::out | ios::ate);

    // 求解N個數獨解,並將其輸入到sudoku.txt中
    bool signal = false;
    for (int i = 0; i < solutionNumber; i++) {
        signal = sudokuGenerator.Generator();
        if (signal) {
            sudokuGenerator.increaseRandomSeed();
            sudokuIOer.outputFile(sudokuGenerator.solution, sudokuFile);
        } else {
            cout << "Error occurs when applying sudokuGenerator." << endl;
            return 1;
        }
    }

    // 關閉文件 sudoku.txt
    sudokuFile.close();

    return 0;
}

函數Generator():

bool SudokuGenerator::Generator() {
    // 初始化數獨解棋盤
    memset(solution, 0, sizeof(solution));
    solution[0][0] = (0+9)%9+1;

    // 判斷當前單元格的合法數字
    // available[格位置][數字] = 0: 數字合法;
    // available[格位置][數字] = 1: 數字非法;
    int available[82][10];
    memset(available, 0, sizeof(available));

    // 記錄先前單元格選擇的數字
    int traverseRecorder[82];
    memset(traverseRecorder, 0, sizeof(traverseRecorder));

    traverseRecorder[0] = 1;
    
    // 指向當前單元格的指針
    int currentPlacePointer = 1;

    int i = 1;

    // 遍歷所有81個單元格
    while (i < 81) {
        // 生成當前單元格的數字
        int getNumber = generateNumber(available[i], i);

        // 如果當前單元格出現無解的情況
        if (getNumber == -1) {
            // 指向當前單元格的指針回退到上一個單元格
            currentPlacePointer--;

            // 將上一次選擇的數字在緩存數組中標記為非法
            int lastChosenNumber = traverseRecorder[currentPlacePointer];
            available[currentPlacePointer][lastChosenNumber] = 1;

            // 在記錄先前選擇數字的數組中,清除上一個單元格選擇的數字
            traverseRecorder[currentPlacePointer] = 0;
            
            i--; // 回到上一個單元格

            // 在棋盤中,清除上一個單元格選擇的數字
            int lineIndex = i/9, columnIndex = i%9; 
            solution[lineIndex][columnIndex] = 0;

            // 將出現無解的單元格處的緩存數組清空
            memset(available[currentPlacePointer+1], 0, sizeof(available[currentPlacePointer+1]));
        } else {
            // 有解,將生成的數字更新到解決方案中,進入下一個單元格
            int lineIndex = i/9, columnIndex = i%9; 
            solution[lineIndex][columnIndex] = getNumber;
            i++;

            // 更新指向當前單元格的指針,和記錄先前選擇數字的數組
            traverseRecorder[currentPlacePointer] = getNumber;
            currentPlacePointer++;
        }
    }

    return true;
}

測試運行

代碼執行時間測試:

1.使用命令:

$ time ./main -c 10/100/1000/10000/100000

2.執行時間:

(1) 10:
real	0m0.032s
user	0m0.010s
sys	0m0.004s

(2) 100:
real	0m0.055s
user	0m0.048s
sys	0m0.004s

(3) 1000:
real	0m0.424s
user	0m0.405s
sys	0m0.017s

(4) 10000:
real	0m4.504s
user	0m4.310s
sys	0m0.169s

(5) 100000:
real	0m48.359s
user	0m46.014s
sys	0m2.029s

代碼覆蓋率測試(Linux下使用gcov):here

輸入檢測:

正確運行結果(部分):

項目改進

在Windows環境下做測試時,發現程序的花費時間非常長,與Linux環境下的測試結果不相符。在檢查之后發現,原有程序中,IO是在sudokuIOer類中的outputFile函數的for循環里面完成的,在原有main函數中調用了N次outputFile函數,因此造成了極大的overhead。

原有程序:

    SudokuGenerator sudokuGenerator;
    SudokuIOer sudokuIOer;

    bool signal = false;

    for (int i = 0; i < solutionNumber; i++) {
        signal = sudokuGenerator.Generator();
        if (signal) {
            sudokuGenerator.increaseRandomSeed();
            // 在程序中執行IO,造成了極大的性能損耗
            sudokuIOer.outputFile(sudokuGenerator.solution, "sudoku.txt"); 
        } else {
            cout << "Error occurs when applying sudokuGenerator." << endl;
            return 1;
        }
}

改進方法是將文件IO放在main函數中處理,往outputFile方法中傳入文件流,將原有的N次IO處理縮減為1次,極大縮短了程序的運行時間。

改進后程序:

    SudokuGenerator sudokuGenerator;
    SudokuIOer sudokuIOer;
    // SudokuPrinter sudokuPrinter;

    // 在main函數中打開文件
    ofstream sudokuFile("sudoku.txt", ios::out | ios::ate);

    bool signal = false;

    for (int i = 0; i < solutionNumber; i++) {
        signal = sudokuGenerator.Generator();
        if (signal) {
            sudokuGenerator.increaseRandomSeed();
            // 傳入文件流,將結果輸出到文件
            sudokuIOer.outputFile(sudokuGenerator.solution, sudokuFile);
        } else {
            cout << "Error occurs when applying sudokuGenerator." << endl;
            return 1;
        }
    }

    // 關閉文件
    sudokuFile.close();

2017.9


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM