八皇后問題——回溯法


八皇后問題

眾所周知國際象棋是一種經典而著名的二人對弈的棋類游戲,相信這個不必我多介紹。棋子共有國王、皇后、戰車、主教、騎士、禁衛軍這七種,不僅出現於國際象棋的棋盤上,在其他領域的作品中也會用這些棋子做點文章,例如《逆轉檢事2》的邏輯象棋系統(御劍檢察官的腦洞)。

不過我仍然不是來向你推薦游戲的,而是想要介紹從國際象棋中引申的一個問題。皇后國際象棋棋局中實力最強的一種棋子,可橫直斜走,且格數不限,吃子與走法相同。也就是說,想要不被皇后棋子吃,就必須不能和該棋子在同一行、列、斜方向上。現在讓我們來看看八皇后問題。

八皇后問題,一個古老而著名的問題,是回溯算法的典型案例。該問題由國際西洋棋棋手馬克斯·貝瑟爾於 1848 年提出:在 8×8 格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。高斯認為有 76 種方案。1854 年在柏林的象棋雜志上不同的作者發表了 40 種不同的解,后來有人用圖論的方法解出 92 種結果。
-- 百度百科

八皇后問題如果用窮舉法解決,就需要驗證 88 =16777216 種情況。每一列放一個皇后,可以放在第 1 ~ 8 行,窮舉的時候從所有皇后都放在第 1 行的方案開始,檢驗皇后之間是否屬於同一行、同一列、同一斜向,如果屬於,就把其中一個皇后皇后挪一格,驗證下一個方案……這種方法的時間復雜度無疑是很龐大的。該方法時間復雜度 O(nn)。

模擬實現

八皇后問題可以推廣至 n 皇后問題,接下來我們先用 4 × 4 的棋盤來模擬一遍棋子的安放。我們先把目光聚焦在第一行。首先在第一行第一列,也就是 (1,1) 位置放置棋子。

為了更加直觀,我把不允許放置棋子的位置塗上紅色,需要注意的是,計算機可不能向我們這樣用肉眼就能看出哪里不能放,需要進行遍歷。
接下來我就需要到第二行找放置棋子的位置,我先放置於 (2,3)。

接下來我就需要到第三行找放置棋子的位置,我先放置於 (3,2)。

接下來我就需要到第四行找放置棋子的位置,不過這個時候已經沒有位置可放了,也就是說我們找到了一個不可行的方式,這個時候就需要退回上一步。

接下來我就需要到第三行的 (3,2) 之后找放置棋子的位置,同樣沒位置可放了,退回上一步。

接下來我就需要到第三行的 (2,3) 之后找放置棋子的位置,放置於 (2,4)。

接下來操作同上,我們發現換了路線之后仍然不可行。

這個時候就說明了,在第一行的 (1,1) 位置放置第一個皇后的話,將不會有符合要求的放法,所以回溯到一開始,在 (1,2) 位置放置棋子。

重復上述操作。

這時候我們發現了,我們找到了一個可行的放置方式。

不過問題還沒有結束,我們繼續進行模擬。由於在第一行 (1,2) 位置放置棋子只有這一種情況,因此進行回溯,在 (1,3) 位置放置棋子。

我想你可能已經發現了,這種情況和在 (1,2) 放置棋子的情況是鏡像對稱的,我們還能夠得到一種解法。

由於在第一行 (1,3) 位置放置棋子只有這一種情況,因此進行回溯,在 (1,4) 位置放置棋子。

這種情況和在 (1,1) 放置棋子的情況是鏡像對稱的,沒有增加解法。由於第一行已經沒有更多的位置了,模擬結束,得到兩組解。

思路解析

我們觀察下皇后問題的一個解法,我們發現由於需要讓皇后棋子之間不能互吃,因此在每一行、每一列中只能出現一個皇后。再來分析斜向的規律,當兩個棋子的坐標關系滿足行數之差的絕對值等於列數之差的絕對值時,說明兩個棋子在斜向上是屬於同一方向。在組織數據的時候,由於我們需要知道每一種解法的具體內容,因此不能用 STL 庫的 stack 容器來實現,因為這樣數據不會被保存,我的解法是用 STL 庫的 vector 容器來模擬棧結構,另外定義一個 top 游標來指向棧頂位置。由於每一行有且僅有一個皇后棋子,因此我們可以用容器的索引描述棋盤行數,用索引對應的值來描述列數,由此來判斷是否同一列、同一斜向就會變得方便許多。
在這里我們可以使用回溯法來解決問題,回溯算法解決問題的思想是有沖突解決沖突,沒有沖突往前走,無路可走往回退,走到最后是答案。我們回憶一下棧結構實現的迷宮尋路算法,我們發現這是有異曲同工之妙的,當我們找不到路徑時,也是通過回溯到之前的路徑重新搜索的。當然也可以用遞歸來解決這個問題,不過遞歸也是棧結構的一種應用。具體算法思路見偽代碼,該方法時間復雜度 O(n2)。

偽代碼

代碼實現

#include<iostream>
#include<vector>
using namespace std;
bool judgePlacement(int top, vector<int>& queens_stack);

int main()
{
    int num;
    cin >> num;
    vector<int> queens_stack(num + 1);    //構造函數初始化
    int top = 1;    //棧頂指針

    while (top)
    {
	queens_stack[top]++;    //棧頂表示的位置橫向移動到下一行
	while (queens_stack[top] <= num && judgePlacement(top, queens_stack) == false)
	{
	    queens_stack[top]++;    //在 top 同一行的后面搜索可放置的列
	}
	if (queens_stack[top] <= num)
	{
	    if (num == top)    //棋盤搜索完畢,得到一組解
	    {
		for (int i = 1; i <= num; i++)   //輸出棋盤
		{
		    for (int j = 1; j <= num; j++)
		    {
			if (j == queens_stack[i])
			    cout << "Q ";
			else
			    cout << "* ";
		    }
		    cout << endl;
	        }
	        cout << endl;
	    }
            else    //棋盤沒有搜索到最后一行,下一行入棧
	        queens_stack[++top] = 0;
	}
	else    //在同一行中找不到任何一列可以放置
	    top--;    //退棧,回溯到上一行搜索可放置的列
    }
    return 0;
}

bool judgePlacement(int top, vector<int>& queens_stack)
{
    for (int i = 1; i < top; i++)    //判斷當前位置是否與之前的每一行中的棋子處於同列或同斜向
    {
	if ((queens_stack[i] == queens_stack[top]) 
                            || (fabs(queens_stack[i] - queens_stack[top]) == fabs(i - top)))
	    return false;
    }
    return true;    //說明該位置可放置
}

運行效果


參考資料

八皇后問題
漫畫:什么是八皇后問題?
數據結構應用案例——棧結構用於8皇后問題的回溯求解
經典算法之八皇后問題


免責聲明!

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



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