【2019.09.19】數獨(Sudoku)游戲之我見(軟工實踐第三次作業)


Github項目地址:https://github.com/MokouTyan/suduku_131700101

【2019.09.20】更新:代碼經過Code Quality Analysis工具的分析並消除所有的警告。

【2019.09.21】更新:使用性能分析工具Studio Profiling Tools來找出代碼中的性能瓶頸。

【2019.09.21】更新:想到了“即使題目是唯一解,但同時每個格子有多種可能性”的情況,新增函數check,以及格式化輸出規范

【2019.09.22】更新:更新了Github上的代碼,可以測試了

【2019.09.23】更新:新增全局變量change與chess_count,在循環中如果不發生改變即產生雙解,並結束本表盤的執行

【2019.09.24】更新:實現對“-m”,“-n”,"-i"、"-o"的判斷

【2019.09.25】更新:《構建之法》前三章讀后感:https://www.cnblogs.com/mokou/p/11582103.html

尚未完成內容:

PSP表格

Personal Software Process Stages 預估耗時 實際耗時
Planning 計划 1小時 5分鍾
Estimate 這個任務需要多少時間 26小時 24小時
Development 開發 5小時 4小時
Analysis 需求分析 (包括學習新技術) 2小時 1小時
Design Spec 生成設計文檔 2小時 1小時
Design Review 設計復審 2小時 1小時
Coding Standard 代碼規范 30分鍾 1小時
Design 具體設計 30分鍾 30分鍾
Coding 具體編碼 5小時 2小時
Code Review 代碼復審 2小時 4小時
Test 測試(自我測試,修改代碼,提交修改) 1小時 3小時
Reporting 報告 1小時 2小時
Test Repor 測試報告 1小時 30分鍾
Postmortem 事后總結, 並提出過程改進計划 1小時 2小時
Improvement Plan 過程改進計划 1小時 2小時
合計 26小時 24小時

視頻介紹所用思路

最近因為有自己的視頻在做,所以有點忙不過來,這次的視頻也是花了兩小時趕出來的,以后如果太忙了可能不會做視頻啦見諒(咕咕咕)

因為這個方法是自己想的,我沒有依據可以證明“唯一的解必須由唯一的可能性推導而出”,所以我也不知道對不對

(我的舍友是認為,即使所有的格子有一個以上的可能性數量,也可以推出唯一的解來)

視頻里是我的思路,如果看不清表盤填的數字可以在視頻右下角切換清晰度

不論是inside函數還是outside函數,他們的執行順序都是橫縱宮

(其實這三個順序無所謂的,但是三個都要去執行)

黃色是傳入函數的位置,而綠色是所要檢測的格子

源代碼解釋

全局變量介紹:

void check();
//記錄棋盤上的標記點 
bool sign[10][10][10];
//記錄標記點的可能性剩余個數 
int sign_count[10][10];
//記錄棋盤 
int checkerboard[10][10];
//記錄類型
int type;
int small_sign[10];
//用於記錄是否有變化
bool change;
int chess_count;

初始化表盤:

在CMD中輸入type和棋盤個數后進入循環

在開始處理數據前先進行表盤重置化

每個位置的可能性初期都為9個(根據輸入type大小而定)

表盤上所有數字為0(代表空)

sign第三個[]內的0號位都為False,其意義是還未填入數字

每個位置上的第一格到第九格可能性都存在(標為True)

void reset()
{
	for (int i = 1; i < type + 1; i++)
	{
		for (int j = 1; j < type + 1; j++)
		{
			//假設每個位置都有type種可能 
			sign_count[i][j] = type;
			//每個位置都是空 
			checkerboard[i][j] = 0;
			//每個位置未曾填寫 
			sign[i][j][0] = false;
			//假設每個位置的type種可能都是可實現的
			for (int k = 1; k < type + 1; k++)
			{
				sign[i][j][k] = true;
			}
		}
	}
	return;
}

inside函數解釋:

我用的第一個函數,我稱為inside函數

從頭到尾遍歷,向函數傳遞格子的位置

它會檢測當前位置橫、縱、九宮格有沒有數字的格子

如果有數字存在並且該數字可能性還未去掉

便把該格子上的相同數字的可能性去掉,同時可能性數量-1

如果當可能性等於1時,立即寫入數字

//查出所有空缺的可能性(位置上還沒有數字) 
//此時是扣除所在位置的可能性 
int inside(int x, int y)
{
	//排除橫縱向可能性
	int remove;
	for (int i = 1; i < type + 1; i++)
	{
		//如果檢測位置存在數 
		if (sign[x][i][0])
		{
			remove = checkerboard[x][i];
			//則這個空位不能出現相同數字
			//防止sign_count被誤減去,前面先判斷是不是已經變否了,未變否才變否 
			if (sign[x][y][remove])
			{
				sign[x][y][remove] = false;
				//可能性-1
				sign_count[x][y]--;
			}
			if (sign_count[x][y] == 1 && !sign[x][y][0])
			{
				write(x, y);
				return 0;
			}
		}
	}
	for (int i = 1; i < type + 1; i++)
	{
		if (sign[i][y][0])
		{
			remove = checkerboard[i][y];
			if (sign[x][y][remove])
			{
				sign[x][y][remove] = false;
				sign_count[x][y]--;
			}
			if (sign_count[x][y] == 1 && !sign[x][y][0])
			{
				write(x, y);
				return 0;
			}
		}
	}
	//宮格判斷 
	if (type == 4 || type == 6 || type == 8 || type == 9)
	{
		int beginx, beginy;
		int xplus, yplus;
		switch (type)
		{
		case 4:
			xplus = 2;
			yplus = 2;
			break;
		case 6:
			xplus = 2;
			yplus = 3;
			break;
		case 8:
			xplus = 4;
			yplus = 2;
			break;
		case 9:
			xplus = 3;
			yplus = 3;
			break;
		}
		beginx = ((x - 1) / xplus)*xplus + 1;
		beginy = ((y - 1) / yplus)*yplus + 1;
		for (int i = beginx; i < beginx + xplus; i++)
		{
			for (int j = beginy; j < beginy + yplus; j++)
			{
				if (sign[i][j][0])
				{
					if (sign[x][y][(checkerboard[i][j])])
					{
						sign[x][y][(checkerboard[i][j])] = false;
						sign_count[x][y]--;
					}
					if (sign_count[x][y] == 1 && !sign[x][y][0])
					{
						write(x, y);
						return 0;
					}
				}
			}
		}
	}
	//經過上面的判斷,如果該位置只剩下一種可能性,那么執行write()
	return 0;
}

write函數:

寫入數字的時候會把位置上的標記改為存在數字(該位置sign第三個的[0]=True)

可能性數量變為0(該位置的sign_count=0;)

防止被二次修改

//填入確定值 
int write(int x, int y)
{
	//這個位置標記為存在數字 
	change = true;
	chess_count--;
	sign[x][y][0] = true;
	sign_count[x][y] = 0;
	//填入數字 
	for (int i = 1; i < type + 1; i++)
	{
		if (sign[x][y][i])
		{
			checkerboard[x][y] = i;
			break;
		}
	}
	outside(x, y);
	return 0;
}

在寫入數字的函數結束前

此時調用第二個函數,我稱為outside函數

進行橫縱宮的外部檢查,將這個數字影響擴出去,當這個格子的“橫縱宮”檢查完成后等於說就成為一張新的表盤了

outside函數:

傳入所寫數字的位置

將它的橫縱九宮格上所有格子上的相同數字的可能性去掉

當其他位置可能性數量為1的時候

再次立即調動write函數

//去除所填位置的橫縱九宮格所有同數可能性(位置上剛填入數字) 
//此時是扣除所填位置的橫縱九宮格的其他位置可能性 
int outside(int x, int y)
{
	//remove是當前位置填入的數字 
	int remove = checkerboard[x][y];
	for (int i = 1; i < type + 1; i++)
	{
		if (!sign[x][i][0] && sign[x][i][remove])
		{
			sign[x][i][remove] = false;
			sign_count[x][i]--;
			if (sign_count[x][i] == 1 && !sign[x][i][0])
			{
				write(x, i);
			}
		}
	}
	for (int i = 1; i < type + 1; i++)
	{
		if (!sign[i][y][0] && sign[i][y][remove])
		{
			sign[i][y][remove] = false;
			sign_count[i][y]--;
			if (sign_count[i][y] == 1 && !sign[i][y][0])
			{
				write(i, y);
			}
		}
	}
	//宮格判斷 
	if (type == 4 || type == 6 || type == 8 || type == 9)
	{
		int beginx, beginy;
		int xplus, yplus;
		switch (type)
		{
		case 4:
			xplus = 2;
			yplus = 2;
			break;
		case 6:
			xplus = 2;
			yplus = 3;
			break;
		case 8:
			xplus = 4;
			yplus = 2;
			break;
		case 9:
			xplus = 3;
			yplus = 3;
			break;
		}
		beginx = ((x - 1) / xplus)*xplus + 1;
		beginy = ((y - 1) / yplus)*yplus + 1;
		for (int i = beginx; i < beginx + xplus; i++)
		{
			for (int j = beginy; j < beginy + yplus; j++)
			{
				if (!sign[i][j][0] && sign[i][j][remove])
				{
					sign[i][j][remove] = false;
					sign_count[i][j]--;
					if (sign_count[i][j] == 1 && !sign[i][j][0])
					{
						write(i, j);
					}
				}
			}
		}
	}
	return 0;
}

經常會出現這樣的情況,(write)填入第一個數字→(第一個outside)檢查第一個數字的橫向,剛好找到可能性數量為1的存在→(write)填入第二個數字→(第二個outside)檢查第二個數字的橫縱宮,減去外部的可能性,沒有出現可能性數量剛好為1的點(此時return回調用自己write,再return回上一個outside)→(第一個outside)重新返回第一個數字的函數內,繼續檢查完第一個數字的橫縱宮

(也正是因為這樣所以write里面要對填入的格子進行鎖死,防止第一個outside會遍歷 第二個及其以后的outside 會再次填入)

write和outside就這樣子互相嵌套調用,每當outside函數徹底完成后(即直到當前的outside是由 inside調用的write所調用的)是相當於生成一個新的表盤

check函數:

想到一種特殊情況

假如在同一排內有三個空格,他們的可能性分別為“12”,“23”,“12”

盡管它們都有一個以上的選項,但是中間這一格必須填3,因為它只在這一排出現過一次

所以要檢查同一排內是否有只有一個可能性種類的情況出現

void check()
{
	//檢查每一橫
	for (int i = 1; i < type + 1; i++)
	{
		for (int j = 1; j < type + 1; j++)
		{
			small_sign[j] = 0;
		}
		for (int j = 1; j < type + 1; j++)
		{
			if (!sign[i][j][0])
			{
				for (int k = 1; k < type + 1; k++)
				{
					if (sign[i][j][k])
					{
						small_sign[k]++;
					}
				}
			}
		}
		for (int k = 1; k < type + 1; k++)
		{
			if (small_sign[k] == 1)
			{
				for (int j = 1; j < type + 1; j++)
				{
					if (sign[i][j][k] && !sign[i][j][0])
					{
						//這個位置標記為存在數字 
						chess_count--;
						change = true;
						sign[i][j][0] = true;
						sign_count[i][j] = 0;
						checkerboard[i][j] = k;
						outside(i, j);
					}
				}
			}
		}
	}
	//檢查每一縱
	for (int j = 1; j < type + 1; j++)
	{
		for (int i = 1; i < type + 1; i++)
		{
			small_sign[i] = 0;
		}
		for (int i = 1; i < type + 1; i++)
		{
			if (!sign[i][j][0])
			{
				for (int k = 1; k < type + 1; k++)
				{
					if (sign[i][j][k])
					{
						small_sign[k]++;
					}
				}
			}
		}
		for (int k = 1; k < type + 1; k++)
		{
			if (small_sign[k] == 1)
			{
				for (int i = 1; i < type + 1; i++)
				{
					if (sign[i][j][k] && !sign[i][j][0])
					{
						//這個位置標記為存在數字
						chess_count--;
						change = true;
						sign[i][j][0] = true;
						sign_count[i][j] = 0;
						checkerboard[i][j] = k;
						outside(i, j);
					}
				}
			}
		}
	}
}

主函數:

int main(int argc, char *argv[])
{
	int n;
	FILE* fp1;
	FILE* fp2;
	type = atoi(argv[2]);
	n = atoi(argv[4]);
	char* InputName = argv[6];
	char* OutputName = argv[8];
	//以只讀方式打開文件
	fp1 = fopen(InputName, "r");
	if (fp1 == NULL) //
		return -1;
	//fscanf(fp1, "%d%d", &type,&n);
	//打開output.txt,並立即關閉,意義為清空文本內容
	fp2 = fopen(OutputName, "w");
	if (fp2 == NULL) //
		return -1;
	fclose(fp2);
	while (n > 0)
	{
		//重置棋盤 
		reset();
		//輸入棋盤 
		for (int i = 1; i < type + 1; i++)
		{
			for (int j = 1; j < type + 1; j++)
			{
				//cin >> checkerboard[i][j];
				fscanf(fp1, "%d", &checkerboard[i][j]);
				if (checkerboard[i][j] != 0)
				{
					sign[i][j][0] = true;
					sign_count[i][j] = 0;
					chess_count--;
				}
			}
		}
		//棋盤上以填格子的數量,當它等於零的時候棋盤被填滿
		chess_count = type * type;
		change = true;
		while (chess_count != 0 && change)
		{
			//先默認棋盤不發生變化
			change = false;
			//找出空缺位置
			for (int k = 0; k < 2; k++)
			{
				for (int i = 1; i < type + 1; i++)
				{
					for (int j = 1; j < type + 1; j++)
					{
						if (!sign[i][j][0])
						{
							inside(i, j);
						}
					}
				}
			}
			check();
		}
		//以只寫方式打開文件
		fp2 = fopen(OutputName, "a");
		if (fp2 == NULL)
			return -1;
		
		bool sign_complete = true;
		if (chess_count != 0) sign_complete = false;
		for (int i = 1; i < type + 1; i++)
		{
			for (int j = 1; j < type + 1; j++)
			{
				//cout << checkerboard[i][j];
				fprintf(fp2, "%d", checkerboard[i][j]);
				if (j != type)
				{
					fprintf(fp2, " ");
					//cout << ' ';
				}
			}
			if (n != 1 && i == type && sign_complete)
			{
				//cout << "\n\n";  
				fprintf(fp2, "\n\n");
			}
			else if (n != 1 && i == type && !sign_complete)
			{
				//cout << "\n無法再確定地填入任何一格\n因此棋盤中有空位\n\n";
				fprintf(fp2, "\n無法再確定地填入任何一格\n因此棋盤中有空位\n\n");
			}
			else if (n == 1 && i == type && sign_complete) {}
			else if (n == 1 && i == type && !sign_complete)
			{
				//cout << "\n無法再確定地填入任何一格\n因此棋盤中有空位";
				fprintf(fp2, "\n無法再確定地填入任何一格\n因此棋盤中有空位");
			}
			else
			{
				//cout << "\n";
				fprintf(fp2, "\n");
			}
		}
		//cout << '\n';//
		//fprintf(fp2, "\n");
		n--;
		fclose(fp2);
	}
	fclose(fp1);
}

代碼調試階段

Code Quality Analysis檢查結果:

測試樣例結果:

九宮格展示:

其他類型宮格展示:

性能分析工具Studio Profiling Tools分析結果

分別是上面的四個四宮格,三個六宮格,兩個九宮格題目的分析結果

總結

這個解決方案只能解決唯一解的數獨問題

面對多個解的數獨棋盤,這個方法可能解不完整,會有空缺位置

因為都是自己的思考,在完成這道題的時候完全按照自己思路,所以並不一定正確

完成后去查看過別人的代碼,他們的程序也都很有道理,我還是要學習一個~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM