非常經典的一道題:
N皇后問題:
國際象棋中皇后的勢力范圍覆蓋其所在的行、列以及兩條對角線,現在考察如下問題:如何在n x n的棋盤上放置n個皇后,使得她們彼此互不攻擊 。
免去麻煩我們這里假定n不是很大。。。
(圖片來自百度百科(這是8皇后問題的一種解法))
某leetcode大犇曾說過:“這個問題和解數獨題目有一個很大的共同點,那就是:我都不會。”
好了下面開始分析:(廢話警告)
初步判斷這問題的特點有:
1.有個場地來放置單位。
2.各個單位之間有制約。
3.沒有特殊的數學方法,得把某一個擺法擺出來才能判斷是否可行。
於是萌新一般都會這么想:對於1,:我搞個二維數組來存。對於2:我搞個判定函數來一個一個判定。對於3:我暴力枚舉。
那么算法框架大概就是:我對二維數組的所有情況進行枚舉,然后對每種情況進行判定
over ,輸個數字n,按下回車,雙手離開鍵盤,等了老半天發現命令提示符只有個光標在跳動 |
如果是在做網題,說不定就判定是超時或者內存溢出,過不了。
然后就開始思考怎么優化:
對於1,我可以縮減嗎····,改成一維數組,,,似乎徒增麻煩。
對於2,我可以有更巧妙的判斷方法嗎?·············有個鬼,還不得把每個棋子的每一行每一列每兩個斜線都瞅一下。
對於3,我一定要把所有情況都舉出來嘛?···(於是腦子里面開始擺起了棋子,模擬算法過程)
然后就會發現 比如第一行前兩個格子一開始就擺了兩個相鄰的棋子,誒這不明擺着皇后互懟了嗎!后面還繼續枚舉就太傻了吧···
這種傻事情做了不是白白增加耗時嗎!
所以就思考如何在這種“傻情況”出現的時候就pass掉后面所有情況。。。
於是腦子陷入了一團亂麻。。。
打住!
咱們換個思考方式,既然直接想有點困難,不如我們想辦法先做點處理,讓這些玩意兒便於我們把弄。
再回頭想想規則,皇后可以吃掉所處的一行一列以及斜線上的棋子,那也就是說,每一行都最多只能有一個棋子,每一列都最多只能有一個棋子,
那我們不妨把每一行看成一組!
那么這一組就只有n個可能性(一行n個位置 每一次只有一個位置被占用)
也就是單個一行有n種可能性
因為有n行
哈哈那么就瞬間只需要討論n*n種可能性了!
還記得我們之前的思考嗎?:
對於3,我一定要把所有情況都舉出來嘛?···(於是腦子里面開始擺起了棋子,模擬算法過程)
然后就會發現 比如第一行前兩個格子一開始就擺了兩個相鄰的棋子,誒這不明擺着皇后互懟了嗎!后面還繼續枚舉就太傻了吧···
這種傻事情做了不是白白增加耗時嗎!
所以就思考如何在這種“傻情況”出現的時候就pass掉后面所有情況。。。
像這種情況:
(我的天這皇后畫的...)
顯然下面3,4,5····行都不用枚舉了,直接pass掉
於是我們思路慢慢清晰了起來:
我們從第一行開始枚舉,第一行第一格:
然后枚舉第二行,也從第一格開始:
判定一下,哇,不行,咋辦呢?這情況下面所有行都沒有枚舉的必要了,但是本行還是可以繼續的。。。於是我們開始枚舉第二行第二個情況:
不行(斜線上互吃)。
繼續第二行下一個情況:
行嘞!
那么我們就可以開第三行了:
不行(一列上吃)。
不行(斜線上吃)
不行。
還不行
行嘞!
··················
好了,算法思路大概就出來了。
我們就這樣干下去,直到最后一行可行,那么我們就獲得了一個可行解了,
然后我們想繼續獲得其他解,那么繼續枚舉下去,但是要退一步:
先從倒數第一行開始,我們枚舉下一個,有解則輸出,無解則繼續。
倒數第一行結束了,咋辦?
別忘了倒數第二行的情況還不一定枚舉完了呢。
於是退到倒數第二行,繼續如上操作。
··················
干說着沒用,擼代碼:
#include<iostream> using namespace std; int main() { return 0; }
先准備好需要用的存儲(這里用64個綽綽有余)
ans是用來存每一次的解 ans[1]表示第一行的那個元素的所在列的位置
1 #include<iostream> 2 3 using namespace std; 4 bool colMark[64] = { 0 };//元素所在列 如果有了元素在第k列 那么colMark[k]就標記上true 5 bool naMark[64] = { 0 };//捺,撇···顧名思義,表示元素所在的兩個斜線o.o 原理同上 6 bool pieMark[64] = { 0 }; 7 int total = 0; //解的總個數 8 int N = 0; 9 int ans[64] = { 0 }; 10 int main() 11 { 12 return 0; 13 }
然后下面先寫一個顯示函數:
1 void showOneSolution()//用來顯示一個解 2 { 3 total++; 4 for (int i = 1; i<=N; ++i) 5 { 6 cout << ans[i] << " "; 7 } 8 cout << endl; 9 }
下面是核心:
void Dfs(int i) //可以百度學習 深度優先搜索 { if (i > N) //判斷是否已經枚舉完了N行 { showOneSolution(); //枚舉完了就輸出(此刻我們處於N+1行) return; //返回到第N行 } for (int j = 1; j <= N; ++j) //這里的j表示 本行的第j列 { if ((!colMark[j])&&(!naMark[j-i+N])&&(!pieMark[i+j])) //這里的"j-i+N" "i+j"建議自己體會 { //判斷這個格子所在的兩個斜線和所在列是否為空 colMark[j] = true; naMark[j - i + N] = true; pieMark[i + j] = true; ans[i] = j; Dfs(i + 1); colMark[j] = false; //這個格子枚舉完了要進入本行第j+1個格子(或者結束本行) 走之前別忘了恢復標記 naMark[j - i + N] = false; pieMark[i + j] = false; } } }
最后組裝起來:(當然啦,還是建議自己擼代碼,你會發現很多意想不到的問題~解決問題的同時也是進步)
1 #include<iostream> 2 3 4 using namespace std; 5 bool colMark[64] = { 0 }; 6 bool naMark[64] = { 0 }; 7 bool pieMark[64] = { 0 }; 8 int total = 0; 9 int N = 0; 10 int ans[64] = { 0 }; 11 void showOneSolution()//用來顯示一個解 12 { 13 total++; 14 for (int i = 1; i<=N; ++i) 15 { 16 cout << ans[i] << " "; 17 } 18 cout << endl; 19 } 20 void Dfs(int i) 21 { 22 if (i > N) //判斷是否已經枚舉完了N行 23 { 24 showOneSolution(); //枚舉完了就輸出(此刻我們處於N+1行) 25 return; //返回到第N行 26 } 27 for (int j = 1; j <= N; ++j) //這里的j表示 本行的第j列 28 { 29 if ((!colMark[j]) && (!naMark[j - i + N]) && (!pieMark[i + j])) 30 { //判斷這個格子所在的兩個斜線和所在列是否為空 31 colMark[j] = true; 32 naMark[j - i + N] = true; 33 pieMark[i + j] = true; 34 ans[i] = j; 35 Dfs(i + 1); 36 colMark[j] = false; // 走之前別忘了恢復標記 37 naMark[j - i + N] = false; 38 pieMark[i + j] = false; 39 } 40 } 41 } 42 int main() 43 { 44 cin >> N; 45 Dfs(1); 46 cout << total; 47 system("pause"); 48 return 0; 49 }
總結:
思路混亂不妨換個角度理一理。
在紙上面畫一畫算法過程會有不少幫助。
代碼少出錯還是得多擼。
想什么呢?!擼代碼!