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


一、問題描述

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

 

 二、算法設計

 

 

 解n后問題的回溯算法描述如下:

#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;
}

三、位運算優化

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

那么該怎么做?

其實n皇后的搜索規模並不是很大,在目前的需求中,最多不過20位,我們可以使用二進制來表示一個集合,而一旦使用二進制時,集合的交並補運算就可以直接使用位運算來實現了,我們知道位運算在計算機中是相當快的(使用指令少)。安利一篇我之前做過的位運算的實驗https://www.cnblogs.com/wkfvawl/p/10034576.html

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

我們先來看程序代碼:

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;  
}
     

初始化:

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

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

程序從上到下搜索。

這樣我們使用三個參數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;
}

 

 

     

 


免責聲明!

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



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