N皇后問題(各種優化)

chengzic1999發表於2020-12-02

0.問題引入

N皇后問題是一個經典的問題,在一個N*N的棋盤上放置N個皇后,每行一個並使其不能互相攻擊(同一行、同一列、同一斜線上的皇后都會自動攻擊),問有多少種擺法。

題目連結:https://www.luogu.org/problemnew/show/P1219

1、普通回溯

深搜+回溯,它是一種系統地(能找到全部的解)搜尋問題的解的方法。

基本思想是:從一條路往前走,能進則進,不能進則退回來,換一條路再試。

演算法思想:

1. 在第k(1≤k≤N)行選擇一個位置,判斷這個位置是否可以擺,可以擺就進入第 k+1 行,不可以就試本行的下一個位置是否可以擺;

2. 如果一直試到本行最後一個都不能擺,說明前面k-1行有位置選得不恰當,回到第 k-1 行,試 k-1 行的下一個位置。

3. 反覆執行1,2,到最後一行擺上棋子時,說明找到了一個解。

一個問題能用回溯法求解,它的解具有 N N N元組的形式,迷宮問題中就是到終點的 N N N 個座標,N皇后問題中就是 N N N 個皇后所處的(不同行)列號。

遞迴實現:

#include <iostream>
#include <math.h>
using namespace std;

int x[15];//x[k]:第k行上第x[k]個位置擺上了皇后
int N, cnt;

//row,col表示當前嘗試擺放皇后的行號與列好
bool check(int row, int col) {
    //回溯,不會受到後面行的影響
    for (int i = 1; i < row; i++) {
        if (x[i] == col)//列衝突
            return false;
        if (abs(row - i) == abs(col - x[i]))//對角線衝突
            return false;
    }
    return true;
}
void DFS(int k) {
    if (k == N + 1) {
        cnt++;
        if (cnt <= 3) {
            for (int i = 1; i <= N; i++) {
                cout << x[i] << " ";
            }
            cout << endl;
        }
        return;
    }
    for (int i = 1; i <= N; i++) {
       	//如果當前位置合法
        if (check(k, i)) {
            x[k] = i;
            DFS(k + 1);
            //本問題沒有明顯的回溯操作,因為下一個位置的賦值會將上一次賦值覆蓋掉
        }
    }
}

int main() {
    cin >> N;
    DFS(1);
    cout << cnt << endl;
    return 0;
}

非遞迴實現:

#include <iostream>
#include <math.h>
using namespace std;

int x[15];
int N, cnt;

bool check(int row, int col) {
    //回溯,不會受到後面行的影響
    for (int i = 1; i < row; i++) {
        if (x[i] == col)return false;
        if (abs(row - i) == abs(col - x[i]))return false;
    }
    return true;
}


void queen(){
    //i表示第幾冊,j表示在第i層搜尋位置
    int i = 1, j = 1;
    while (i <= N){
        while (j <= N){
            //如果當前位置合法
            if (check(i, j)) {
                //把x[i]暫時賦值成j
                x[i] = j;
                j = 1;
                break;
            }
            else
                j++;
        }
        //第i行沒有找到可以放置皇后的位置
        if (x[i] == 0){
            //如果回溯到了第0行,說明搜尋結束
            if (i == 1)
                break;
            //回溯
            else{
                i--;
                j = x[i] + 1;//j為上一行的皇后位置+1
                x[i] = 0;//上一行清零
                continue;
            }
        }
        //如果找到了第N層,輸出出來
        if (i == N){
            cnt++;
            if (cnt <= 3) {
                for (int i = 1; i <= N; i++) {
                    cout << x[i] << " ";
                }
                cout << endl;
            }
            j = x[i] + 1; 
            x[i] = 0;     
            continue;
        }
        i++;              
    }
}
int main() {
    cin >> N;
    //DFS(1);
    queen();
    cout << cnt << endl;
    return 0;
}

均無法通過最後一個測試點

在這裡插入圖片描述

2、減半優化

其實不難想象,因為棋盤的對稱,每個結果總有另一個與之對稱。只用回溯一半 ,效率能提升50%左右。

方法就是第一行只搜尋該行的前一半的位置即可(只搜一半的行也是可以的,只是這樣更方便)。但是對於奇數的N,計算出來的結果會將第一行中間位置的解算了兩遍。所以要單獨處理一下。

#include <iostream>
#include <vector>
#include <math.h>
using namespace std;

int x[15];
vector<int> v[3];//處理解數量不足3個的情況
int N, cnt;
int flag, oddCnt;

bool check(int row, int col) {
    //回溯,不會受到後面行的影響
    for (int i = 1; i < row; i++) {
        if (x[i] == col)return false;
        if (abs(row - i) == abs(col - x[i]))return false;
    }
    return true;
}
void DFS(int k) {
    if (k == N + 1) {
        if (flag&&x[1] == (N + 1) / 2) {
            oddCnt++;
            if (oddCnt % 2 == 0)cnt++;
        }
        else
        cnt++;
        if (cnt <= 3) {
            for (int i = 1; i <= N; i++) {
                cout << x[i] << " ";
                v[cnt - 1].push_back(x[i]);
            }
            cout << endl;
        }
        return;
    }
    int len = (k == 1) ? (N + flag) / 2 : N;
    for (int i = 1; i <= len; i++) {
        if (check(k, i)) {
            x[k] = i;
            DFS(k + 1);
        }
    }
}

int main() {
    cin >> N;
    if (N & 1)flag = 1;
    DFS(1);
    for (int i = cnt, j = cnt - 1; i < 3 && j >= 0; i++, j--) {
            for (int k = N - 1; k >= 0; k--) {
                cout << v[j][k] << " ";
            }
            cout << endl;
        }
    
    cout << cnt*2 << endl;
    return 0;
}

勉強跑過了

在這裡插入圖片描述

3、優化判斷

以本圖為例:

每條橙色對角線的行列之差是相同的。

每條藍色對角線的行列之和是相同的。

img

用兩個bool陣列用來記錄藍色、橙色斜線是否已經被佔據。考慮到行列之差可能為負數,棋盤座標 [x,y] 對應下標 [ x - y + n ]。

這裡下標不能用 a b s ( x − y ) abs(x-y) abs(xy) 的原因是主對角線邊上的兩條對角線,他們的 a b s ( x − y ) abs(x-y) abs(xy) 都是 1 1 1

再用一個陣列記錄第 i 列是否有元素。

#include <iostream>
using namespace std;

int N, cnt, a[15];
//正斜線、反斜線、行
bool x1[31], x2[31], y[15];

void DFS(int k) {
    if (k == N + 1) {
        cnt++;
        if (cnt <= 3) {
            for (int i = 1; i <= N; i++) {
                cout << a[i] << " ";
            }
            cout << endl;
        }
        return;
    }
    for (int i = 1; i <= N; i++) {
        //這裡x2下標不能用abs(k-i),那樣是不對的,因為主對角線邊上的兩條對角線,他們的abs(k-i)都是1
        if (!x1[i + k] && !x2[k - i + N] && !y[i]) {
            a[k] = i;
            x1[i + k] = true;
            x2[k - i + N] = true;
            y[i] = true;
            DFS(k + 1);
            x1[i + k] = false;
            x2[k - i + N] = false;
            y[i] = false;
        }
    }
}


int main() {
    cin >> N;
    DFS(1);
    cout << cnt << endl;
    return 0;
}

當N較大時,演算法會耗費大量的次數在無用的回溯上,時間還是沒有顯著提高。

在這裡插入圖片描述

4、位運算優化

警告:以下程式碼可能引起不適,建議60歲以下使用者在家長陪同下閱讀。

位運算是計算機最快的操作,我們可以用數的二進位制位表示各縱列、對角線是否可以放置皇后。

參考:https://blog.csdn.net/Hackbuteer1/article/details/6657109

#include <iostream>
#include <queue>
using namespace std;

int n, limit, cnt;
int x[15], k = 1;
//行,左對角線,右對角線
void DFS(int row,int left,int right) {
    if (row != limit) {
        //row|left|right表示這一行的所有禁止位置,取反再和limit按位與,得到的是該行可以放的幾個位置        
        int pos = limit & ~(row | left | right);
        //每一個可以擺的位置,都要做一次
        while (pos) {
            //找到的可以放皇后的位置(pos二進位制最右邊的一個1)
            int p = pos & -pos;// pos & (~pos+1);
            //把這一位置0,表示不為空
            pos &= pos - 1;//pos=pos-p;
            //把p所在row,left,right的位都置1。
            //(left | p)<< 1 是因為這一行由左對角線造成的禁止位在下一行要右移一下;right同理
            DFS(row | p, (left | p) << 1, (right | p) >> 1);
        }
    }
    else {
        cnt++;
    }
}

int main() {
    cin >> n;
    limit = (1 << n) - 1;
    DFS(0, 0, 0);
    cout << cnt << endl;
    return 0;
}
#include <iostream>
#include <queue>
using namespace std;

int n, limit, cnt;
int x[15], k = 1;
//行,左對角線,右對角線
void DFS(int row,int left,int right) {
    if (row != limit) {
        int pos = limit & ~(row | left | right);
        while (pos) {
            //找到的可以放皇后的位置
            int p = pos & -pos;// pos & (~pos+1);
            pos &= pos - 1;
            if (cnt < 3) {
                int t = p, num = 1;
                while (t != 1) {
                    num++;
                    t >>= 1;
                }
                x[k++] = num;
            }
            DFS(row | p, (left | p) << 1, (right | p) >> 1);
            if (cnt < 3) k--;
        }
    }
    else {
        if (cnt < 3) {
            for (int i = 1; i <= n; i++) {
                cout << x[i] << " ";
            }
            cout << endl;
        }
        cnt++;
    }
}

int main() {
    cin >> n;
    limit = (1 << n) - 1;
    DFS(0, 0, 0);
    cout << cnt << endl;
    return 0;
}

效率提升顯著

在這裡插入圖片描述

相關文章