N皇后问题 各种优化

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$元组$(x_1,x_2,...,x_n)$表示问题的解,其中$x_i$表示第$i$行的皇后所处的列号。

核心代码:

//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++;
        return;
    }
    for (int i = 1; i <= N; i++) {
        if (check(k, i)) {
            x[k] = i;//标注第k行上第i个位置摆上了皇后
            DFS(k + 1);
        }
    }
}

1.1 递归实现:

N=11,12的时就顶不住了,嗝屁了。

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

1.2 非递归实现:

算法优化一般不从这里考虑,因为非递归虽然是会快一点,但也只是那么一点而已,数据量小几乎没有区别,两个都跑不过去。

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

 

2、减半优化

其实仔细看解就不难发现,每个结果总有另一个与之对称。我们可以利用棋盘的对称, 只用回溯一半 。效率能提升50%。

对于第一层,只下该行的前一半的位置即可。但是对于奇数的N,计算出来的结果会将第一行下在中间位置的解算了两遍。所以要单独处理一下。

效率提升不到50%(奇数的情况),并不算多,题目的测试数据只到13,勉强跑过了,但优化还没结束。

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

int x[15];
vector<int> v[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;
}
View Code

 

3、优化判断

以本图为例:

每条橙色对角线的行列之差是相同的。

每条蓝色对角线的行列之和是相同的。

用两个bool数组用来记录行列之和为 i 的正斜线、行列之差为 i 的反斜线是否已经被占据。考虑到行列之差可能为负数,棋盘坐标 [x,y] 对应下标 [ x - y + n ]。

再用一个数组记录第 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,那样是不对的
        if (!x1[i + k] && !x2[k - i + N] && !y[i]) {
            a[k] = i;
            x1[i + k] = 1;
            x2[k - i + N] = 1;
            y[i] = 1;
            DFS(k + 1);
            x1[i + k] = 0;
            x2[k - i + N] = 0;
            y[i] = 0;
        }
    }
}


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

当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;
}
View Code

果然名不虚传~

 

posted @ 2019-03-21 21:45  czc1999  阅读(262)  评论(0编辑  收藏  举报