N皇后问题初探

问题简述

在n*n的棋盘上放n个皇后,使其互相不能进行攻击,问共有几种放法。

题解

递归版本

看到问题之后自己先写了一个简单的递归版本。没有什么太多想说的,就有一点,一开始想的还是用个二维数组标标0/1,后来想到每一行有且仅有一个皇后,那其实只要说知道某行的那个皇后在第几列就行了,于是改成了一维数组。

//recursive
#include<stdio.h>
#include<iostream>
#include<time.h>
using namespace std;
int a[30];
int n,sum=0;
int judge(int x,int y)
{
    for(int i=1;i<x;i++)
        if(y==a[i]||(x+y)==(i+a[i])||(x-y)==(i-a[i]))
            return 0;
    return 1;
}
void fun(int i)
{
    if(i>n)
    {
        //for(int k=1;k<=n;k++)
        //  printf("%d ",a[k]);
        //printf("\n");
        sum++;
    }
    else
    {
        for(int j=1;j<=n;j++)
            if(judge(i,j))
            {
                a[i]=j;
                fun(i+1);
            }
    }
}
int main()
{
    time_t start,stop;//计时用的
    start=time(NULL);

    scanf("%d",&n);
    fun(1);

    stop=time(NULL);
    printf("%ld\n",(stop-start));//n=15用时66s左右 
    return 0;
}

非递归版本

听说非递归比递归效率高而且不会堆栈(?)溢出,于是开始写非递归版本。就是要手动回溯。
这里主要是这样一个情况:某一行的皇后依次在他可以放置的地方放下去,直到放无可放了,那么就要回到上一行,将上一行的皇后放到下一个可放的位置上去,这里要有一个回溯。有两种特殊的情况:如果是第一行的皇后到头了,那么整个程序结束。如果是最后一行的皇后放下去了,那么就算得了一组解,然后回到倒数第二行,摆下一个(也就是另一个)位置。

non-recursive
#include<stdio.h>
#include<iostream>
#include<time.h>
#include<string.h>
using namespace std;
int a[30];
int judge(int x,int y)
{
    for(int i=1;i<x;i++)
        if(y==a[i]||(x+y)==(i+a[i])||(x-y)==(i-a[i]))
            return 0;
    return 1;
}
int main()
{
    time_t start,stop;
    start=time(NULL);

    int i=1,j=1;
    int n,sum=0;
    scanf("%d",&n);
    memset(a,-1,sizeof(a));
    while(i<=n)
    {
        while(j<=n)
        {
            if(judge(i,j))//能放 
            {
                a[i]=j;
                j=1;
                break;
            }
            else//不能放,下一列 
                j++;
        }
        if(a[i]==-1)//该行放不了 
        {
            if(i==1)//第一行不能再放了 
                break;
            else
            {
                i--;
                j=a[i]+1;
                a[i]=-1;
                continue;
            }
        }
        if(i==n)//最后一行 (已经是该行能放的情形了) 
        {
//          for(int k=1;k<=n;k++)
//              printf("%d ",a[k]);
//          printf("\n");
            sum++;
            j=a[i]+1;
            a[i]=-1;
            continue;
        }
        i++;
    }
    printf("%d\n",sum);
    stop=time(NULL);
    printf("%ld\n",(stop-start));//n=15用时65左右 
    return 0;
}

但是从结果可以看到,这个速度并没有变快,而且代码烦了不止一点点,可以说是非常气人了。

位运算版本

首先列几个位运算:

a & b 按位与 每一位上进行与运算
a | b 按位或 每一位上进行或运算
a ^ b 按位异或 每一位上进行异或运算(不同则为1)
~a 按位取反 每一位上取反
a << b 左移b位 …… 字面意思,相当于十进制*2
a >> b 右移b位 本题不涉及符号问题,故同上,相当于十进制/2

首先说明,我看了以上这些位运算操作之后,想不到和这道题有一点联系,于是直接看了别人的代码,在看他的注释之前,自己写了一点解析。我看的是他先给了个核心部分,代码如下

//初始化: upperlim =  (1 << n)-1; Ans = 0;

//调用参数:test(0, 0, 0);
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
n位上都是1的数 (所以row=upperlim时得到一个解)

三个参数row,ld,rd中的1表示该位不能放

pos=upperlim&(~(row|ld|rd))
row|ld|rd形成的二进制数中的1表示该位不行
取反后形成的二进制数中的1表示该位可以
与upperlim 按位与 后确保了位数为n

p=pos&(~pos+1)
p的2进制中有唯一一个1表示pos的最右边一个1的位置,也即可放的最右一列
举个例子:
pos =10100
~pos =01011//~将右侧0全变为1,第一个1变为0
~pos+1 =01100//+1把0111…111变为1000…000
pos&…=00100//左边几位 取反 后与原数 按位与 后都为0,而最右端的1此时仍为1

pos=pos-p
因为前面是while(pos)所以每次都把最右边一个1取掉,也就是放入的操作
用来看什么时候取完的

放入由p定位的皇后之后新的三个参数
(row | p, (ld | p) << 1, (rd | p) >> 1)
row|p就是把p这一列标为不行,ld|p再<<1表示p这的左侧一列标为不行
相当于每进行一层递归(到下一行),就会多一个由p决定的不行的位置,然后平移
00*00 例如第一行ld=00000,p=00100,
0100* 那么第二行的ld*=(ld|p)<<1=01000,p*比如=5
10010 第三行的ld**=(ld*|p*)<<1=10100
可以看到这个ld已经将皇后的左下斜线上的点都标为了1

完整代码如下
#include<stdio.h>
#include<iostream>
#include<time.h>
#include<string.h>
using namespace std;
long sum,upperlim;
int n;
int fun(long row,long ld,long rd)
{
    int pos,p;  
    if(row!=upperlim)  
    {
        pos=upperlim&(~(row|ld|rd));  
        while(pos)  
        {
            p=pos&(~pos+1);  
            pos=pos-p;  
            fun((row|p),(ld|p)<<1,(rd|p)>>1);  
        }
    }
    else  
        sum++;
}
int main()
{
    time_t start,stop;
    start=time(NULL);

    scanf("%d",&n);
    upperlim=(1<<n)-1;
    fun(0,0,0);
    printf("%d\n",sum);

    stop=time(NULL);
    printf("%ld\n",(stop-start));//n=15三秒,n=16用时17s,n=17用109秒 
    return 0;
}

可以看到速度上有了较为明显的提升。当然这个地方要注意,因为是整型的缘故,最多n=32。除了使用位运算外,还可以利用对称性来进行进一步优化,这里立一个flag,等有空的时候会来做这个事情的。对n皇后问题的初次尝试到这里就告一段落。(逃

最后分享一个终级版本

我说不出话来

//poj3239,n的规模300,但只要一个解
#include<iostream>
using namespace std;
int main()
{
    int n,k,i;
    while(1)
    {
       cin>>n;
       if(n==0)break;

       if(n%6!=2 && n%6!=3)
       {
           if(n%2==0)
           {
              for(i=2;i<=n;i+=2)cout<<i<<' ';
              for(i=1;i<n-1;i+=2)cout<<i<<' ';
              cout<<n-1<<endl;
           }else
           {
              for(i=2;i<n;i+=2)cout<<i<<' ';
              for(i=1;i<n;i+=2)cout<<i<<' ';
              cout<<n<<endl;
           }
       }else
       {
           k=n/2;
           if(k%2==0 && n%2==0)
           {
              for(i=k;i<=n;i+=2)cout<<i<<' ';
              for(i=2;i<=k-2;i+=2)cout<<i<<' ';
              for(i=k+3;i<=n-1;i+=2)cout<<i<<' ';
              for(i=1;i<k+1;i+=2)cout<<i<<' ';
              cout<<k+1<<endl;
           }
           else if(k%2==0 && n%2==1)
           {
              for(i=k;i<=n-1;i+=2)cout<<i<<' ';
              for(i=2;i<=k-2;i+=2)cout<<i<<' ';
              for(i=k+3;i<=n-2;i+=2)cout<<i<<' ';
              for(i=1;i<=k+1;i+=2)cout<<i<<' ';
              cout<<n<<endl;
           }
           else if(k%2==1 && n%2==0)
           {
              for(i=k;i<=n-1;i+=2)cout<<i<<' ';
              for(i=1;i<=k-2;i+=2)cout<<i<<' ';
              for(i=k+3;i<=n;i+=2)cout<<i<<' ';
              for(i=2;i<k+1;i+=2)cout<<i<<' ';
              cout<<k+1<<endl;
           }
           else
           {
              for(i=k;i<=n-2;i+=2)cout<<i<<' ';
              for(i=1;i<=k-2;i+=2)cout<<i<<' ';
              for(i=k+3;i<=n-1;i+=2)cout<<i<<' ';
              for(i=2;i<=k+1;i+=2)cout<<i<<' ';
              cout<<n<<endl;
           }
       }
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值