N皇后問題的位移解法

N皇后問題

以八皇后為例,在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,皇后可以在其所在位置的對應的行,列,對角線,反腳線上發動攻擊,請問一共有多少種擺法.

如果我們將這里的8拓展一下,變成N,那么這個問題就變成了N皇后問題.

八皇后

算法

下面是算法的高級偽碼描述,這里用一個N*N的矩陣來存儲棋盤:

  1. 算法開始, 清空棋盤,當前行設為第一行,當前列設為第一列;
  2. 在當前行,當前列的位置上判斷是否滿足條件(即保證經過這一點的行,對角線與反對角線上都沒有兩個皇后),若不滿足,跳到第4步;
  3. 在當前位置上滿足條件的情形:
    在當前位置放一個皇后,若當前行是最后一行,記錄一個解
    若當前行不是最后一行,當前行設為下一行, 當前列設為當前行的第一個待測位置
    若當前行是最后一行,當前列不是最后一列,當前列設為下一列
    若當前行是最后一行,當前列是最后一列,回溯,即清空當前行及以下各行的棋盤,然后,當前行設為上一行,當前列設為當前行的下一個待測位置
    以上返回到第2步;
  4. 在當前位置上不滿足條件的情形:
    若當前列不是最后一列,當前列設為下一列,返回到第2步;
    若當前列是最后一列了,回溯,即,若當前行已經是第一行了,算法退出,否則,清空當前行及以下各行的棋盤,然后,當前行設為上一行,當前列設為當前行的下一個待測位置,返回到第2步;

算法不算復雜,可是各種實現的速度卻千差萬別,不過解決N皇后問題主體思想就是回溯法,說白了,就是依靠一次一次地搜索(暴力法)來得到最終的結果.這篇文章的話,我想講一個用位移運算實現的N皇后求解程序,相對而言,這是一個非常高效的實現.

代碼實現

#include <iostream>
#include <stdint.h>
#include <string.h>
#include <assert.h>
using namespace std;


struct BackTracking
{
    const static int kMaxQueens = 20; // 最多支持20皇后

    const int N;
    int64_t count;
    // bitmasks, 1 means occupied, all 0s initially
    uint32_t columns[kMaxQueens]; // cloumns[row]的值對應的bit位表示在第row行中,有哪些位置已經被占用了
    uint32_t diagnoal[kMaxQueens]; // 對角線方向,哪些位置已經被占用了
    uint32_t antidiagnoal[kMaxQueens];  // 反對角線,哪些位置已經被占用了.

    BackTracking(int nqueens)
        : N(nqueens)
        , count(0)
    {
        assert(0 < N && N <= kMaxQueens);
        memset(columns, 0, sizeof columns);
        memset(diagnoal, 0, sizeof diagnoal);
        memset(antidiagnoal, 0, sizeof antidiagnoal);
    }

    int ctz(int n) // 對n對應的bit位從右邊開始數,第一個1之后0的個數
    {
        assert(n != 0);
        int count = 0;
        while (!(n & 1)) {
            n = n >> 1;
            count++;
        }
        return count;
    }

    void search(const int row)
    {
        uint32_t avail = columns[row] | diagnoal[row] | antidiagnoal[row]; // 找出有哪些位置可以放皇后
        avail = ~avail; // 得到這一行,哪些位置是可以用的

        while (avail) {
            // ctz(avail)用于找出avail對應的bit位右起第一個1后面有多少個0
            // 舉個例子,如果avail=6,對應的二進制數為1100,那么ctz(6)=2
            // avail=4,即0x1000,ctz(4)=3
            // 換句話說,就是找到第1個可以放置的位置的下標
            int i = ctz(avail);
            if (i >= N) {
                break;
            }
            if (row == N - 1) { // 已經是最后一行,得到一個解
                ++count;
            }
            else {
                const uint32_t mask = 1 << i; // 將要放置的位置對應的mask
                columns[row + 1] = columns[row] | mask; // 下一行的mask位置,這個位置已經被占用了,對應綠色的線
                diagnoal[row + 1] = (diagnoal[row] | mask) >> 1; // 對角線方向是朝右下方移動的,對應藍色的線
                antidiagnoal[row + 1] = (antidiagnoal[row] | mask) << 1; // 反對角線方向是朝左下方移動的,對應紅色的線
                search(row + 1); // 繼續往下搜索
            }
            // 運行到了這里的話,說明前面選擇的位置i不可行,所以要將第i位上的bit關閉
            // 我們來舉一個例子,假設avail是6,即1100,則6-1=5,即1011
            //   1 1 0 0
            // & 1 0 1 1
            //------------
            //   1 0 0 0
            // 可以看得到的是,恰好屏蔽了最后一個bit位,就這樣不斷選擇可以放入的位置
            avail &= avail - 1;  // turn off last bit
        }
    }
};

int64_t backtrackingsub(int N, int first_row, int second_row) // N指的是皇后的個數
{
    // first_row表示queen放在第一行放在哪一個位置上
    // second_row表示queen放在第二行的哪一個位置上
    const uint32_t m0 = 1 << first_row; // 得到位置的mask
    BackTracking bt(N);
    bt.columns[1] = m0; // 第1行的first_row這個格子已經不能放入
    bt.diagnoal[1] = m0 >> 1; // 對角線
    bt.antidiagnoal[1] = m0 << 1; // 反對角線上有一些位置也已經不能使用了

    if (second_row >= 0) // 如果第2個位置上也放置了值的話
    {
        const int row = 1;
        const uint32_t m1 = 1 << second_row;
        uint32_t avail = bt.columns[row] | bt.diagnoal[row] | bt.antidiagnoal[row];
        avail = ~avail; // avail所指帶的bit為1表示該位置可以放queen,否則不行
        if (avail & m1)
        {
            bt.columns[row + 1] = bt.columns[row] | m1; // 第2行
            bt.diagnoal[row + 1] = (bt.diagnoal[row] | m1) >> 1;
            bt.antidiagnoal[row + 1] = (bt.antidiagnoal[row] | m1) << 1;
            bt.search(row + 1);
            return bt.count;
        }
    }
    else
    {
        bt.search(1); // 否則的話,表示不限制第2行皇后的位置
        return bt.count;
    }
    return 0;
}


int main(int argc, char* argv[])
{
    int N = 13;
    int64_t count = 0;
    for (int i = 0; i < N; ++i) {
        count += backtrackingsub(N, i, -1); // 八皇后問題的解的個數
    }
    printf("%d\n", count);
    system("pause");
}

一個例子

關于上面的核心代碼search,我這里舉一個栗子.當然,行為不完全一致,但是讀了這個例子之后,你理解上面的代碼會簡單很多.

在開始之前,我們有這樣一個結構:

uint32_t columns[N];   // cloumns[row]的值對應的bit位表示在第row行中,有哪些位置已經被占用了
uint32_t diagnoal[N];  // 對角線方向,哪些位置已經被占用了
uint32_t antidiagnoal[N];  // 反對角線,哪些位置已經被占用了.

假設我們將第1個皇后放在第0行的下標為3的格子中,下圖標記了這個位置,請不要吐槽列的標記為什么不反過來,因為標記正著反著沒有多么大的關系,但是反著標記的話,它和我們的直覺是相符的.可以幫助我們更好地理解.

0
0

那么對于第1行來說,有這么一些位置是不能夠使用了的:

所以:

columns[1] = 0x0001000; // ==> 第1行第3格
diagnoal[1] = 0x0001000 >> 1; // ==> 第1行第2格
antidiagnoal[1] = 0x0001000 << 1; // ==> 第1行第4格

對應到下面的圖中,就是第1行的2, 3, 4格不能填寫了,我們可以這樣來取得能夠放入的位置:

avail = columns[1] | diagnoal[1] | antidiagnoal[1]; // 0x00011100
// 然后取反,然后avail對應的bit位為1所對應的位置就可以放入皇后啦.
avail = ~avail; // 0x11100011

這樣的話,在第1行填入我們隨意選擇一個位置吧,就把皇后放到第6格好了,該位置對應的mask = 0x01000000.

1
1

填入之后,我們繼續來限制第2行能夠填入的格子.

顯然對于上圖中綠色的線條,添加上第2行的皇后所在的位置后,后要繼續往下延伸:

columns[2] = columns[1] | mask; // 0x0001000 | 0x01000000 = 0x0101000; ==> 第6,3個格子不能填入

藍色的對角線元素填上皇后的新位置后,要向右下方延伸:

diagnoal[2] = (diagnoal[1] | mask) >> 1; // 0x01000100 >> 1 = 0x00100010; ==> 第5,1個格子不填

綠色的反對角線元素填上皇后新位置后繼續向左下方延伸:

antidiagnoal[2] = (antidiagnoal[1] | mask) << 1; // ==> 第7,5個格子不能填入

即:

2
2

現在我們在第2行選擇了第0個格子,所以mask=0x00000001.

所以在第3行,我們有了這么一些限制:

columns[3] = columns[2] | mask; //  ==> 第6,3, 0個格子不能填入
diagnoal[3] = (diagnoal[2] | mask) >> 1; //  ==> 第4,0個格子不填
antidiagnoal[3] = (antidiagnoal[2] | mask) << 1; // ==> 第7, 1格子不能填入
3
3

我們這一次選擇第3行的第2個格子放入皇后,那么接下來將會演變成下圖這樣:

4
4

在第4行的第5個格子放入皇后,我們可以接著做下去:

5
5

我們繼續在第5行的第7個格子中放入皇后,接下來如圖:

6
6

在第6行已經沒有格子允許我們放入了,這顯然是一個錯誤的擺法,所以要退回去,這就是所謂的回溯,接下來的步驟我就不一一演示了.

參考

N皇后問題的兩個最高效的算法

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 八皇后問題:在8*8的棋盤上放置8個皇后,保證任意兩個皇后之間不能相互攻擊。(即沒有兩個皇后是在同一行、同一類、或...
    五秋木閱讀 773評論 0 0
  • N皇后問題是一個經典的問題,在一個N*N的棋盤上放置N個皇后,每行一個并使其不能互相攻擊(同一行、同一列、同一...
    HUNYX閱讀 2,366評論 0 0
  • 一、實驗目的 學習使用 weka 中的常用分類器,完成數據分類任務。 二、實驗內容 了解 weka 中 explo...
    yigoh閱讀 8,638評論 5 4
  • 周瑜十分妒忌諸葛亮的才干。一天周瑜在商議軍事時提出讓諸葛亮趕制10萬枝箭,并說不要推卻。諸葛亮說,都督委托,...
    失心愛閱讀 1,043評論 2 2
  • 相信很多人都聽過周董的《蝸牛》:我要一步一步往上爬,等待陽光靜靜看著它的臉,小小的天 有大大的夢想,重重的殼裹著著...
    瀟湘溫柔夜閱讀 179評論 0 0