算法設計與分析——n後問題(回溯法+位運算)

1、問題描述

在n×n格的國際象棋上擺放n個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。html

 

 2、算法設計

 

 

 解n後問題的回溯算法描述以下:ios

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
int n;
long long int sum;
int x[11];
int Check(int row, int col)
{
    for(int i = 1; i < row; i++)
    {
        if(col == x[i] || abs(row - i) ==abs(col - x[i])) //在同一列或者在同一斜線。必定不在同一行
            return 0;
    }
    return 1;
}

void backtrack(int k)
{
    if(k>n)     //求出一種解, sum+1
    {
        sum++;
        return;
    }
    for(int i=1; i<=n; i++)//n叉樹
    {
        if(Check(k, i))     //剪枝,檢查是否知足條件
        {
            x[k]=i;      //記錄第k皇后在第i列
            backtrack(k+1);   //遞歸查找
        }
    }

}
int main()
{
    while(scanf("%d",&n)!=EOF)
    {
        if(n==0)
        {
            break;
        }
        for(int i=0; i<n; i++)
        {
            x[i]=0;
        }
        sum=0;
        backtrack(1);
        printf("%lld\n",sum);
    }
    return 0;
}

3、位運算優化

上面的程序我在求16皇后的時候大概跑了近乎200s,咱們能夠想象到每次搜索第k行的狀態的時候,都是從第1列開始枚舉每一列,這樣是很低效的,浪費了不少時間,咱們須要提升枚舉的命中率甚至每一次的嘗試都是正確的,都是可行解。算法

那麼該怎麼作?佈局

其實n皇后的搜索規模並非很大,在目前的需求中,最多不過20位,咱們可使用二進制來表示一個集合,而一旦使用二進制時,集合的交併補運算就能夠直接使用位運算來實現了,咱們知道位運算在計算機中是至關快的(使用指令少)。安利一篇我以前作過的位運算的實驗http://www.javashuo.com/article/p-pxzjyhks-dv.html優化

兩個數字與運算就是求交集,或運算就是求並集,取反就是求集合的補集。spa

咱們先來看程序代碼:設計

void test(int row, int ld, int rd)  
{  
    int pos, p;  
    if ( row != upperlim )  
    {  
        pos = upperlim & (~(row | ld | rd ));  
        while ( pos )  
        {  
            p = pos & (~pos + 1);  
            pos = pos - p;  
            test(row | p, (ld | p) << 1, (rd | p) >> 1);  
        }  
    }  
    else  
        ++Ans;  
}
     

初始化:3d

upperlim =  (1 << n)-1; Ans = 0;

upperlime =(1 << n)-1 就生成了n個1組成的二進制數。code

程序從上到下搜索。htm

這樣咱們使用三個參數row、ld和rd,分別表示在縱列和兩個對角線方向的限制條件下這一行的哪些地方不能放。位於該行上的不能放置的位置就用row、ld和rd中的1來表示。把它們三個並起來,獲得該行全部的禁位,取反後就獲得全部能夠放的位置(用pos來表示)

這裏須要注意一點:

對應row、ld和rd來講1表示的是不能放置皇后的佔用位置,但對於pos來講1表明能夠放置皇后的位置!

 p = pos & (~pos + 1)其結果是取出最右邊的那個1。

由於取反之後恰好全部數都是相反,再加 1 ,就是改變最低位,若是低位的幾個數都是1,加的這個 1 就會進上去,一直進到 0 ,在作與運算就和原數對應的 1 重合了。舉例能夠說明:

       原數 0 0 0 0 1 0 0 0    原數 0 1 0 1 0 0 1 1

       取反 1 1 1 1 0 1 1 1    取反 1 0 1 0 1 1 0 0

      加1   1 1 1 1 1 0 0 0     加1  1 0 1 0 1 1 0 1

與運算    0 0 0 0 1 0 0 0    and  0 0 0 0 0 0 0 1

 

從集合的角度來看p是位置集合pos上的一位置,將皇后置於位置p,位置集合就要減小一個位置,因此須要:

pos = pos - p

那這個while咱們也就明白了,須要把位置集合全都用完放置皇后嘛!

最後咱們要注意遞歸調用時三個參數的變化,每一個參數都加上了一個佔位,但兩個對角線方向的佔位對下一行的影響須要平移一位。最後,若是遞歸到某個時候發現row=upperlim了,說明n個皇后全放進去了,找到的解的個數加1。

 

 這裏拿兩個例子來講明,對於第一張圖的例子。

在已經安置好3個皇后的狀況下,對於第4個皇后

row = 101010 棕色線表明縱列上不能放置皇后的佔位

 ld  = 100100  藍色線表明左對角線列上不能放置皇后的佔位

 rd =  000111  色線表明右對角線列上不能放置皇后的佔位

 對角線是45度傾斜的,這樣兩個對角線方向的佔位要影響下一行對應位置的下一位也就很好理解了,這偏偏可使用位運算的左移和右移來實現。

(ld | p)<< 1 是由於由ld形成的佔位在下一行要右移一下;

(rd | p)>> 1 是由於由rd形成的佔位在下一行要左移一下。 

固然 ld rd row 還要和upperlime 與運算 一下,這樣作的結果就是從最低位數起取n個數爲有效位置,緣由是在上一次的運算中ld發生了右移,若是不and的話,就會誤把n之外的位置當作有效位。

 

#include<cstdio>
#include<algorithm>
#define ll long long int
using namespace std;

// sum用來記錄皇后放置成功的不一樣佈局數;upperlim用來標記全部列都已經放置好了皇后。
ll sum;
ll upperlim = 1;

// 試探算法從最右邊的列開始。
void test(ll row, ll ld, ll rd)
{
    if (row != upperlim)
    {
        // row,ld,rd進行「或」運算,求得全部能夠放置皇后的列,對應位爲0,
        // 而後再取反後「與」上全1的數,來求得當前全部能夠放置皇后的位置,對應列改成1
        // 也就是求取當前哪些列能夠放置皇后
        ll pos = upperlim & ~(row | ld | rd);
        while (pos)    // 0 -- 皇后沒有地方可放,回溯
        {
            // 拷貝pos最右邊爲1的bit,其他bit置0
            // 也就是取得能夠放皇后的最右邊的列
            ll p = pos&-pos;

            // 將pos最右邊爲1的bit清零
            // 也就是爲獲取下一次的最右可用列使用作準備,
            // 程序未來會回溯到這個位置繼續試探
            pos -= p;
            // row + p,將當前列置1,表示記錄此次皇后放置的列。
            // (ld + p) << 1,標記當前皇后左邊相鄰的列不容許下一個皇后放置。
            // (ld + p) >> 1,標記當前皇后右邊相鄰的列不容許下一個皇后放置。
            // 此處的移位操做其實是記錄對角線上的限制,只是由於問題都化歸
            // 到一行網格上來解決,因此表示爲列的限制就能夠了。顯然,隨着移位
            // 在每次選擇列以前進行,原來N×N網格中某個已放置的皇后針對其對角線
            // 上產生的限制都被記錄下來了
            test(row + p, (ld + p) << 1, (rd + p) >> 1);
        }
    }
    else
    {
        // row的全部位都爲1,即找到了一個成功的佈局,回溯
        sum++;
    }
}

int main()
{
    int n;
    while(scanf("%d",&n)!=EOF)
    {
        if(n==0)
        {
            break;
        }
        sum = 0;
        upperlim = (1 << n) - 1;
        test(0,0,0);
        printf("%lld\n",sum);
    }
    return 0;
}
相關文章
相關標籤/搜索