八皇后问题——列出所有的解,可推至N皇后
《數據結構》——鄧俊輝版本 讀書筆記
今天學習了回溯法,有兩道習題,一道N皇后,一道迷宮尋徑。今天,先解決N皇后問題。由于筆者
擅長java,所以用java重現了八皇后問題。
注意是java實現版本,其實用什么語言,都一樣。
寫在前面的話,
如果想看代碼,直接往下拉,注釋寫的很清楚,憑借注釋應
該也能理解八皇后問題。
1. 首先知道什么是八皇后問題,不過,相信看到這里的同學,都應該知道八皇后
問題,簡單概括為:每個皇后的勢力范圍如下圖紅線標注所示,也就是橫縱軸、兩條對角線 。在一個皇后的勢力范圍內,就不能再出現其他皇后了。
2. 這里選用經典的八皇后問題,想讓八個皇后,在棋盤上相安無事。用代碼怎么實現?首先,明白一件事,電腦就是小萌新啊,只會計算最簡單的計算,2+3,它興許都得差分為(1+1)+(1+1+1)來計算,唯一的優勢就是計算的速度比我們快。
3. 剛看到回溯法的時候,想著要用代碼,寫出來。感覺無從下手,以為很難的,以為電腦不會笨的一個一個去嘗試,以為有很神奇的方法存在。學完以后,才發現,很簡單,發現回溯法,也是從窮舉起手的,只不過帶著一定的策略,說白了,優化過的暴力求解。以后,遇到各種聽著很高大上的問題別慌,比如,動態規劃、深度優先搜索。都是窮舉?。≈皇羌恿艘恍┎呗?。I can I believe !你之所能,是因為你想信你能!
解題思路:
都是文字講解,主要是筆者沒有圖?。?!誒,希望你們讀完,能學會八皇后問題。如果文字看不下去。
可以直接去看代碼,注釋寫的很詳細。
一行只能有一個皇后,這個根據游戲規則中的皇后的勢力就可以得知。
首先讓皇后從起點開始擺放。因此,第一個皇后的位置先擺放在(0,0)上,第二個皇后根據游戲規則,只能在第二行尋找合適的位置。在第二個皇后的位置確定以后,再去第三行尋找第三個皇后的位置,以此內推。直到所有的皇后的位置都確定了,問題的一組解,就出來了!但是問題并不是這么簡單。
比如現在有三個皇后A、B、C。A在第一行,B在第二行都找到合適的位置了,在第三行所有的位置都是A或者B皇后的勢力范圍。尷尬了,皇后之間要打架了。怎么辦?聰明的你,可能會想到讓B皇后,重新找一個合適的位置(這個合適的位置,從B當前的下一個位置開始尋找),然后再來為 C選擇合適的位置。如果C還是沒有合適的位置,就再次為B尋找合適的位置,循環往復這個動作。
細心的你,可能會有疑問,每次C皇后,找不到合適的位置,就去讓B重新尋找合適的位置難道B皇后就一直會有新的合適的位置嗎?答案是否定的,當B皇后在它所處的行,再也找不到合適的位置,就說明A皇后的位置,也需要變動了。A皇后的位置變動,跟其他皇后不一樣,因為,當前B、C皇后都因為沒有合適的位置,所有,就沒有擺放在棋盤上。因此,這里棋盤上就一個皇后A,所有它只需要右移一個位置即可。A皇后位置更新以后,就再去為B皇后找合適位置,如果有合適位置,就再去為C皇后尋找合適的位置;如果B皇后沒有合適的位置,就再次右移A皇后,循環上面的過程。
有的同學,可能會有疑問,照你說的,計算機就是通過窮舉解決問題的,上面的解題過程,計算機確實也是笨笨的一個一個位置去尋找的。那么這算法有效率嗎?答案是有效率的。不知你們發現沒有,我們每個為下一個皇后尋找合適的位置的時候,都與之前存在的皇后做了比較。只有,有合適的位置,才會擺放皇后;這里尋找合適的位置,會排除沒有意義的試探,比如,將皇后擺放在之前某個皇后的勢力范圍內。沒有排除沒意義的試探,才是正正的傻傻的窮舉。我們的算法,是帶著策略的窮舉。
上述每次為下一個皇后,尋找合適的位置,就是試探。每次試探不到合理的位置,回頭去改變前一個皇后的位置,就是回溯;排除沒意義的試探,就是剪枝。上訴的解題過程,就是一個回溯法的思路。
用代碼求解八皇后問題
回溯法,如何回溯?需要我們的棧結構,來保存我們每次已經找到合適位置的皇后的列坐標,坐標從0開始。為什么不保存行坐標呢?因為,一行只能有一個皇后,棧中皇后的下標,就是對應皇后的行坐標啦。
首先我們定義一個皇后類
在皇后類中,我們寫了一個方法判斷,兩個皇后之間是否安全
/**
* 皇后類
*/
class Queen {
// 棋盤中的皇后下標,從0開始
int x;
int y;
public Queen(int x, int y) {
this.x = x;
this.y = y;
}
public Queen() {
}
/*
判斷皇后所處位置是否相互安全,接收一個皇后對象
安全返回真,不安全返回假
*/
public boolean isSafe(Queen q) {
return !(this.x == q.x ||
this.y == q.y ||
this.x + this.y == q.x + q.y ||
this.x - this.y == q.x - q.y);
}
}
在測試類中定義一個方法,求新皇后應該放置的合理位置
這個方法中創建一個新皇后對象,與之前已經存在的皇后作比較。比較函數則是皇后類定義的方法。
找到合適位置,就返回合適的位置的列坐標;返回-1,代表沒有合適位置。
/**
* 為皇后尋找合法位置
*/
public int setY(int N, ArrayList<Integer> list, int y) {
Queen q;
Queen q_exist;
// 做個標記,
boolean flag ;
// 遍歷一行的所有位置
for (int z = y; z < N; z++) {
// 為每個格子創建一個新的皇后
q = new Queen(list.size(), z);
// 起始標記為真
flag = true ;
for (int x = 0; x < list.size(); x++) {
// 創建之前存在的皇后
q_exist = new Queen(x, list.get(x));
// 安全 就返回真,循環作比較
if (!(q_exist.isSafe(q))) {
// 只要和之前存在的的任何一個皇后沖突,
// 并且說明當前皇后不是真的,將標記改為假
// 并跳出當前跳出循環,
flag = false;
break;
}
}
// 如果標記還為真,說明皇后是對滴,并返回新皇后的列坐標,結束函數
if (flag) {
list.add(q.y);
return q.y;
}
}
// 函數沒有提前結束,說明沒有合適的位置,返回-1;
return -1;
}
求出所有組解
跟著注釋看,相信大家都能學會
/**
* 求解N皇后的問題,并給出所有的解,找到一組解,就讓最后的皇后換位置,直達第一個皇后的角標越界
* 筆者認為 next_y 變量的動態賦值的 需要很好的理解
* @param N 表示一共有多少皇后,也是棋盤的規模 N x N
*/
private void placeQueen(int N) {
// 表示一共有多少種解法
int all = 1;
// 代表皇后的y坐標
int y = 0;
// 在當前行,下一個位置開始重新尋找合適的位置
int next_y = 0;
// 建立一個棧,用于回溯
// 棧中存放的是皇后的列坐標,也就是y坐標
ArrayList<Integer> list = new ArrayList();
// 定義一個皇后
Queen queen;
// 開始計算每種情況
while (true) {
// 先判斷之前有沒有皇后存在
if (list.size() == 0) {
// 創建一個皇后對象,從0開始擺放
queen = new Queen(0, y);
// 將皇后的y坐標記錄下來,而x坐標就是棧的長度-1
list.add(queen.y);
// 下一次,棧再次空了,說明第一行的皇后需要右移
y++;
} else {
// 如果已經有皇后存在,既需要為當前皇后尋找合適的位置了
// 就是看setY()方法的返回值
// 在尋找當前皇后合適位置之前,我們先判斷是否求出一組解
// 如果,求出一組解,我們打印當前的解
if (list.size() == N) {
System.out.print("第" + all + "組解: ");
all++;
for (int num : list) {
System.out.print(num + " ");
}
System.out.println();
// 當前一組解,已經打印完畢
// 需要重新尋找新的解
// 我們這里讓棧頂的皇后,重新尋找位置
// 改變next_y的值就好了。
next_y = list.get(list.size() - 1) + 1;
// 記得彈出棧頂的皇后
list.remove(list.size() - 1);
// }
} else {
// *****************************************************
// 就是這里 我上午寫了一個比較臃腫的方法
// 我下午回來,試著重構,結果改了3個小時,才改對了!
// 簡直日了狗了,澡都沒趕上洗,午覺也沒睡成
// setY()方法中,尋找到合適的位置,就會自動添加到棧中
// 因此,這里只關注返回-1的情況
// 返回-1 ,表示需要回溯前一個皇后了
if (-1 == setY(N, list, next_y)) {
// 記錄下,先開始的位置
// 這個next_y 很重要的
// 如果需要在當前行 重新尋找合適的位置
// 就需要從當前位置的下一個位置開始遍歷尋找
// 因此next_y的值,為當前位置+1
next_y = list.get(list.size() - 1) + 1;
list.remove(list.size() - 1);
// 這里針對的是求出所有解的情況下,退出
// 當求出最后一組解的時候,再次將棧頂元素彈出,重新回溯的話
// 必然會回溯到最初的皇后,即第一個皇后,彈出第一個皇后的時候
// 棧為空。這里就需要作出判斷了
// 第一個皇后的下一個位置,是否超出了N的限制
if (list.size() == 0) {
// 判斷是右移還是退出
if (next_y < N) {
list.add(next_y);
next_y = 0;
} else {
break;
}
}
} else {
// 如果setY()沒有返回-1,說明找到了合適的位置
// 就需要為下一個皇后尋找合適的位置了
// 在新的一行開始尋找,必然需要從0開始尋找位置
next_y = 0;
// ****************************************************
}
}
}
}
}
測試方法
不了解Junit測試框架的同學,這里將 placeQueen(6);寫到main函數里面即可。
@Test
public void test() {
placeQueen(6);
}
運行結果
六皇后問題一共有四組解
得出的解,只是皇后從0開始的 “列坐標” ,它們的行坐標,是0到N-1順序排列;
比如 : 1 3 5 0 2 4,就是下面的情況
列 行
1 0
3 1
5 2
0 3
2 4
4 5
下面運算一下經典的八皇后,看看一個有多少組解
在圖中可以看出,一共有92組解。并且可以看到我們計算所有的解,只花費了0.026s。還是很快的。
八皇后問題,已經被我們解決了,用的就是回溯法,可能你
已經對回溯法,有了一定的了解,就是試探-剪枝-回溯。
希望你下次遇到什么新的算法策略。不要緊張,它們只是窮
舉,帶有策略的窮舉,策略還需要你來寫呢!
回溯法還有一個好玩的題,迷宮尋徑。過幾天,我在寫一個關于迷宮尋徑的博
客,題目有千千萬,但是思想是一樣的。希望大家和我,都能在解題過程中,對
回溯法有一個很好的掌握,下一次遇到問題的時候,能熟練使用回溯法解決它。
總結
以上是生活随笔為你收集整理的八皇后问题——列出所有的解,可推至N皇后的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 光标失去焦点事件 onblur
- 下一篇: 八哥鸟可以吃紫薯吗?