軟件工程基礎個人項目——生成數獨終局以及求解數獨
本次個人項目的需求可以分爲兩個主要部分:生成指定數量的不重複的數獨終局以及讀取文件內的數獨終局,求解並將結果輸出到文件。
整體項目的流程圖分析如下:
一. 項目的GitHub地址
https://github.com/MAJIUWANG/-Software-engineering-project-mjw.git
二. 填寫表格中的預計時間
PSP2.1 | Personal Software Process Stages | 預估耗時 (分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 100 | |
Estimate | 估計這個任務需要多少時間 | 2400 | |
Development | 開發 | 1200 | |
Analysis | 需求分析(包括學習新技術) | 300 | |
Design Spec | 生成設計文檔 | 100 | |
Design Review | 設計複審(和同事審覈設計文檔) | 0 | |
Coding Standard | 代碼規範(爲目前的開發制定合適的規範) | 20 | |
Design | 具體設計 | 300 | |
Coding | 具體編碼 | 300 | |
Code Review | 代碼複審 | 0 | |
Test | 測試(自我測試,修改代碼,提交修改) | 300 | |
Reporting | 報告 | 300 | |
Test Report | 測試報告 | 100 | |
Size Measurement | 計算工作量 | 20 | |
Postmortem & Process Improvement Plan | 事後總結,並提出過程改進計劃 | 60 | |
Total | 合計 | 3100 |
三. 解題思路描述
1. 生成數獨終局模塊解題思路
首先對第一個問題進行分析,通過閱讀CSDN博客我瞭解到,數獨終局的形成有自己的內在規律,可以通過應用該規律大大降低生成數獨終局的時間複雜度。
在生成數獨矩陣時,左上角第一個數爲(學號後兩位相加)%9 + 1,這爲此題目唯一的一個硬性約束條件。對於以下數獨終局,其形成中,可以從第二行開始,每行分別是第一行右移3、6、1、4、7、2、5、8列的結果。(其中左上角第一個數爲我的學號BIT-1120161752後兩位相加模9+1,即爲8):
8 9 1 2 3 4 5 6 7
5 6 7 8 9 1 2 3 4
2 3 4 5 6 7 8 9 1
7 8 9 1 2 3 4 5 6
4 5 6 7 8 9 1 2 3
1 2 3 4 5 6 7 8 9
6 7 8 9 1 2 3 4 5
4 5 6 7 8 9 1 2 3
9 1 2 3 4 5 6 7 8
按照同樣的規律,可以獲得8!即40320種不同的終局。同時,如果任意交換2-3行、4-6行或7-9行三行的位置,可以使每一種原先的終局擴展爲72個終局,得到最多2903040種不同的終局情況,即可以滿足題目中1000000種情況的要求。
爲什麼這個規律可以成功擴展生成的數獨終局呢?可以看出,其每三行內部之間平移的距離差都是3或者6,即保證了三行內每一行與前一行之間沒有重複的可能性且一定包含1-9的全部數字,三乘三方格的規律被滿足。同時,每行之間數值也都相互錯開,即3、6、1、4、7、2、5、8序列中沒有重複的數值,保證了每一列中數字的不重複。每一行的數字本身也爲1~9不重複,故數獨終局的全部條件都被滿足。
據此分析後,就可以設計實現生成數獨終局需求的代碼了。
2. 求解數獨模塊解題思路
在求解數獨終局這個問題上,我想到的是使用回溯的方法,在一個給定的題目中,根據空結點的排列按自左至右、自上至下的順序遍歷尋找數獨的解。如果當前空格可以放下某一個數字,就標記當前空格爲已搜索,存入當前數字至該空格處,標記該數字在矩陣中已訪問,並在當前情況下繼續進行下一個空結點的搜索。如果當前空格不能放下任何一個數字,說明之前的空格中填入的數字有誤,回溯至上一級將原操作撤銷,並繼續尋找新的數字進行填入,在新的可行情況下繼續進行搜索,直至9x9的數獨矩陣中沒有空格時函數終止,即找到了該數獨的一個解。
四. 設計實現過程
1. 需求分析
實現一個能夠生成數獨終局並求解數獨終局的控制檯程序。
-
生成數獨終局:在控制檯輸入指令“sudoku.exe -c 有效數字n”,可以輸出n個數獨終局到當前目錄下sudoku.txt文件中。
-
求解數獨終局:在控制檯輸入指令“sudoku.exe -s 文件路徑”,求解該文件路徑對應文件中的數獨題目,並將結果輸出到當前目錄下sudoku.txt文件中。
-
另:爲生成求解數獨終局所需題目,需要編寫挖空函數實現對生成數獨終局的一次隨機挖空。
由此可見,該項目可以分爲分析輸入、生成數獨終局、求解數獨終局三個模塊。其中分析輸入模塊在主函數中進行判斷即可,生成數獨終局和求解數獨終局需要分別調用相應的函數。
2. 建模分析
功能建模
3. 生成數獨終局模塊設計實現過程
在生成數獨終局模塊,主函數部分的僞代碼如下:
根據輸入分析得到需要生成的數獨終局個數n
通過n判斷需要生成幾輪隨機數(當n大於72時round=n/72+1)
while(round)
{
Initial函數初始化數獨的第一行
計算出當前輪數需要生成的終局個數demand(如果不是最後一輪則爲72,爲最後一輪則爲n%72)
在當前數獨第一行確定的情況下調用Produce_Sudoku()函數生成所需要的數獨終局並逐個輸出
--round;
}
在我的設計實現過程中,用到的函數有:
- void transform(double* temp1) 編碼函數,採用浮點數隨機序列生成方法(rand()/(double)RAND_MAX)*((upper)-(lowwer))+(lowwer),生成-30到30之間的8個隨機浮點數,並通過編碼函數對其從小到大排序,按其大小順序得到一個整型隨機數序列,加上左上角要求的學號後二位組成的數字,構成一個1~9的數獨第一行隨機數序列。
- void Initial()數獨第一行初始化函數,生成浮點數隨機序列並調用transform函數對其進行編碼。
- void Print_Sudoku(int tag),爲打印數獨終局函數,其中由於打印生成的數獨終局以及打印求解的數獨終局使用同一個函數,在這裏tag爲1時爲打印生成數獨終局中的結果,tag爲2時爲打印求解數獨終局中的結果。
- void Produce_Sudoku(int DEMAND),爲該模塊核心函數,其根據變換順序的不同組合,一個數獨終局可以延伸成爲72個相同數值但不同組合結構的數獨終局,並在其中調用Print_Sudoku函數進行輸出。其流程圖如下:
4. 求解數獨模塊設計實現過程
在求解數獨模塊,主函數部分的僞代碼如下:
while(一行一行地讀入含有數獨題目的文件)
{
將文件中的數獨題目逐行預存入生成的數獨數組中
if(讀入行數爲9即讀入了一個完整的數獨題目後)
{
置標記找到答案的布爾值變量爲false
Solve_Sudoku() 回溯求解數獨
Print_Sudoku() 將求解結果打印
重置標記結點是否被訪問的vis數組以及行計數變量
}
}
在最初我編寫函數時,發現如何標記數獨矩陣中一個點被訪問過了很複雜,需要記錄其在行、列以及九宮格中的出現情況。通過在網上搜索資料,我瞭解到,使用vis[3][10][10]數組來標記十分方便。其中第一維中0表示行、1表示列、2表示九宮格,第二維中表示在第幾個行、列或九宮格中,而第三維表示其中的某個數字,如果該數字被填入了,vis值置1,否則置0。
在我的設計實現過程中,用到的函數有:
- void Set_Vis(int r, int c,int num),其置數獨矩陣中r行c列數字num的位置爲1。
- void Reset_Vis(int r, int c, int num),其置數獨矩陣中r行c列數字num的位置爲0。
- bool Check_Vis(int r, int c, int num),其檢查數獨矩陣中的r行c列的位置是否可以填入一個值爲num的數字。
- void Print_Sudoku(int tag),爲打印數獨終局函數,在這裏tag置2。
- void Solve_Sudoku(int r, int c),其爲求解數獨模塊的核心函數,利用回溯算法對數獨題目進行求解,其中調用Set_Vis、Reset_Vis以及Check_Vis函數,以及迭代調用自身。
5. GUI界面的生成
在生成GUI界面時,我原本採用MFC控制程序進行設計,然而由於時間只剩下一天多一點了,又感到不是很容易操作,就改爲使用之前小學期熟悉過的Windows窗體程序進行GUI界面的設計。
將GUI界面設計分成填充數獨題目以及判斷提交界面兩個主要部分。在填充數獨題目時,從文件sudokuQuestions_1000.txt按順序讀取數獨題目,在有數字時置終局棋盤爲相應數字,無數字時置空。讀取完數獨題目後,僅需要判斷填入的數獨是否滿足其適應規則即可,如果正確則彈出“你是真滴厲害,做對了喔!”,填錯了就彈出“你做錯了喔!”,如果在還沒有完成數獨題目即棋盤中有空格時就點擊了提交按鈕,就彈出“你還沒有做完喔!”。
界面如下:
作答正確顯示:
作答錯誤顯示:
作答沒有完成顯示:
五. 程序改進
1. 應用代碼分析工具消除警告
在編寫完程序的首個版本後,應用vs自帶的代碼分析工具分析得到的警告如下:
解決方案爲如下:
警告一:數值類型不匹配。程序中得到生成數獨終局的個數時需要將字符串數據轉換成整型數字,原本使用了pow函數,其導致了數值類型的不匹配問題。故採用原始的for循環進行字符串向整型數字的轉化消除了警告。
警告二:輸出文件目錄未添加斜槓,而是讓系統自動補全了。在項目屬性中設定輸出文件目錄BIN時在其後補上斜槓即可。
應用解決方案後如下圖可見,程序警告得到了消除。
2. 程序性能改進
改進一:代碼塊複用
在函數設計過程中,我把需要多次調用的代碼塊寫成了函數。例如transform函數、Print_Sudoku函數等,同時將打印數獨終局結果的函數和打印求解數獨結果的函數合二爲一,通過參數標記的不同調用函數的不同部分,使得函數的功能更加清晰,代碼的整體結構更爲簡潔。
改進二:改機隨機數生成方法
在生成數獨終局模塊中,生成數獨第一行的隨機序列時,我最先使用的是最爲基礎的rand()%9+1的方式生成1-9的隨機數,然而這種方式如果要保證每一次生成的隨機序列1-9的數字不重複,在生成隨機數種子上將會浪費過長的時間。我採用的改進方法爲一次性生成一串-30到30之間的浮點型序列,然後調用transform函數對其進行編碼,將其大小排列順序得到的整型序列作爲數獨第一行的隨機序列。由於排序中已經可以保證數字相異,故有效地解決了之前的問題。
改進三:改進輸出
在生成數獨終局模塊,最初我的程序輸出所用的時間很長,滿足不了項目的正確性需求,這讓我很苦惱。之後,我通過查閱資料發現,可以將輸出模塊由一個一個地打印數獨矩陣中的整型數字改爲將一個數獨終局存入一個字符型數組中,按字符串輸出的方式輸出一整個數獨終局。改進了輸出模塊後,我的程序速度提升了很多。
3. 應用性能分析工具
應用性能分析工具進行分析後發現,程序中主函數佔用CPU最多,其次是讀寫文件的函數,剩餘的函數佔用CPU比例較小。
六.代碼說明
1.生成數獨終局模塊代碼說明
核心函數代碼如下:
void Produce_Sudoku(int DEMAND)
{
int count = 0;
for (int i = 0; i < 2; i++) //共可以生成72種不同的排序方式
{
for (int j = 0; j < 6; j++)
{
for (int k = 0; k < 6; k++)
{
char s1[15], s2[15];
strcpy(s1, move1[i]);
strcpy(s2, move2[j]);
strcpy(move_boss, strcat(strcat(s1, s2), move3[k])); //排列組合出的字符型序列
To_int(); //將字符型序列轉換爲int型
for (int q = 2; q <= 9; q++) //逐行生成一個數獨矩陣
{
for (int w = 1; w <= 9; w++)
{
int m = (w - move_boss_int[q - 1] + 9) % 9;
if (m == 0) //對應最後一列的情況需要特殊處理
m = 9;
a[q][w] = a[1][m];
}
}
++count;
Print_Sudoku(1);
if (count == DEMAND)//如果滿足了這一輪的需求,就退出
return;
}
}
}
}
其中,move1、move2、move3數組的定義如下:
char move1[10][5]={"036","063"}; //移動規則
char move2[10][5]={"258","285","528","582","852","825"};
char move3[10][5]={"147","174","417","471","714","741"};
2.求解數獨模塊代碼說明
核心函數代碼如下:
void Solve_Sudoku(int r, int c)
{
while (a[r][c] != '0') //找到一個空的數獨位置
{
if (c < 8)
c++;
else //再來一輪
{
c = 0;
r++;
}
if (r == 9) //找到了一個答案即9x9數獨中沒有0,那就是找到了一個解答
{
Found_Ans = true;
return; //走嘍
}
}
bool Can_Search=false; //標記回溯算法中當前結點是否可以搜索
for(int i=1;i<=9;i++)
{
if(Check_Vis(r,c,i))
{
Can_Search=true; //標記可以搜索
Set_Vis(r,c,i); //當前結點搜索過
a[r][c]=i+'0';
Solve_Sudoku(r,c);
if(Found_Ans) //剪枝
return;
Can_Search=false;
Reset_Vis(r,c,i);
a[r][c]='0';
}
}
}
七. 填寫表格中的實際時間
PSP2.1 | Personal Software Process Stages | 預估耗時 (分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 100 | 80 |
Estimate | 估計這個任務需要多少時間 | 2400 | 2400 |
Development | 開發 | 1200 | 800 |
Analysis | 需求分析(包括學習新技術) | 300 | 200 |
Design Spec | 生成設計文檔 | 100 | 120 |
Design Review | 設計複審(和同事審覈設計文檔) | 0 | 0 |
Coding Standard | 代碼規範(爲目前的開發制定合適的規範) | 20 | 20 |
Design | 具體設計 | 300 | 240 |
Coding | 具體編碼 | 300 | 420 |
Code Review | 代碼複審 | 0 | 0 |
Test | 測試(自我測試,修改代碼,提交修改) | 300 | 200 |
Reporting | 報告 | 300 | 350 |
Test Report | 測試報告 | 100 | 60 |
Size Measurement | 計算工作量 | 20 | 20 |
Postmortem & Process Improvement Plan | 事後總結,並提出過程改進計劃 | 60 | 30 |
Total | 合計 | 3100 | 2540 |
八. 實驗總結
在完成這個實驗的過程中,我還是有許多的收穫的。我熟悉並掌握了控制檯程序的編寫方法,掌握了生成數獨終局以及求解數獨終局的算法,同時也熟悉了使用GitHub管理項目代碼的方法以及如何撰寫CSDN博客。
但是還是明顯地感覺到了自己的不足,我全篇代碼都是使用C語言面向過程進行編程的,沒有實現其向C++的轉化,而在該問題中使用面向對象編程可以更方便地進行測試以及代碼結構的管理。