• 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

    果然名不虚传~

  • 相关阅读:
    window.location.Reload()和window.location.href 区别
    PHP substr(),mb_substr()及mb_strcut的区别和用法
    jstree节点展开设置
    关于Jquery中ajax方法data参数用法
    HTML相对路径(Relative Path)和绝对路径(Absolute Path)
    Win32基础编程了解窗口类
    Visual C++ ActiveX 开发指南:第一章 什么是ActiveX
    分粥
    蛙蛙推荐:ASP实现自定义标签模板
    蛙蛙请教:如何利用委托实现多个方法同时调用.
  • 原文地址:https://www.cnblogs.com/czc1999/p/10534475.html
Copyright © 2020-2023  润新知