准備工作階段
1,創建好Github庫后傳輸文件,Github項目地址(https://github.com/leee329/031702339)
2,並按要求安置好Visual Stdio 2017並做好相關文件夾


3.PSP表格
| PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
|---|---|---|---|
| Planning | 計划 | 40 | 40 |
| Estimate | 估計這個任務需要多少時間 | 10 | 15 |
| Development | 開發 | 120 | 120 |
| Analysis | 需求分析 (包括學習新技術) | 120 | 90 |
| Design Spec | 生成設計文檔 | 20 | 30 |
| Design Review | 設計復審 | 5 | 5 |
| Coding Standard | 代碼規范 (為目前的開發制定合適的規范) | 60 | 120 |
| Design | 具體設計 | 20 | 30 |
| Coding | 具體編碼 | 120 | 180 |
| Code Review | 代碼復審 | 30 | 15 |
| Test | 測試(自我測試,修改代碼,提交修改) | 60 | 300 |
| Reporting | 報告 | 30 | 45 |
| Test Repor | 測試報告 | 30 | 40 |
| Size Measurement | 計算工作量 | 15 | 30 |
| Postmortem & Process Improvement Plan | 事后總結, 並提出過程改進計划 | 60 | 70 |
| 合計 | (自閉時長) | 740 | 1130 |
沒錯,因為回溯函數的一個等號我debug了5個小時,一共耗時1130分鍾=18h50m,自閉.jpg
開始階段
1,思考階段
一開始看到這個題目,我尋思從三宮格往外擴展,實在是太麻煩了,因為實現完三宮格之后,空間還要再擴張,那個時候代碼對於新的空間擴張就需要很大的改動,不如直接就從九宮格下手,對於九宮格來說,我得從合法性和唯一性函數以及回溯性函數來進展,其中合法性函數用於判斷某個數字在某個位置的合法性,唯一性函數用於判斷某個數在某一行/列/宮上的某個位置是否是唯一合法的,回溯函數也就是深度優先搜索函數,用於嘗試填充
2,算法關鍵:三大函數
計算模塊接口的設計與實現過程:
合法性函數F1:判定某個數n在某個位置(x,y)的合法性:需要考慮到,如果x所在的這一行,y所在的這一列(x,y),n所在的這一宮全都沒有n,就是合法的。
需要考量以(x,y)為中心的十字,和一個九宮格,共21個位置(畫圖工具真難用):
例如:如圖,5在4行4列上,如果第4行和第4列和第5宮都沒有5,那么5在這里就算合法的。

唯一性函數F2:先說小函數f:對於每一個數,在每一個位置都進行唯一性判斷,判定某個數n,在某行或者某列或者某宮是否是唯一合法的(這一行/列/宮的其他地方都沒有n的容身之地):如果合法就填上,需要對n所在的行和列個宮的另外21個數,需要遍歷21X21次。
再說大函數F,因為每次填充都會為下一次填充提供新的條件,因此一次的f是不夠的,因為f之后又有新條件了,所以f得循環下去,但如果在其中一次f中,沒有進行填充操作,F就該結束了
例如:5在4行4列上,如果這一行(4行)中,只有(4,9)5是合法的,那么5在第四行上就算唯一的,如果在第四行上有兩個位置是合法的,那么它就不是唯一的了,如果它在(2,2)號宮里只有(5,5)是合法的,那么對於這一宮,它也是唯一的
因此(4,9)和(5,5)將會填入5

試探性函數F3:因為唯一性函數以后,仍然有空位(低級題可以過,高級題仍然有空位),也就是說剩下來的所有數,在所有位置上都是不唯一的,試探性函數也就是一種回溯函數,如果這個數目前來說是合法的,就填下去,但是如果后續有數因為它的填入而無處安放,我就把這個數取出來,換另一個數。
我試着用結構體記下位置和數值供以后返回,無果,我試着用二維來定位一個位置,無果,於是我將一維來定制,比如42,對應着橫坐標42/9=4,縱坐標42%9=6
在進行唯一性函數填充以后,我從0開始試探性函數,如果被占據,直接到1,如果1在位置1上不合法,就看2。。。如果3合法,就把3填入1號位后進入2號位試探,如果2號位1-9全都不合法,回退到1號位把3拿出來,進行4的試探。。。直到第80號位都沒有出現不合法問題,就結束試探性函數。有點難解釋,直接上圖,序號表示程序順序。
獨到之處:通過將唯一性函數與試探性函數結合在一起,達到降低CPU消耗的效果,具體在后面大的算法改進和性能分析。
流程圖如下

在這樣的結構下,無論是3宮格還是456789宮格,都是適用的
3,模塊間的關系以及計算模塊部分單元測試展示:
(siz,gl,gw分別是數獨規格和宮格規格,因此如下三個函數是針對多種數獨的求解方案)
單元測試與模塊結合:
對於F1,直接對一個題目遍歷一遍就能看出測試是否出問題。
對於F2,在F1沒問題的情況下,再將1-9對81個位置進行判斷。
對於F3,和F2情況類似,因為F3和F2沒有什么關系。
具體單元測試與模塊結合如下。
合法性函數:
模塊關系:這是最基本的模塊,所以它沒有被其他模塊調用:
Locate錯誤:占位不合法。
HL錯誤:行列不合法。
GG錯誤:宮不合法。
1 bool judge(int x, int y, int num)//坐標和數字 2 { 3 int i, j, t = 0, x1, y1; 4 if (gl != 0)//如果有宮 5 x1 = x / gl, y1 = y / gw;//宮坐標轉換 6 if (sd[x][y] == 0)//進行位置判斷,對應下圖的LOCATE錯誤 7 { 8 for (i = 0; i < siz; i++) 9 if (sd[x][i] == num || sd[i][y] == num)//進行十字判斷,對應下圖的HL型錯誤 10 return 0; 11 if (gl != 0) 12 { 13 for (i = x1 * gl; i < x1*gl + gl; i++)//進行宮判斷,對應下圖GG型錯誤 14 for (j = y1 * gw; j < y1*gw + gw; j++) 15 if (sd[i][j] == num) 16 return 0; 17 } 18 return 1;//對應下圖的成功提示 19 } 20 else 21 return 0; 22 }
測試方法:
for(k=0;k<siz;k++)//siz是規格 for (i = 0; i < siz; i++) for (j = 0; j < siz; j++) judge(i,j,k);
測試數據覆蓋的代碼有如上代碼和數據輸入輸出代碼。


唯一性函數:
模塊關系/測試思路,如下:
測試數據輸入到sd[][]后,就到達了圖中的“上一模塊”,然后直接調用即可

測試數據覆蓋的代碼除了如下代碼以外,還有合法性函數的代碼,和數據輸入輸出代碼。
1 void single()//唯一性 2 { 3 int i, j, k, l, m, t = 0, x0 = 0, y0 = 0, flag, p = 1;; 4 while (1) 5 { 6 flag = 0; 7 for (i = 1; i <= siz; i++)//對每一個數字進行唯一性判斷 8 { 9 for (j = 0; j < siz; j++) 10 { 11 t = 0; x0 = 0; y0 = 0; 12 for (k = 0; k < siz; k++)//在某行的唯一性 13 { 14 t = t + judge(j, k, i); 15 if (judge(j, k, i)) 16 x0 = j, y0 = k; 17 } 18 if (t == 1)//對於行的位置上填充 19 { 20 sd[x0][y0] = i; 21 flag++; 22 } 23 t = 0; x0 = 0; y0 = 0; 24 for (k = 0; k < siz; k++)//在某列的唯一性 25 { 26 t = t + judge(k, j, i); 27 if (judge(k, j, i)) 28 x0 = k, y0 = j; 29 } 30 if (t == 1)//對應列上的填充 31 { 32 sd[x0][y0] = i; 33 flag++; 34 } 35 } 36 if (gl != 0) 37 { 38 for (j = 0; j < gl; j++) 39 { 40 for (k = 0; k < gw; k++)//x在某宮的唯一性 41 { 42 t = 0; x0 = 0; y0 = 0; 43 for (l = gl * j; l < gl*j + gl; l++) 44 for (m = gw * k; m < gw*k + gw; m++) 45 { 46 t = t + judge(l, m, i); 47 if (judge(l, m, i)) 48 x0 = l, y0 = m; 49 } 50 if (t == 1)//填充 51 { 52 sd[x0][y0] = i; 53 flag++; 54 } 55 } 56 } 57 } 58 } 59 if (flag == 0) 60 break; 61 } 62 }
測試方法:直接調用



可以看出,簡單一點的數獨,都可以用唯一性函數做完,復雜一點的,需要用回溯法解決。
試探性函數:
模塊關系/測試思路如下:
測試數據輸入到sd[][]后,就到達了圖中的“上一模塊”,然后直接調用即可

測試數據覆蓋的代碼除了如下代碼以外,還有合法性函數的代碼,和數據輸入輸出代碼。
1 int find(int s) 2 { 3 int x = s / siz, y = s % siz, n; 4 if (s > siz*siz - 1) 5 return 1; 6 7 if (sd[x][y] != 0)//每次探索都檢查有沒有占位 8 return (find(s + 1)); 9 else 10 { 11 for (n = 1; n <= siz; n++) 12 { 13 if (judge(x, y, n))//判斷成功 14 { 15 sd[x][y] = n;//先填充 16 if (find(s + 1))//繼續探索 17 return 1; 18 } 19 sd[x][y] = 0;//后取出 20 } 21 } 22 return 0; 23 }
測試方法:直接調用


4,算法/性能改進:
改進大概花費了3小時,對於試探性函數,算法的時間復雜度是O(n2n+1),對於唯一性函數算法的時間復雜度是O(n5),顯然唯一性函數會比試探性函數來的好
因此每多一個不確定的空位,試探性函數的工作時間都要增加很多,所以我放棄了一開始的只有合法性和試探性函數的結構,中間加個唯一性函數來為試探性函數減少工作時間:
VS 2017 性能測試結果如下,對於CPU占用率:
簡單題:
2 0 0 0 0 0 0 0 0 0 0 6 3 0 0 0 7 0 5 0 0 0 0 0 1 0 0 9 6 7 4 0 0 0 0 5 8 1 3 0 0 0 0 0 0 4 2 0 7 1 8 9 6 3 3 5 0 0 4 1 6 9 7 6 9 8 2 7 3 5 4 1 0 4 0 0 5 9 2 0 8
進行唯一性和試探性求解:

只進行試探性求解:

困難題:
0 0 0 0 0 0 0 0 0 5 6 4 1 0 0 0 0 0 0 0 8 0 0 0 0 6 0 1 0 0 0 8 4 7 5 0 0 0 0 2 7 0 0 0 0 7 0 0 0 0 3 1 2 0 0 0 3 0 0 0 0 7 9 0 0 7 0 0 8 0 3 0 0 9 0 0 3 0 6 0 0
進行唯一性和試探性求解:

只進行試探性求解:

對比可以發現,雙函數聯合求解與單一試探性求解在
簡單難度下的時候:
性能差別不大,有時甚至后者更優,比如圖中,唯一性函數用了3個單位的CPU使用量,才讓試探性的CPU使用量降低1個單位,指標上降低1個百分點
總體上性能的CPU使用量都為40多,其他簡單題的測試樣例也都是40-60以內,其中兩種求解方案也是不相上下;
困難難度下的時候:
性能差距拉大,前者秒殺后者:唯一性函數僅用了6個單位的CPU使用量,就將試探性函數占用的CPU大幅降低了53個單位,指標上降低27個百分點
總體性能上,差距大都由試探性函數拉開,其他困難題目的測試樣例也是如此,困難題目的CPU使用量浮動較大不便統計,一般在200±50,但都是雙函數聯合求解的方式明顯取勝
5,異常處理
對命令行參數的異常處理,針對了總參數,和數獨規格處理
if(argc!=9) { cout<<"總參數錯誤"; return 0; } else { if(siz>9||siz<3) { cout<<"規格參數錯誤"; return 0; } }
對最后求解完的數獨做異常處理,如果仍有空位,就會出現數獨錯誤
for(k=0;k<time;k++)//容錯性 { for(i=0;i<siz;i++) for(j=0;j<siz;j++) if(da[k][i][j]==0) cout<<"數獨錯誤"; }
(siz是數獨規格,gl和gw是宮的長和寬)
6,總程序
在主函數中進行參數的解析和文件的打開->讀取->求解->寫入->關閉操作,其中讀取求解是多次循環,因為不止一個數獨。
#include "stdafx.h" #include<stdio.h> #include<stdlib.h> #include<iostream> #include<string.h> #pragma warning(disable:4996) using namespace std; int sd[90][90], siz, gl, gw, time, da[10][90][90]; bool judge(int x, int y, int num) { int i, j, t = 0, x1, y1; if (gl != 0) x1 = x / gl, y1 = y / gw; if (sd[x][y] == 0) { for (i = 0; i < siz; i++) if (sd[x][i] == num || sd[i][y] == num) return 0; if (gl != 0) { for (i = x1 * gl; i < x1*gl + gl; i++) for (j = y1 * gw; j < y1*gw + gw; j++) if (sd[i][j] == num) return 0; } return 1; } else return 0; } void single() { int i, j, k, l, m, t = 0, x0 = 0, y0 = 0, flag, p = 1;; while (1) { flag = 0; for (i = 1; i <= siz; i++) { for (j = 0; j < siz; j++) { t = 0; x0 = 0; y0 = 0; for (k = 0; k < siz; k++) { t = t + judge(j, k, i); if (judge(j, k, i)) x0 = j, y0 = k; } if (t == 1) { sd[x0][y0] = i; flag++; } t = 0; x0 = 0; y0 = 0; for (k = 0; k < siz; k++) { t = t + judge(k, j, i); if (judge(k, j, i)) x0 = k, y0 = j; } if (t == 1) { sd[x0][y0] = i; flag++; } } if (gl != 0) { for (j = 0; j < gl; j++) { for (k = 0; k < gw; k++) { t = 0; x0 = 0; y0 = 0; for (l = gl * j; l < gl*j + gl; l++) for (m = gw * k; m < gw*k + gw; m++) { t = t + judge(l, m, i); if (judge(l, m, i)) x0 = l, y0 = m; } if (t == 1) { sd[x0][y0] = i; flag++; } } } } } if (flag == 0) break; } } int find(int s) { int x = s / siz, y = s % siz, n; if (s > siz*siz - 1) return 1; if (sd[x][y] != 0) return (find(s + 1)); else { for (n = 1; n <= siz; n++) { if (judge(x, y, n)) { sd[x][y] = n; if (find(s + 1)) return 1; } sd[x][y] = 0; } } return 0; } int main(int argc, char *argv[]) { FILE* fp; int i, j, k, l, m, t = 0, x0 = 0, y0 = 0; char *x; char *y; for (i = 0; i < argc; i++)//參數讀取 { if (strlen(argv[i]) == 1) { if (i == 2) siz = atoi(argv[i]);//讀取規格 if (i == 4) time = atoi(argv[i]);//讀取數獨個數 } if(i==6) x=argv[i]; if(i==8) y=argv[i]; } if (siz % 3 == 0)//宮的規格的轉化 { gl = siz / 3; if (gl != 0) gw = siz / gl; } if (siz % 2 == 0) { gl = siz / 2; if (gl != 0) gw = siz / gl; } if(siz==6) gl=2,gw=3; if(argc!=9) { cout<<"總參數錯誤"; return 0; } else { if(siz>9||siz<3) { cout<<"規格參數錯誤"; return 0; } } fp = fopen(x, "r");//文件讀取 if (fp == NULL) //為空時返回錯誤 return -1; for (k = 0; k < time; k++) { i = 0; j = 0; while (i != siz) //將每一個數獨划開 { fscanf(fp, "%d", &sd[i][j]); j++; if (j == siz) { j = 0; i++; } } single(); t = find(0); for (i = 0; i < siz; i++) for (j = 0; j < siz; j++) da[k][i][j] = sd[i][j];//解決后存入da三維數組 } fclose(fp); fp = fopen(y, "w"); if (fp == NULL) return -1; for (k = 0; k < time; k++) { for (i = 0; i < siz; i++) { for (j = 0; j < siz; j++) { fprintf(fp, "%d", da[k][i][j]);//依次遞交 if (j != siz - 1) fprintf(fp, " "); } if (i != siz - 1) fprintf(fp, "\n"); } if (k != time - 1) fprintf(fp, "\n\n"); } fclose(fp); for(k=0;k<time;k++)//容錯性 { for(i=0;i<siz;i++) for(j=0;j<siz;j++) if(da[k][i][j]==0) cout<<"數獨錯誤"; } return 0; }
測試:
例1:
E:\軟工\031702339\Sudoku\Sudoku>Sudoku.exe -m 9 -n 2 -i input.txt -o output.txt


例2:
E:\軟工\031702339\Sudoku\Sudoku>Sudoku.exe -m 3 -n 1 -i input.txt -o output.txt


例3:
E:\軟工\031702339\Sudoku\Sudoku>Sudoku.exe -m 8 -n 2 -i input.txt -o output.txt


例456789:





例10(異常):

總結:一步一步過來,遇到了很多問題,所以也確確實實學習到了很多,學到了Github的文件上傳,VS性能測試,VS的E0266,C4996,LNK2019,LNK2005,C2085等等各種奇葩報錯或警告的解決辦法,還有函數定制的方式改善,還有文件傳輸的具體規范等等很多很多,同時也意識到項目制定的規范性。
面對這些問題,有的繞開了,有的迎難而上,這樣的處理方式不由得令我反觀試探性函數,我不正如它一樣不斷的尋錯,不斷的改進嗎?
好啦,為期20h的周常自閉結束。容錯性函數或將改善。

