Github項目地址
https://github.com/JackManTvO/sudoku
PSP表格
| PSP2.1 |
Personal Software Process Stages |
預估耗時(分鐘) |
實際耗時(分鐘) |
| Planning |
計劃 |
20 |
12 |
| Estimate |
估計這個任務需要多少時間 |
20 |
12 |
| Development |
開發(fā) |
520 |
|
| Analysis |
需求分析(包括學習新技術) |
120 |
170 |
| Design Spec |
生成設計文檔 |
30 |
55 |
| Design Review |
設計復審(和同時審核設計文檔) |
20 |
15 |
| Coding Standard |
代碼規(guī)范(為目前的開發(fā)制定合適的規(guī)范) |
20 |
10 |
| Design |
具體設計 |
60 |
80 |
| Coding |
具體編碼 |
180 |
280 |
| Code Review |
代碼復審 |
30 |
40 |
| Test |
測試(自我測試,修改代碼,提交修改) |
60 |
120 |
| Reporting |
報告 |
100 |
|
| Test Report |
測試報告 |
60 |
|
| Size Measurement |
計算工作量 |
10 |
|
| Postmortem & Process Improvement Plan |
事后總結,并提出過程改進計劃 |
30 |
|
|
合計 |
640 |
|
解題思路
生成數獨終局
??數獨的要求是在9×9的格子內,每一行和每一列和每一宮內都包含不重復的九個元素,但數獨終局的個數共有6,670,903,752,021,072,936,960(6.67×1021)種組合,盡管終局要求左上角第一個數字為固定值,其終局的種數也是巨大的。而程序設計對終局數量的需求僅在1,000,000個以內,因此,沒有必要窮舉找到前1,000,000種終局,而沒有合適的生成策略隨機生成則可能造成終局的重復。因此需制定合適的生成終局策略,而我的生成終局策略為對固定的終局模板進行數字交換以及行交換。這種策略共可以生成8!(除首數字外8個數字兩兩交換)×2(23行交換)×6(4~6行兩兩交換)×6(7~9行兩兩交換)=2,903,040種終局,大于要求的終局數。根據要求生成的終局個數對變換方式進行深度優(yōu)先搜索,第一層為數字交換,第二層為23行交換,第三層為4~6行交換,第四層為7~9行交換,以快速地遍歷第四層的葉子,以生成多個不重復的終局。
??具體步驟如下:
- 生成一固定的有效終局作為模板。
- 對模板進行數字交換。
- 對進行數字交換后的終局進行行交換(2~3行交換、4~6行交換、7~9行交換)保證宮內不重復。
- 若生成終局數滿足程序要求個數即返回,若不滿足,重復進行步驟2、步驟3。
求解數獨謎題
??對于給定的數獨謎題,按要求補充數獨的空缺,以完成數獨。因為填入的數需要滿足數獨的要求,所以每個固定位置的數值是有限制的,在剩余的可行值中選擇一個填入空白,接著填入下一個空白。若發(fā)現(xiàn)無可行解,則父節(jié)點選擇錯誤,采用回溯法,返回上一節(jié)點,填入另一可行解。直到生成樹的層數等于空缺數,則生成一可行的數獨終局,求解數獨謎題成功。
??具體步驟如下:
- 掃描題目,將空白值坐標取出存入數組A。
- 在數組A中選擇下一個坐標。
- 查詢其所在行、列、宮的數值,填入可行解數組,在可行解數組中選取下一可行解填入。
- 若填入成功,則返回步驟2,若無可行解,則在數組A中返回上一坐標,返回步驟3填入另一可行解。若A中無剩余,則返回,求解數獨成功。
設計實現(xiàn)過程
類
??我設計了三個類,分別為向量類、點類和數獨類(改進前)
Seed類(改進前)
| 變量名稱 |
類型 |
說明 |
| val |
int[9] |
生成向量值 |
| 接口名稱 |
參數類型 |
返回類型 |
功能 |
| Seed |
null |
null |
根據宏定義的首數字生成生成向量 |
| swapi |
int(腳標1),int(腳標2) |
null |
根據腳標交換向量中兩數的位置 |
| getVal |
int(腳標) |
int(結果) |
根據腳標查詢向量中數值 |
Point類(改進前)
| 變量名稱 |
類型 |
說明 |
| x |
int |
橫坐標 |
| y |
int |
縱坐標 |
| pos |
int[10] |
點的可行解標識 |
| 接口名稱 |
參數類型 |
返回類型 |
功能 |
| Point |
null |
null |
初始化類 |
| Point |
Point*(實例) |
null |
初始化類使其值與實例相同 |
Sudoku類(改進前)
| 變量名稱 |
類型 |
功能 |
| seed |
Seed |
生成向量 |
| val |
int[9][9] |
數獨 |
| zero |
Point[60] |
空缺點集合 |
| 接口名稱 |
參數類型 |
返回類型 |
功能 |
| permuate |
int(層數) |
null |
根據層數dfs全排列 |
| mod |
null |
null |
根據生成向量生成模板 |
| generate |
null |
null |
全排列交換行生成終局 |
| cpy |
const Sudoku*(模板) |
null |
根據模板復制數獨 |
| swapRow |
int(行數1),int(行數2) |
null |
交換兩行數值 |
| write |
FILE*(文件) |
null |
將數獨寫入文件 |
| set |
char*(字符串),int(行數) |
null |
將文件讀出,按行給數獨賦值 |
| dfs |
null |
int(記號) |
深度優(yōu)先搜索,成功返回1,失敗返回0 |
類圖(改進后)
關鍵函數
元素交換函數
行交換函數
解數獨函數
單元測試設計
??使用了代碼走讀,并對三個關鍵函數使用路徑測試法進行了單元測試:
元素交換函數
| 路徑 |
輸入數據 |
輸出數據 |
| path1 |
level=7 |
多個模板和終局 |
| path2 |
level=10 |
無預期輸出 |
| path3 |
level=6,nsudoku=0 |
無預期輸出 |
| path4 |
level=6,nsudoku=1 |
1個終局 |
行交換函數
| 路徑 |
輸入數據 |
輸出數據 |
| path1 |
nsudoku=72 |
72個模板和終局 |
解數獨函數
| 路徑 |
輸入數據 |
輸出數據 |
| path1 |
空點集 |
無預期輸出 |
| path2 |
包含3個點的點集 |
3個點值確定 |
改進
終局生成
??設置參數需要的棋盤數量為1,000,000,對該改進前代碼進行了10次性能分析,平均時間為138.251秒,最接近平均時間的一次為138.708秒。
??由性能分析可見,消耗最大的函數是Sudoku類的寫文件函數中的fputc函數,為了方便添加空格,我采用的是fputc函數逐個添加字符,且運行write函數后就關閉文件。這樣在1,000,000個終局生成中,會發(fā)生反復調用fputc函數和打開、關閉文件函數。因此我改動為在開始生成終局時打開文件,在寫入完成后關閉文件,將字符連接為字符串后,將一整個數獨終局以字符串寫入文件。且Point類的封裝性不強,我加強了Point類的封裝性(見類圖)。改進前在行交換時,回溯時需要置為交換前,浪費大量時間,我在行變換前,將數獨終局作為新變量保存以作為模板,方便進行二次行交換。
??改進后10次性能分析的平均時間為5.137秒,最接近平均時間的一次為5.368秒。
??由性能分析可見,改進后代碼平均時間減少為改進前的3.716%,大大縮短了生成時間。
數獨求解
??為了方便進行數獨求解,我添加了Pset類已保存空白點(見類圖),且封裝性良好。為了優(yōu)化性能,我采用了隨機的策略選取下一空白點的解,并記錄空白點處理的順序以方便回溯。這個算法的時間復雜度明顯優(yōu)于全圖遍歷。且為了減少數據的冗余,在讀入數獨時完成空白點集的添加。
代碼說明
元素交換函數
void Sudoku::permutate (int level) {
if (level == side - 2) { //全排列結束
mod (); //生成模板
generate (); //行交換
return;
}
for (int i = level; i < side; i++) {
if (i != level)
seed.swapi (level, i); //交換元素
permutate (level + 1); //遞歸
if (nsudoku == 0)
return;
if (i != level)
seed.swapi (level, i);
}
return;
}
行交換函數
void Sudoku::generate () { //生成終局
/*行變換*/
Sudoku ans;
for (int i = 0; i < 2 && nsudoku>0; i++) //第一階段
for (int j = 0; j < 6 && nsudoku>0; j++) //第二階段
for (int k = 0; k < 6 && nsudoku>0; k++) { //第三階段
ans.cpy(this);
switch (i) {
case 0:
break;
case 1:
ans.swapRow (1, 2); break;
}
switch (j) {
case 0:
break;
case 1:
ans.swapRow (4, 5); break;
case 2:
ans.swapRow (3, 4); break;
case 3:
ans.swapRow (3, 5); ans.swapRow (3, 4); break;
case 4:
ans.swapRow (3, 4); ans.swapRow (3, 5); break;
case 5:
ans.swapRow (3, 5); break;
}
switch (k) {
case 0:
break;
case 1:
ans.swapRow (7, 8); break;
case 2:
ans.swapRow (6, 7); break;
case 3:
ans.swapRow (6, 8); ans.swapRow (6, 7); break;
case 4:
ans.swapRow (6, 7); ans.swapRow (6, 8); break;
case 5:
ans.swapRow (6, 8); break;
}
ans.write (fpw);
nsudoku--; //生成數+1
if (nsudoku == 0) {
if (fpw != NULL)
fclose (fpw);
return;
}
if (fpw != NULL)
fprintf (fpw, "\n\n");
}
return;
}
解數獨函數
int Sudoku::dfs () {
int iz = pset.geti();
Point p (this->pset.getVal(iz));
/*初始化占有標識數組*/
for (int i = 0; i < 9; i++) {
if (val[p.getx ()][i] != 0 && p.getpos (val[p.getx ()][i]) == 0)
p.setpos (val[p.getx ()][i], 1);
if (val[i][p.gety ()] != 0 && p.getpos (val[p.getx ()][i]) == 0)
p.setpos (val[i][p.gety ()], 1);
}
for (int i = (p.getx () / 3) * 3; i < (p.getx () / 3) * 3 + 3; i++)
for (int j = (p.gety () / 3) * 3; j < (p.gety () / 3) * 3 + 3; j++)
if (val[i][j] != 0 && p.getpos (val[p.getx ()][i]) == 0)
p.setpos (val[i][j], 1);
/*尋找可行解*/
int found = 0;
for (int i = 1; i < 10; i++)
if (p.getpos(i) == 0) { //找到一可行解
found = 1;
val[p.getx ()][p.gety ()] = i;
p.setpos (i, 1);
pset.del (iz);
if (pset.isEmpty()) {
write (fpw);
return 1;
}
if (dfs ()) //找下一個空位的可行解
return 1;
p.setpos (i, 0);
}
return 0;
}
附加題