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