第二次作業——個人項目實戰之隨機數獨生成
遇到的困難及解決方法
- 解題思路以及中間編程過程描述以及相關代碼
在剛剛看到這個題目的時候,第一印象是覺得很難,因為數獨在我看來就是一個非常神奇的東西,不是一般的人能破解和構造的。
- 而我在解這個題目時的第一個想法就是:從數組第一個數按照開始往下遍歷,並利用一個函數IsSuitable(int row, int col)判斷該數在該位置是否合適,IsSuitable的具體實現就是去滿足數獨的3個規則,滿足1-9數字在行和列以及所在九宮格的不重復性,以此尋找可能的解法。但是第一次我便碰壁了,因為我沒有考慮的一種情況,天真的我想當然的認為只要遍歷一次便能生成整個數獨,沒有想到一個位置可能1-9這9個數字都不滿足的情況,這個時候便需要回溯,嘗試修改前一個位置的值去滿足這個位置的需求。這便要利用到遞歸函數和隨機函數了。第一種想法可以滿足極大的隨機性,構造的數獨矩陣可以有非常之多,甚至是全部。
- 但是由於一遍一遍的回溯速度可能會非常的慢,因此我又萌生了第二種想法(參考博客請戳),這里我們首先將9X9矩陣按順序編號為9個3X3矩陣,然后以中間的矩陣為中心,利用列變換生成2號和8號矩陣(我們將所有子矩陣按照行優先的順序從1-9進行編號),再使用行變換生成4號和6號矩陣,最后分別用4號和6號矩陣利用列變換生成1號、7號矩陣和3號9號矩陣。這樣就形成了一個合法的數獨。這種方法雖然簡單,運行效率也高,但是有一個弊端就是生成的矩陣太有規律,而且生成的矩陣種數大不如第一種想法。
- 因此第三種想法算是第二種想法的一種改變,可以在9X9矩陣中先預設一個模板,如下
只要事先使用隨機數給a,b,c,d,e,f,g,h,i賦1-9的數字值,然后在填入如圖所示模板上即可生成合理的數獨,理論上給9個字母賦值有9的階層種,但由於本次作業i是固定的,因此只有8的階層種情況,可以看出,這種方法效率很高,但有局限性,即生成的矩陣總數仍有限制。
- 最后第四種想法是這樣子的,也是前面矩陣變換法和回溯法的一個變種結合,即首先隨機初始化1,5,9號矩陣,因為1,5,9號矩陣本身是不沖突的,不需要再增加任何判斷語句,之后再利用回溯法生成剩下的矩陣。隨機生成矩陣的代碼如下:
void generate1to9(){
for(int k = 0;k < 9;k++){
temp[k]=0;
}//初始化數組
for(int i=0;i<9;i++){
temp[i]=1+rand()%9; //得到隨機數(范圍在1-9之間)
for(int j=0;j<i;j++) //判斷和前面的數是否重復
if(temp[i]==temp[j]) {i--;break;}//如果重復,重新生隨機數
}//產生9個隨機數
}// 隨機產生1-9不重復的數,結果填到temp數組
- 當然還有**其他種方法**,比如先隨機成一行或者一列的數據,然后用回溯法去補充剩下的格子,另外可以先手動填充一個9X9矩陣,然后根據列變換或者行變換去構造更多矩陣,但往往只能構造出少數的幾十種罷了。初始的數獨終盤生成規則 1、按順序將1~9填入宮格中; 2、檢查所在行、列及小九宮格是否存在相同數字 3、若存在相同數字則將數字加1 ,重復第2步 參考自[博客鏈接](http://www.cnblogs.com/JasonBourn/p/7279164.html) 當然還有其他的生成算法,有待研究。
設計實現過程
本次設計一共實現兩種方法,回溯和模板算法,一共有四個函數
void inti();利用generate1to9()初始化
void generate1to9(); 隨機產生1-9不重復的數
bool IsSuitable(int row, int col); 判別數填在該位置是否合適
bool generate(int row, int col);回溯遞歸實現核心算法
void output();輸出函數
(其中回溯法僅僅用到后三個,而模板算法則只用前兩個以及最后一個即可)
關鍵代碼分析說明
當然,最關鍵的還是代碼了,以上幾種思路我都有嘗試過實現,但是大同小異,核心還是在於回溯思想的實現,當然純矩陣變換以及上述提到的模板算法思想可以除外,這兩種相對比較容易實現。
最終我采用了直接回溯法+模板算法的思想,以下是的算法相關組件函數的實現:
(注:因為事先生成隨機矩陣的隨機性大,運氣好的話可以減少時間消耗,運氣不好的話也會耗費很多不必要的時間,因為你不知道中間是否會卡住而需要向上回溯,而當你回溯到事先生成的矩陣時,又會多出很多判斷~~~我嘗試了很久,最終還是沒有成功實現)bool Gen::generate(int row, int col) {
int nextrow, nextcol;
vector<int> number;
for (int i = 1; i <= 9; i++)
number.push_back(i);//將1-9裝入容器
while (!number.empty()) {
int randindex = rand() % number.size(); //隨機產生1到(size-1)里的 1 個 數字randindex
number.erase(number.begin() + randindex); //刪除索引位置的數據
num[row][col] = number[randindex]; //將數據填在第row行,第col列
if (!IsSu.IsSuitable(row, col)) continue; //如果 randnum不能填在number[row][col]這個位置,則繼續循環找一個合適的數
if (row == SIZE - 1 && col == SIZE - 1) {
return true;
} //如果最右下角的空也填上了,返回ture,成功生成數獨矩陣
if (col == SIZE - 1) {
nextrow = ++row;
nextcol = 0;
} //如果探索到最后一列,則換行
else {
nextrow = row;
nextcol = ++col;
} //nextrow,nextcol指向下一個空格
bool next = generate(nextrow, nextcol); //遞歸遍歷整個數獨矩陣
if (next) return true; //當返回ture時 矩陣成功生成
}
if (number.empty()) {
return false;
} //生成的時候卡住了便回溯上一層
}
[參考自](http://blog.csdn.net/bupt8846/article/details/43503447) 其中一種模板算法如下:#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<ctime>
#include<vector>
#include<fstream>
using namespace std;
int temp[9];
int num[9][9];
void generate1to9() {
for (int k = 0; k < 9; k++) {
temp[k] = 0;
}//初始化數組
for (int i = 0; i<9; i++) {
temp[i] = 1 + rand() % 9; //得到隨機數(范圍在1-9之間)
for (int j = 0; j<i; j++) //判斷和前面的數是否重復
if (temp[i] == temp[j]) { i--; break; } //如果重復,重新產生隨機數
}//產生9個隨機數
}// 隨機產生1-9不重復的數,結果填到temp數組中
char model[9][9] = {
{ 'i','g','h','c','a','b','f','d','e' },{ 'c','a','b','f','d','e','i','g','h' },{ 'f','d','e','i','g','h','c','a','b' },{ 'g','h','i','a','b','c','d','e','f' },{ 'a','b','c','d','e','f','g','h','i' },{ 'd','e','f','g','h','i','a','b','c' },{ 'h','i','g','b','c','a','e','f','d' },
{ 'b','c','a','e','f','d','h','i','g' },{ 'e','f','d','h','i','g','b','c','a' }
};
void init()
{
generate1to9();
for (int i = 0; i<9; i++) {
if (temp[i] == 6) {
temp[i] = temp[8];
temp[8] = 6;
}
}
}
void generator()
{
for (int i = 0; i<9; i++)
{
for (int j = 0; j<9; j++)
{
if (model[i][j] == 'a') num[i][j] = temp[0];
else if (model[i][j] == 'b') num[i][j] = temp[1];
else if (model[i][j] == 'c') num[i][j] = temp[2];
else if (model[i][j] == 'd') num[i][j] = temp[3];
else if (model[i][j] == 'e') num[i][j] = temp[4];
else if (model[i][j] == 'f') num[i][j] = temp[5];
else if (model[i][j] == 'g') num[i][j] = temp[6];
else if (model[i][j] == 'h') num[i][j] = temp[7];
else if (model[i][j] == 'i') num[i][j] = temp[8];
}
}
}
void output()
{
for (int i = 0; i<9; i++)
{
for (int j = 0; j<9; j++)
{
printf(" %d", num[i][j]);
}
printf("\n");
}
}
int main()
{
clock_t start, finish;
double totaltime;
start = clock();
int n;
printf("請輸入您要生成的數獨矩陣個數:\n");
int CharJduge = scanf_s("%d", &n);
for (int i = 0; i<n; i++)
{
init();
generator();
output();
printf("\n");
}
finish = clock();
totaltime = (double)(finish - start) / CLOCKS_PER_SEC;
cout << "\n此程序的運行時間為" << totaltime << endl;
system("pause");
return 0;
}
[源碼請戳](https://github.com/MarcsOne/gitLearning/tree/master/%E6%A8%A1%E6%9D%BF%E7%AE%97%E6%B3%95) 而結合**回溯法與模板算法**的具體實現思路是這樣子的:首先回溯算法生成的不再是直接的數獨矩陣,而是數獨矩陣的模板,再產生隨機的9個隨機數填入模板中,一個模板矩陣可以生成 **8!**個隨機數獨,但是模板算法的一大弊端是**生成的隨機矩陣有很大的可能會產生重復,但是結合模板算法可以很大的提高性能**,因此我采用了折中的方法,即**每個模板只產生k個隨機矩陣,k越小重復的概率就越低,但是運行時間會加大;然而k越大重復的概率就越高,運行時間會變小。** **在代碼中,每個模板矩陣的生成矩陣規模GRAND我取30,經過概率論的相關計算得出,0.0744%的概率會產生重復的矩陣,即如果要生成100萬個數獨,平均重復數獨個數為744個。** [相關代碼請戳](https://github.com/MarcsOne/gitLearning/tree/master/SudokuProject1)
測試運行以及相關說明
- 純回溯法的情況下:
詳細代碼請戳- 模板算法的情況下:
由於第二種只是臨時寫了簡單的代碼,便沒有加上文件的輸出,經過網上查閱,文件的輸出比cmd的輸出快得多,因此總體上,第二種方法的速度是比第一種高的多的,但是第二種僅能生成40320種數獨(在右上角第一個數固定的情況下,如果沒有固定則可以生成9X40320種數獨)。
現在來看看回溯法+模板法,並且在重復概率為0.0744%的情況下的運行生成100萬個隨機矩陣的結果:

單元測試以及性能分析和改進
針對於回溯法,單元測試如下:

性能分析如下:




從圖中可以看出generator占用極大,IsSuitable次之,其中generator主要在遞歸時占用最大,其中采用如上矩陣變化+回溯法的思想,成功實現話可以降低generator的遞歸次數以此來提高效率(很可惜由於個人能力有限,我未能實現出來),另外IsSuitable中盡量避免變量的重復定義也可以提高效率,其中有實現的就是之前寫代碼的時候前面num[0][0]每次都重新判斷是否i0並且j0,現在直接初始化的時候就固定num[0][0]的值。原本考慮提高vector 的push_back的速度,但是經網上查閱無果。
改動:已實現回溯法+模板法思路如上所示。
代碼見我的github。
PSP表格
| PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
|---|---|---|---|
| Planning | 計划 | 40 | 45 |
| · Estimate | · 估計這個任務需要多少時間 | 1200 | 2520 |
| Development | 開發 | 180 | 235 |
| · Analysis | · 需求分析 (包括學習新技術) | 35 | 45 |
| · Design Spec | · 生成設計文檔 | 20 | 15 |
| · Design Review | · 設計復審 (和同事審核設計文檔) | 15 | 20 |
| · Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 15 | 20 |
| · Design | · 具體設計 | 60 | 75 |
| · Coding | · 具體編碼 | 320 | 260 |
| · Code Review | · 代碼復審 | 20 | 40 |
| · Test | · 測試(自我測試,修改代碼,提交修改) | 120 | 98 |
| Reporting | 報告 | 120 | 180 |
| · Test Report | · 測試報告 | 23 | 35 |
| · Size Measurement | · 計算工作量 | 15 | 20 |
| · Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 35 | 60 |
| 合計 | 2218 | 3668 |
總結
這次項目實踐算是在計算機軟件這條路上走的第一步吧,雖然自我感覺難度有點大(畢竟很多東西不了解,比如git,github,markdown,以及visual studio以及單元測試,性能分析等工具的使用,大神忽略),可能做得不夠圓滿,但是至少我獨立算是勉強完成了一次實踐,希望自己在以后的路上可以越走越遠,學到更多東西,豐富自己。(感謝助教和老師們的指導)

只要事先使用隨機數給a,b,c,d,e,f,g,h,i賦1-9的數字值,然后在填入如圖所示模板上即可生成合理的數獨,理論上給9個字母賦值有9的階層種,但由於本次作業i是固定的,因此只有8的階層種情況,可以看出,這種方法效率很高,但有局限性,即生成的矩陣總數仍有限制。


