採用回溯法最經典的例子是解決8皇后和迷宮的問題。不習慣走別人的路,因此下面介紹下用回溯法解數獨遊戲。寫這個算法的原由是以前在玩數獨遊戲時,遇到了難解的專家模式,就想着寫程序來暴力破解,是否是很無賴,啊哦……算法
數獨(すうどく,Sūdoku),是源自18世紀瑞士發明,流傳到美國,再由日本發揚光大的一種數學遊戲。是一種運用紙、筆進行演算的邏輯遊戲。玩家須要根據9×9盤面上的已知數字,推理出全部剩餘空格的數字,並知足每一行、每一列、每個粗線宮內的數字均含1-9,不重複。bash
數獨盤面是個九宮,每一宮又分爲九個小格。在這八十一格中給出必定的已知數字和解題條件,利用邏輯和推理,在其餘的空格上填入1-9的數字。使1-9每一個數字在每一行、每一列和每一宮中都只出現一次,因此又稱「九宮格」。下圖是一個完整的數獨例子。函數
下圖是iPad上一個數獨遊戲專家模式下的截圖。81個格子,只給了17個數字,確實有點難度哈。 2.png測試
將上圖轉化成程序可以識別的輸入,{1,1,9}表示第一行第一列的格子數字是9。ui
{1,1,9},{1,2,1},{1,8,4},{3,4,5},{3,6,3},{4,1,5},{4,3,6},{4,6,8},{4,7,3},{6,5,1},{6,8,2},{6,9,4},{7,1,8},{7,3,5},{8,3,3},{9,5,4},{9,9,1}
複製代碼
運行程序,得出以下解:spa
9 1 7 | 6 8 2 | 5 4 3
3 5 2 | 1 9 4 | 7 6 8
6 8 4 | 5 7 3 | 1 9 2
---------------
5 9 6 | 4 2 8 | 3 1 7
4 2 1 | 7 3 6 | 9 8 5
7 3 8 | 9 1 5 | 6 2 4
---------------
8 7 5 | 2 6 1 | 4 3 9
1 4 3 | 8 5 9 | 2 7 6
2 6 9 | 3 4 7 | 8 5 1
---------------
3d
仔細觀察不難發現,每一行、每一列和每個九宮格里都是數字1~9不重複。這就構成了一個數獨的解。也證實,算法經過測試。code
算法在解數獨題時,在依次決定每一個格子的數字時會根據以前已經填入的數字判斷當前格子可能填入的數字,而後選擇其中一個再去跳到下一個格子。當後面出現無解的狀況(一個格子沒有可填入的數字),就依次回退到上一個格子,選取下一個可能填入的數字,再依次執行下去。直到填入了最後一個格子,纔算完成了數獨的一個解。cdn
在包含問題的全部解的解空間樹中,按照深度優先搜索的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,若是包含,就從該結點出發繼續探索下去,若是該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜索算法)。 若用回溯法求問題的全部解時,要回溯到根,且根結點的全部可行的子樹都要已被搜索遍才結束。 而若使用回溯法求任一個解時,只要搜索到問題的一個解就能夠結束。blog
/** * 填充數字。實質上是深度遍歷一棵具備9分支的樹,直至遍歷到它的第81層。必定要經過一些判斷剪斷其無用分支,否則該算法的時間複雜度至關可怕 */
void set_item(Item* items, int position) {
if (position==ROW*COLUMN) {
printf("第%d個\n",++count);
//填充完最後一個元素,輸出結果
for (int i = 0; i < COLUMN * ROW; i++) {
printf("%3d", items[i].numbers[0]);
if ((i + 1) % COLUMN == 0)
printf("\n");
}
printf("\n");
return ;
}
if(items[position].choose!=0){
//解數獨題時,當前格子已經有填入的數字,就跳到下一個格子。
//若是窮舉出9x9數獨全部的解,這個if判斷不執行
set_item(items, position + 1);
return;
}
int num =0;//保存當前格子可輸入的數字
while (( num= getUseableNum(items, position)) != 0) {
//while循環依次找出當前格子,全部可能插入的數字,再交給下面的if函數判斷。找不到就結束while循環,回到上一個格子,找到了就遞歸進入下一個格子。
//不爲0,可輸入
//判斷能夠填入num
if (is_can_set(items, num, position) == 1) {
//設置下一個格子
set_item(items, position + 1);
}
}
//找不到可輸入的數字,清空當前格子填充記錄,回到上一個格子
//若爲第一個格子,找不到填充的數字就結束
if (position != 0&&items[position].numbers[0]!=0)
goback(items, position);
}
複製代碼
測試算法代碼段
//定義好17個已經確認的格子
Point points[17]={ {1,1,9},{1,2,1},{1,8,4},{3,4,5},{3,6,3},{4,1,5},{4,3,6},{4,6,8},{4,7,3},{6,5,1},{6,8,2},{6,9,4},{7,1,8},{7,3,5},{8,3,3},{9,5,4},{9,9,1} };
//初始化數獨矩陣
init_matrix(items, COLUMN * ROW);
//將17個格子填入數獨矩陣中
init_point(items, points, 17);
//從第一個格子開始遍歷
set_item(items, 0);
複製代碼
附上用到的自定義的函數,用來決定當前格子可填入的數字的判斷。
/** * 獲取可用的數字 */
int getUseableNum(Item* items, int position) {
for (int i = 1; i < SCALE * SCALE + 1; i++) {
if (items[position].used[i] == 0 && items[position].numbers[i] == 0) {//數字i(1~9)未被使用過且在它所屬的行、列、九宮格里均未出現過就表示能夠填入數字i
//有可用的數字,直接返回
return i;
}
}
//沒有可用的數字,返回0
return 0;
}
/** * 判斷一個數字在指定的格子中是否能夠插入,返回0表示不能插入,尋找下一個, */
int is_can_set(Item* items, int num, int position) {
//找到影響的行
find_row(row_matrix, position);
for (int i = 1; i < SCALE * SCALE + 1; i++) {
if(items[row_matrix[i]].numbers[0]==num&&(items[row_matrix[i]].used[num]*items[row_matrix[i]].numbers[num])!=0){
items[position].used[num]=1;
return 0;
}
}
//找到影響的列
find_column(column_matrix, position);
for (int i = 1; i < SCALE * SCALE + 1; i++) {
if(items[column_matrix[i]].numbers[0]==num&&(items[column_matrix[i]].used[num]*items[column_matrix[i]].numbers[num])!=0){
items[position].used[num]=1;
return 0;
}
}
//找到影響的塊
find_block(block_matrix, position);
for (int i = 1; i < SCALE * SCALE + 1; i++) {
if(items[block_matrix[i]].numbers[0]==num&&(items[block_matrix[i]].used[num]*items[block_matrix[i]].numbers[num])!=0){
items[position].used[num]=1;
return 0;
}
}
if (items[position].numbers[0] != 0) {
// items[position].used[items[position].numbers[0]]=0;
//重置影響到的行
changeItemNum(items, row_matrix, items[position].numbers[0],
SCALE * SCALE + 1, -1);
//重置影響到的列
changeItemNum(items, column_matrix, items[position].numbers[0],
SCALE * SCALE + 1, -1);
//重置影響到的塊
changeItemNum(items, block_matrix, items[position].numbers[0],
SCALE * SCALE + 1, -1);
}
items[position].numbers[0] = num;
//設置影響到的行
changeItemNum(items, row_matrix, num, SCALE * SCALE + 1, 1);
//設置影響到的列
changeItemNum(items, column_matrix, num, SCALE * SCALE + 1, 1);
//設置影響到的塊
changeItemNum(items, block_matrix, num, SCALE * SCALE + 1, 1);
items[position].used[num] = 1;
//能填入
return 1;
}
/** * 清空當前格子輸入的信息 */
void goback(Item* items, int position) {
//刪除當前數字產生的影響
int currentNum = items[position].numbers[0];
//重置影響到的行
find_row(row_matrix, position);
changeItemNum(items, row_matrix, currentNum, SCALE * SCALE + 1, -1);
//重置影響到的列
find_column(column_matrix, position);
changeItemNum(items, column_matrix, currentNum, SCALE * SCALE + 1, -1);
//重置影響到的塊
find_block(block_matrix, position);
changeItemNum(items, block_matrix, currentNum, SCALE * SCALE + 1, -1);
items[position].numbers[0]=0;
//清空這個格子裏面,使用過的數字記錄
for (int i = 1; i < SCALE * SCALE + 1; i++) {
items[position].used[i] = 0;
}
}
/** *修改item中的數字,items表示整個數獨。matrix表示須要修改的下標,length表示須要修改個數。flag +1表示設置,-1 表示重置 */
void changeItemNum(Item* items, int* matrix, int num, int length, int flag) {
for (int i = 1; i < length; i++) {
//須要修改的位置
int position = matrix[i];
items[position].numbers[num] += flag;
}
}
複製代碼
這個算法,不只能夠用來解數獨題,還能夠用來遍歷數獨全部的終盤,因爲9x9的數獨,終盤數量巨大,共6,670,903,752,021,072,936,960(約爲6.67×10^21)種組合,程序一直運行不完結果。若是對這個值沒有概念,請試想將全部的終盤存在txt文本中,只存儲數字將佔用6.07x10^9 TB存儲空間,相信沒有哪臺電腦可以作到。換句話說,我電腦CPU的主頻是2.4GHz,意思是1秒鐘執行2.4x10^12條指令,假設解出一種終盤須要一條指令(事實上遠大於1條),消耗的時間是88年,本寶寶等不了那麼久。慶幸的是4x4 的數獨只有288個終盤,程序仍是可以很快的完美輸出全部解。爲何二者差異這麼大,由於窮舉法數獨的算法時間複雜度是T= n^(n^2). n = 4時,T = 4294967296。n = 9時,T= 1.97Ex10^77。呵呵噠……