zoukankan      html  css  js  c++  java
  • N皇后 八皇后 位运算解法

    位运算 八皇后 N皇后

    问题描述

    什么是八皇后?

    题目链接

    N皇后

    解法

    ​ 直接在N*N的棋盘上进行深搜,试探着下棋,也就是回溯法。

    ​ 对于一个皇后来说,我们需要判断她的 八个方向 ,即 主对角线,副对角线,行,列

    ​ 1. 确定状态

    ​ 第一眼的感觉是要用 四个数组来储存情况,但实际上只需要三个 ,把 行 排除在外

    ​ 因为每次搜索的时候,都自动按搜索,也就是变量 row ,每改变一次,代表了不同的行

    ​ 接下来,分析主对角线 ,对于N*N的矩阵来说,主对角线上的值满足 行和列的差为定值

    ​ 即,做减法得到的下标是唯一的,因为数组没有 负值的索引 所以要 再差值的基础上,加n

    row - col + n 得到唯一的下标,

    副对角线 ,行 列 之和为定值 row + col

    直接利用下标 储存就行

    ​ 2.试探着下棋 (回溯)

    i 代表列 ,每次搜索按列递进,这样保证了每一列,只可能放一个棋子

    ​ lvis 代表 主对角线的状态 ,rvis 代表副对角线的状态 ,col 代表 列 的状态

    ​ if 那段表示 ,如果 主对角线,副对角线,列 上都没有棋子落下

    ​ 之后 改变状态 ,将 主对角线,副对角线,列 都设置为 1

    ​ dfs(row + 1) 按行递进

    ​ dfs(row + 1)执行完之后,退回到之前的状态:

    ​ lvis,rvis,col 都置为 0 ,在这一行中找下一个可以放的位置

    for(int i=1;i<=n;++i){
            if(!lvis[i-row+8]&&!rvis[i+row]&&!col[i]){
                lvis[i-row+8] = 1;
                rvis[i+row] = 1;
                col[i] = 1;
                ans[row] = i;
                dfs(row+1);
                lvis[i-row+8] = 0;
                rvis[i+row] = 0;
                col[i] = 0;
                }
            }
    

    1.朴素解法(回溯)

    #include <bits/stdc++.h>
    using namespace std;
    const int N =1e2+10;
    bool lvis[N],rvis[N],col[N];
    int n,tot,ans[N];
    void dfs(int row){
        if(row == n+1){
             tot++;
             for(int i=1;i<=8;++i){
                 cout<<ans[i]<<" ";
             }
             cout<<endl;
        }
        else {
            for(int i=1;i<=n;++i){
            if(!lvis[i-row+8]&&!rvis[i+row]&&!col[i]){
                lvis[i-row+8] = 1;
                rvis[i+row] = 1;
                col[i] = 1;
                ans[row] = i;//保存棋子的位置, 第row行,第i列;
                dfs(row+1);
                lvis[i-row+8] = 0;
                rvis[i+row] = 0;
                col[i] = 0;
                }
            }
        }
    }
    int main(){
        //cin>>n;
        n = 8;
        dfs(1);
        cout<<tot<<endl;
        return 0;
    }
    

    2.位运算解法(状态储存方式的不同)

    ​ 根本思路是一样的,拿位运算解,就是把原先用数组储存状态的方式,改变为用 整数 int

    ​ 因为 一个int 占 4个字节 4*8 个bit,所以相当于 含有 32个空间的数组

    ​ 用位运算 大大降低了占用的空间支出

    基本位运算操作

    ​ 对于改变状态来说,无非就两种 ,1 和 0

    ​ 对于特定位置 1 :

    	a |= (1<<i);
    

    ​ 把 1 左移 i 位 ,得到一个32位整数

    ​ 假如 i 为 2

    ​ (省略24位前置 0) 0000 0100

    ​ 假如 i 为 1

    ​ (省略24位前置 0) 0000 0010

    ​ 假如 i 为 7

    ​ (省略24位前置 0) 1000 0000

    ​ 将其与 a 进行 或运算 ,原先 a上的第 i 位, 将被置为 1

    ​ 对于特定位置 0:

    	a &= ~(1<<i);
    

    ​ ~运算 ,把各位上的值都取反

    ​ 假如 i 为 2

    ​ (省略24位前置 1) 1111 1011

    ​ 假如 i 为 1

    ​ (省略24位前置 1) 1111 1101

    ​ 假如 i 为 7

    ​ (省略24位前置 1) 0111 1111

    ​ 将其与 a 进行 与运算 ,原先 a 上的第 i 位,将被置为 0

    判断状态

    ​ x 的第 i 位 是否为 1

        bool getbit(int x,int i){
            return !((x>>i) & 1);
        }
    

    ​ 把 x 右移 i 位(这样当前 x 的第0 位,就是原先 x 的第 i 位), 和 1进行与运算

    完整代码

    #include <bits/stdc++.h>
    using namespace std;
    const int N =1e2+10;
    int lvis,rvis,col;
    int n,tot,ans[N];
    bool getbit(int x,int i){
        return !((x>>i) & 1);
    }
    void dfs(int row){
        if(row == n+1){
             tot++;
             for(int i=1;i<=8;++i){
                 cout<<ans[i]<<" ";
             }
             cout<<endl;
        }
        else {
            for(int i=1;i<=n;++i){
            if(getbit(lvis,i-row+8)&&getbit(rvis,i+row)&&getbit(col,i)){
                lvis |= (1<<(i-row+8));
                rvis |= (1<<(i+row));
                col |= (1<<i);
                ans[row] = i;
                dfs(row+1);
                lvis &= ~(1<<(i-row+8));
                rvis &= ~(1<<(i+row));
                col &= ~(1<<i);
                }
            }
        }
    }
    int main(){
        cin>>n;
        //n = 8;
        dfs(1);
        cout<<tot<<endl;
        return 0;
    }
    

    (C++ 自带 bitset ,可以不用这么麻烦)

    只要把 这行

    bool lvis[N],rvis[N],col[N];
    

    替换成这行

    bitset<N> lvis,rvis,col;
    
    #include <bits/stdc++.h>
    using namespace std;
    const int N =1e2+10;
    bitset<N> lvis,rvis,col;
    int n,tot,ans[N];
    void dfs(int row){
        if(row == n+1){
             tot++;
             for(int i=1;i<=8;++i){
                 cout<<ans[i]<<" ";
             }
             cout<<endl;
        }
        else {
            for(int i=1;i<=n;++i){
            if(!lvis[i-row+8]&&!rvis[i+row]&&!col[i]){
                lvis[i-row+8] = 1;
                rvis[i+row] = 1;
                col[i] = 1;
                ans[row] = i;
                dfs(row+1);
                lvis[i-row+8] = 0;
                rvis[i+row] = 0;
                col[i] = 0;
                }
            }
        }
    }
    int main(){
        //cin>>n;
        n = 8;
        dfs(1);
        cout<<tot<<endl;
        return 0;
    }
    

    3. 最终版

    位运算八皇后1

    位运算八皇后2

    既然状态可以用bit来储存,那么搜索的过程,是否可以通过位运算来简化呢?

    可以的。

    思路

    ​ 我们把最终要达到的目标设置为命名为 goal ,

    ​ goal 在二进制位中,表示为 1111 1111 (八个一),代表我们要放八个皇后

    ​ 对于 row ,lvis ,rvis 要把他们拆成 一维来看待,

    ​ row表示的是 这一行中已经被标记 (row的二进制为 1的个数) 的棋子,看成一维

    ​ 例如 0100 1000 ,代表这一行中 第3位 和 第6位 不能放棋子,(也就是二进制为1的列有棋子)

    ​ lvis ,rvis 也看成一维 (与上面两种解法的状态表示完全不同

    ​ 代表的是当前这一行中 , 由其他行棋子所产生的 标记 ,

    ​ 例如 第一行有个位置被标记

    ​ 0 0 1 0 0 0 0 0

    ​ 0 1 0 1 0 0 0 0

    ​ 那么,第一行,lvis,rvis代表的是 由第一行的位置 5 所产生的标记 在第二行中的标记

    	goal & (~ (row|lvis|rvis));
    

    ​ 所以,safe 得到的就是可行解的个数,全为 1 的goal & 可放位置 (如果不进行 & 运算,就会溢出)

    ​ 这个可放位置就是 不可放位置的取反

    ​ 因为 int 有 32个bit,这样就safe的值就被限制在了 8个bit 之间

    	while(safe)
    

    ​ 相当于 BFS 中的解的队列 safe中的 1 一直出队,寻找下一个位置,直到safe为空

    	next = safe & (~safe + 1);//这句 等同于 
    //	next = safe & -safe;
    

    ​ 我们要拿出一个解,即最右边的一个解的位置,放棋子

    ​ 例如 0100 1000 要取出这个1,该怎么办呢?

    ​ ~0100 1000 = 1011 0111

    ​ 1011 0111 + 1 = 1011 1000

    ​ 再把 1011 1000

    ​ & 0100 1000

    ​ = 0000 1000 也就是最后一位 1

    ​ 写的时候有两种方式,其实都代表的是一个意思

    ​ 因为 在二进制当中 ,把一个整数 转变为 负数 ,相当于 各位取反 再 加 1

    	safe ^= next;// 删除最右边的一个解,等同于
    //  safe -= next;  直接删除
    

    ​ ^运算 ,相同为假,不同为真

    ​ 因为 next 代表 safe 的最右边的 1 ,所以 那一位就会被 去除

    ​ 第二种写法 是因为 最右边为 1 ,假如他的位置是 i ,那么可以 直接减去 一个 2的幂次方的数字 (2^i)

    	dfs((lvis|next)<<1,(rvis|next)>>1,row|next);
    

    ​ 当前 livs 对下一行 livs的影响 是把所有的标记左移一位得到的

    ​ 例如 第一行有个位置被标记

    ​ 0 0 1 0 0 0 0 0

    ​ 0 1 0 1 0 0 0 0

    ​ 那么 lvis 左移 一位, 表示 第二行中的 第六个位置被标记

    ​ rvis 右移 一位, 表示 第三行中的 第四个位置被标记

    ​ lvis|next 标记占位 ,同理 rvis|next , row|next

    	row != goal
    

    ​ 如果 row 等于 goal ,说明 一行中的所有位置都被标记过,也就是放满了棋盘

    ​ tot++ ,答案数

    #include <bits/stdc++.h>
    using namespace std;
    int n,tot,goal = (1<<8) - 1;
    void dfs(int lvis,int rvis,int row){
        int safe,next;
        if(row != goal){
            safe = goal & (~ (row|lvis|rvis));//得到可行解
            while(safe){//
                next = safe & (~safe + 1);// 得到 最右边的一个位置,safe & (-safe)
                safe ^= next;// 删除最右边的一个解
                bfs((lvis|next)<<1,(rvis|next)>>1,row|next);
            }
        }
        else
            tot++;
    }
    int main(){
        //cin>>n;
        n = 8;
        dfs(0,0,0);
        cout<<tot<<endl;
        return 0;
    }
    

    建议结合三篇博文来看,再用笔、纸、代码 自行验证

  • 相关阅读:
    Access的相关SQL语句
    决心创业
    [转]在.NET环境中使用单元测试工具NUnit
    [转]IE"单击以激活控件"网站代码解决法
    [转]C#中ToString格式大全
    [转]div中放flash运行30秒钟后自动隐藏效果
    Property和attribute的区别
    C++中的虚函数(virtual function)
    进程间通信方式
    关于页面传值的方法
  • 原文地址:https://www.cnblogs.com/lukelmouse/p/10579916.html
Copyright © 2011-2022 走看看