Github:Sudoku
項目相關要求
項目需求
利用程序隨機構造出N個已解答的數獨棋盤 。
輸入
數獨棋盤題目個數N(0<N<=1000000)。
輸出
隨機生成N個不重復的已解答完畢的數獨棋盤,並輸出到sudoku.txt中,輸出格式見下輸出示例。
[2017.9.4 新增要求] 在生成數獨矩陣時,左上角的第一個數為:(學號后兩位相加)% 9 + 1。
以上要求摘抄自第二次作業博文,詳細內容如:輸出示例和測試須知以及選做附加題,請參見:作業原文。
過程記錄
以為下次作業是開學布置,看到消息多少有點措手不及,點進去又看到那么多字,開始有點慌張,立馬升級為首要任務。稍微冷靜之后,又認真地看了幾遍作業,羅列下主要要做什么,又該怎么安排時間。需要做的事情主要有:看《構建之法》,VS的安裝及單元測試和效能分析的使用,以及生成數獨的算法,其他一些細節感覺問題不大,至於附加題,有空再說吧。安排上,想爭取去學校前做完附加題之外的內容,因為到學校還有些事要處理,具體來說,打算4到8號每天控制在3小時左右,總計15小時,PSP 2.1(附:Markdown頁內跳轉)。考慮到其他課程和自身等因素,15小時也是每周能夠保證的投入,平均大致一天兩小時。所以不僅是建立在工作量上,也是測試,估計得視情況調整,畢竟不想經常看到凌晨三四點的福大。
《構建之法》沒精讀,有效閱讀時間大致只有兩小時,主要了解到有單元測試和效能分析這兩個東西。單元測試就是測試自己寫的代碼是不是表現得和預期一樣,是代碼正確性的保證,應該由最熟悉代碼的作者本人來寫,畢竟代碼的作者最了解代碼的目的、特點和實現的局限性。效能分析是改進代碼的重要途徑,可以很有針對性地對性能瓶頸進行改進,進一步提高代碼的質量。工具VS的安裝倒是挺順利,至於單元測試和效能分析的使用,作業原文最后都很貼心地給出了參考鏈接,都照着例子試一試,到時候再搗鼓一下用到自己的代碼上,應該問題不大吧。拋去安裝不算,學習單元測試和效能分析的使用的有效時間也是兩小時左右。
生成數獨的算法最頭疼,很長一段時間都沒有思路,覺得專門去想也想不出來,這需要靈感,所以只是在心里惦記着時不時想一想。最初打算一個個隨機,不符合要求就再隨機,實在不行就回退再隨機原來的,直到最后一個。覺得太復雜,寫單元測試也很麻煩吧,就沒有繼續想實現的細節。但是也想不出其它的,開始抱怨數獨怎么這么麻煩,懷疑是不是沒有多少種情況。可是題目中的N最大到100w,還能不重復,那該是有很多種。看着數獨的條條框框,覺得有點像魔方,感覺可以轉一轉變成另一個數獨表,比如旋轉,所以才有那么多情況。然后就想到了很簡單的變換方法:在九宮格范圍內變換行或列,舉例來說第1,2,3行可以互相交換順序,仍然符合要求,還和原來不一樣。再結合和上列,多交換幾次,也沒深想,感覺可以變出所有情況了。
於是我就興沖沖地開始測試,想寫出主要的生成部分看看效果。太久沒打代碼,有種一夜回到解放前的感覺,前前后后折騰了3小時左右才實現想法。然后,終於發現自己還是太天真了,輸出的數獨長得超像,感覺多換幾次就回去了。其實仔細想想也很好理解,在九宮格范圍內交換行和列,原來九宮格同一行的三個數字還是在同一行,頂多順序不一樣,也就六種情況。加上題目增加要求左上角固定,我就讓第一行第一列不參與交換,那就只有(2×6×6)^2 = 5184種情況,N可是有100w啊,肯定會重復的。這就很尷尬了,沒想清楚就打代碼,還得想新法子。又想了下最初的一個個生成,果然還是復雜,感覺應該還有其它變換方式。於是我試着交換同一個九宮格的數字,發現在其它九宮格也交換相同的數字就可以,辦法傻倒確實有用啊。除去固定的左上角,那就有8!=40320種可能,可惜還是比100w小。但是,我可以把二者結合起來啊!5184×40320就足夠了,理論上100w不重復是有可能的,具體內容參見下文設計實現及關鍵代碼。
設計實現
代碼組織結構很簡單,總共三個類,分別是“Scan”類,“Print”類以及“Generator”類,功能也很簡單。“Scan”類處理輸入,只有一個檢查輸入是否合法的函數“checkInput(int argc, char * argv[])”,合法返回要求生成的數獨個數N,不合法返回0。不合法的輸入調用“Print”類的報錯函數“printError()”,合法則調用“Print”類的另一個函數“printSudokuToFile(int N, char * filepath)”,在這個函數里會調用N次“Generator”類的公共函數“getNewSudoku()"。
稍微復雜的是“Generator"類,有個私有的初始數獨表,還有些函數用來支持生成新的數獨,像上面提到的行列交換,數字交換,而且考慮到可能要寫單元測試,隨機獲取交換的行列號和交換的數字也單獨寫了函數,盡可能使函數功能明確可驗,具體組織在下面的關鍵代碼講。
把各個代碼組織起來,解決一些不熟悉的細節問題,像文件輸入輸出,再加上前前后后的調試,搗鼓了得有三小時左右。然后借西瓜學長的博客:Git和Github簡單教程以及git教程再熟悉了下git(至少也有一小時其實),終於上傳了第一個版本,感覺別說附加題了,單元測試和效能分析再改進都緊張。時間安排果然得調整,15小時不夠的啊,一邊做一邊寫博客也要花好多時間。
關鍵代碼
int * Generator::getNewSudoku()
{
int * random;
int random1, random2;
random = getRandomRorC();
random1 = random[0];
random2 = random[1];
swapRows(random1, random2);
random = getRandomRorC();
random1 = random[0];
random2 = random[1];
swapColumns(random1, random2);
int cnt = rand() % 81 + 1;
for (int i = 0; i < cnt; i++)
{
random = getRandomNumber();
random1 = random[0];
random2 = random[1];
swapNumbers(random1, random2);
}
return &initSudoku[0][0];
}
這就是關鍵的生成代碼,邏輯很簡單,先生成隨機交換的行號,然后交換行,類似的交換列,最后再交換隨機數字。其中,因為交換數字一次只交換兩個數字,可能性遠沒有潛力的8!那么多,於是就多交換幾次,九九八十一再隨機下,問題不大,理論上可能性超過100w種了。當然,作業要求左上角固定為學號后兩位之和對九取余加一(我是42,也就是7),於是隨機生成的行列號不包括1,隨機交換的數字不包括7,初始數獨的左上角為7。
感覺本可以寫得優雅些,比如把隨機生成行列號集成在交換行列的函數里,可這樣結果不確定,考慮到可能的單元檢測及代碼覆蓋率,最終還是寫成這樣。生成兩個隨機的行列及隨機數字感覺用數組傳出來方便些,然后把數組元素當函數參數傳入就又有點尷尬,應該是有更好的寫法。而且只是理論上有超過100w種的可能性,並沒有實際運行去檢驗。但現在是既沒想法也沒時間,能夠實現基礎功能就好。至於里面被調用的那些函數,技術含量不高,頂多情況多點,不再細說。
測試運行
附上測試運行的截圖:
可以發現長得有點像,因為每個數獨都是在上個數獨的基礎上變化出來的,但是又不至於非常像,畢竟可能性算多了吧,不過你變着變着再變回去,那我也只能攤手了。
單元測試
最終我只會寫一個很簡單的函數的測試,這個類里面還只有這么一個函數,原來在Calculator里把函數寫得簡單點的考慮並沒有什么用。當初看作業里給的例子就覺得自己的函數沒有這么簡單(而且例子這么簡單為啥要測試),查了下Assert類的一些函數,還是不會寫對復雜函數的測試。而且程序要求隨機生成數獨,有些生成隨機數的函數,不能預料結果不是么,然后輸出到控制台的函數又要怎么檢驗。照理應該可以測稍復雜的函數的吧,不過不管了,就先這樣吧。
效能分析與改進
直接把N設置為100w,采用性能向導中的CPU采樣分析了一下,附上結果相關截圖:
額,和我想象的不大一樣,這個“msvcp140d.dll”是什么鬼。沒查到具體是什么,不過.dll又讓我想起了先前糾結的靜態編譯問題,查了資料,改成靜態編譯,重新生成解決方案。新的.exe比原來的大得多,不過本來就沒幾KB,現在也還在KB的程度,想着問題不大,上傳Github更新。於是繼續CPU采樣,結果如下:
這個看起來就比較符合我的預期,主要的時間花在了輸出到文件上,不過這次分析的時間也比上次長,1000多秒,差不多得有三倍,靜態編譯的會比較慢么。輸出沒辦法,就是很耗時,主要看下“getNewSudoku()”函數里的情況:
為了增加隨機性而多交換幾次數字的處理,不意外地成了里面最耗時的部分。交換次數在1-81隨機,而不是固定的81,大概也算一種改進吧,感覺跑100w也不是特別久。其它改進就是代碼加了點注釋,函數命名比較好懂,盡量少點意義不明的magic數字,把很多的if-else換成switch-case讓代碼好看點吧。
說點題外話,不過也是相關的。前面用的都是CPU采樣,就CPU時不時過來看下程序,收集一下數據,最后分析。還有種“檢測”就是一直盯着程序,函數調用次數都清清楚楚,數據比較准確(也多),分析時間自然也長。看了點構建之法,本打算先用CPU采樣,再用檢測檢查耗時的部分,我這個也就一個模塊吧,就想着把N設小點然后再檢測一遍看看。但是事情並沒有這么順利,出現了些少見的警告(沒找到中文資料),就“警告 VSP2347:...”,然后“停止運行”。按網友所說以管理員身份運行VS,解決了這個又出現另一個“警告 VSP2317:...”,繼續停止運行。最后並沒有找到靠譜的解決方法(大概也是英語菜的關系),雖然CPU采樣好像也能說明一些問題了,但是折騰了好久提下也好,說不定有人知道咋整呢。
9.10 更新: release模式下的效能分析
知道release模式沒有調試信息,還會進行優化啥的,運行起來比較快,可我當初的debug和release運行的結果不一樣(可能原因),debug可以,release不行,就先用debug來效能分析。
模式release,項目右鍵->屬性->配置屬性->調試->命令行參數,先設為“-c 100”,調試結果截圖:
就是上面關鍵代碼getNewSudoku出了問題,有些用不到的變量都被優化掉了(果然可以寫得更優雅點吧),比如for (int i...)
中的i就在其中(那要怎么改啊)。本來就是不會寫得更好看,於是“項目右鍵->屬性->配置屬性->c/C++->優化->已禁用”(額,還會比debug快么),調試成功,把命令行參數改成“-c 1000000”,性能向導CPU采樣模式結果如下:
看來還是比debug快(輸出到文件好像變快了),於是上傳Github更新,不過性能分析“檢測”模式依然“警告...”,依然崩潰(攤手)。
個人軟件開發流程(PSP)
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | 30 | 20 |
▪ Estimate | ▪ 估計這個任務需要多少時間 | 30 | 20 |
Development | 開發 | 840 | 1160 |
▪ Analysis | ▪ 需求分析 (包括學習新技術) | 360 | 300 |
▪ Design Spec | ▪ 生成設計文檔 | 0 | 0 |
▪ Design Review | ▪ 設計復審 (和同事審核設計文檔) | 0 | 0 |
▪ Coding Standard | ▪ 代碼規范 (為目前的開發制定合適的規范) | 30 | 20 |
▪ Design | ▪ 具體設計 | 60 | 120 |
▪ Coding | ▪ 具體編碼 | 60 | 180 |
▪ Code Review | ▪ 代碼復審 | 30 | 60 |
▪ Test | ▪ 測試(自我測試,修改代碼,提交修改) | 180 | 480 |
Reporting | 報告 | 60 | 20 |
▪ Test Report | ▪ 測試報告 | 0 | 0 |
▪ Size Measurement | ▪ 計算工作量 | 10 | 10 |
▪ Postmortem & Process Improvement Plan | ▪ 事后總結, 並提出過程改進計划 | 50 | 10 |
合計 | 930 | 1200 |
稍微總結一下。果然15小時對我是不夠的,基礎要求都做不到,從第一次打代碼失敗就可以預見。PSP的實際用時其實並不准確,因為沒有像表那樣一個階段一個階段,只能估計,像生成數獨的算法就是閑暇時的想法。但還是能看出一些東西,比如我在設計上投入的時間明顯比打代碼的時間短,這也導致了沒發現第一次算法的缺陷,然后是更長的打代碼時間。而且,對語言還不夠熟悉,很多時候在解決低層次上的問題,也讓打代碼時間變長,其實看了下也就300多行而已。另外一點是越發覺得效率低下,找資料找着找着就不知道自己在干啥,回過神來已經過了好久,進展緩慢。關於改進的計划,一個是多打代碼的念頭更加堅定,另一個主要是提高效率。再具體一點就是打算,明確時間段的小目標,到時間就寫點博客回顧收獲,發現問題,適當取舍,確保進度(畢竟寫博客也很耗時)。還有其它事情,附加題估計是沒戲了,以上。