https://github.com/MinstrelZal/Sodokuhtml
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 |
題目要求:
1.程序能生成不重複的數獨終局至文件;
2.程序能讀取文件內的數獨問題,求一個可行解並將結果輸出到文件;java
要解決這個問題,首先要知道數獨遊戲的規則git
數獨是源自18世紀瑞士的一種數學遊戲。是一種運用紙、筆進行演算的邏輯遊戲。玩家須要根據9×9盤面上的已知數字,推理出全部剩餘空格的數字,並知足每一行、每一列、每個粗線宮(3*3)內的數字均含1-9,不重複。數獨盤面是個九宮,每一宮又分爲九個小格。在這八十一格中給出必定的已知數字和解題條件,利用邏輯和推理,在其餘的空格上填入1-9的數字。使1-9每一個數字在每一行、每一列和每一宮中都只出現一次,因此又稱「九宮格」。———引用自《數獨_百度百科》github
在瞭解了題目要求和規則之後,我立刻想到的一種算法就是回溯法:對於生成數獨終局,咱們只要按順序一個個填數字就行了,每填完一個數字都檢查它所在的行,列和宮是否知足數獨的規則,若知足則填下一個數字,若不知足則回溯。而且因爲題目要求中的第二點只要求一個可行解,所以一、2兩個要求感受實質上是同樣的。在肯定了算法之後,要解決的就是一些技術上的問題,好比,學習一下C++(捂臉)。算法
固然,我還搜索了其餘算法,一個比較不錯的算法是Dancing Links;在一些較早完成的同窗的博客中,我也看到了他們在《編程之美》這本書中找到了一個不錯的算法叫矩陣生成法。編程
一開始我直接把這個題當成了一道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.針對生成數獨的個數以及解決數獨問題的個數進行測試;數據結構
花費的時間:一下午(大概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);性能
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還能夠繼續優化;