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分析結果
分別是上面的四個四宮格,三個六宮格,兩個九宮格題目的分析結果
總結
這個解決方案只能解決唯一解的數獨問題
面對多個解的數獨棋盤,這個方法可能解不完整,會有空缺位置
因為都是自己的思考,在完成這道題的時候完全按照自己思路,所以並不一定正確
完成后去查看過別人的代碼,他們的程序也都很有道理,我還是要學習一個~