POJ3239 http://poj.org/problem?id=3239
Time Limit: 1000MS | Memory Limit: 131072K | |||
Total Submissions: 3445 | Accepted: 1267 | Special Judge |
Description
The eight queens puzzle is the problem of putting eight chess queens on an 8 × 8 chessboard such that none of them is able to capture any other. The puzzle has been generalized to arbitrary n × n boards. Given n, you are to find a solution to the n queens puzzle.
Input
The input contains multiple test cases. Each test case consists of a single integer n between 8 and 300 (inclusive). A zero indicates the end of input.
Output
For each test case, output your solution on one line. The solution is a permutation of {1, 2, …, n}. The number in the ith place means the ith-column queen in placed in the row with that number.
Sample Input
8 0
Sample Output
5 3 1 6 8 2 4 7
Source
这道题是经典的八皇后问题的扩展——n皇后问题,解决n皇后问题的方法很多,但是这一题的特别之处在于n的规模可以达到300,尽管只要求一个解即可,但是300的规模也使得常见的搜索算法不可避免地超时。所以针对这一问题,目前有两种主流的解法:1.构造公式;2.随机算法。目前我的实现是基于随机算法,而这个算法的思路主要来源于如下博文:http://hi.baidu.com/zhuangxie1013/item/39f94824082c29869d63d1f7, 我的实现过程也是基本参照了该文的描述。
解题思路
对于这个题的每一个解可以由一个一维数组表示,数组的索引值表示列数,而数组中每一项的值,表示在该列上,皇后所处的行数。因此随机算法的思路就是随机生成一组数字,然后检测这组数字是否满足要求,如果不符合,记录有多少个冲突,然后交换数组中的两列,然后检测冲突数,如果冲突数没有减少,那么取消该次交换,然后将两列中的一列与数组中其他某列交换。如果交换后冲突数减少了,那么保留该次交换,如果冲突数已经减少到0,那么找到一个解,输出,如果还不为零,那么在该次交换的基础上继续进行交换,直到冲突数减到0。
下面就根据我的具体的代码实现,来详细阐述,顺便提一下poj提交系统对/**/注释的支持好像有问题,我提交这一题时,点击submit后,页面卡死,换用其他浏览器后显示“访问禁止”,原本以为是服务器维护之类的,使得这一题没办法提交,结果看提交记录,发现其他人在近期有提交该题的记录,所以觉得很奇怪,写了个hello world在代码框,发现可以提交了,这就费解了,这明显不是服务器的问题了。一点一点地添代码来测试,在牺牲了近10次提交数之后,终于发现时注释的问题。但让我现在也很费解的是:/**/作为C的注释类型,没理由不被C++编译器支持啊。
#include <iostream> // std::cout
#include <algorithm> // std::random_shuffle
using namespace std;
int num;
int board[301];
int diagRDtoLU[301*2-1]; //record the number of queens in each diagonal(right down to left up)
int diagLDtoRU[301*2-1]; //record the number of queens in each diagonal(left down to right up)
int NumOfConflicts;
num用于记录n皇后问题的维数, board数组用于存储n个皇后的位置,因为最高维数可能是300,所以使用301为了安全。diagRDtoLU和diagLDtoRU分别用于记录从右下到左上,和从左下到右上各条对角线上的冲突数,不难发现,对于n维的数组,它所对应的对角线数维2n-1,如下图:10*10,19条对角线。
NumofConflicts用于记录总的冲突个数。
void ComputeConflicts()
{
for(int i=0;i<num;i++)
{
int r = i;
int c=board[r];
diagRDtoLU[c+r+1]++; // the queen at (r,c) is on the (c+r+1) diagnoal(RD to LU) (r,c begin from 0)
diagLDtoRU[num-c+r]++; // the queen at (r,c) is on the (num-c+r) diagnoal(LD to RU) (r,c begin from 0)
}
for(int j=0;j<num*2-1;j++)
{
if(diagRDtoLU[j]>1)
NumOfConflicts += diagRDtoLU[j]-1;
if(diagLDtoRU[j]>1)
NumOfConflicts += diagLDtoRU[j]-1;
}
}
在这题中我将数组坐标看做行数,数组中每一个的值看做列数,这与题目叙述相反,但对于这题来说,不会对解产生影响,也就是说行和列在这题中是等效的。不难总结出如下规律对于右下到左上的对角线,坐标为(r,c)的网格对应于第c+r+1条对角线(从1开始算),同理,对于左下到右上的对角线,坐标为(r,c)的网格对应于第num-c+r条对角线,num为问题的维数。从代码可以看出,首先遍历整个数组,找出在从右下到左上和从左下到右上的所有对角线上的皇后数,当每条对角线上的皇后数多于1时,就发生了冲突,所以总的冲突数NumOfConflicts加1,遍历完所有的从右下到左上和从左下到右上的对角线后,得到当前总的冲突数。
int Delete(int r, int c, int NoC) //NoC means the number of conflicts
{
if(--diagRDtoLU[c+r+1])
{
NoC--;
}
if(--diagLDtoRU[num-c+r])
{
NoC--;
}
return NoC;
}
int Insert(int r, int c, int NoC)
{
if(++diagRDtoLU[c+r+1]>1)
{
NoC++;
}
if(++diagLDtoRU[num-c+r]>1)
{
NoC++;
}
return NoC;
}
void swap(int i, int j)
{
int temp = board[i];
board[i] = board[j];
board[j] = temp;
}
Delete和Insert两个函数用于计算在交换数组中两项后的冲突数,这个交换过程可以分为两部分,第一部分将要交换的数组的元素删掉,第二部分在要交换到的位置插入这个单元。Delete和Insert这两个函数并不真正的删除或插入数组中的元素,它们只是“模拟”删除或插入后的情况来计算冲突数,如果冲突数变少,那么才使用swap函数,真正的交换两个数组元素。Delete和Insert的if判断条件比较巧妙,例如:Delete中,先自减1,如果大于等于1,那么自减之前大于等于2,也就是说有冲突存在,那么删除该元素后,总的冲突数就自减1。同理,对于Insert,先自加,如果大于1,那么自加之前大于0,也就是说之前该对角线上就有皇后存在,那么插入新的数组元素后,在该对角线上必然产生冲突,所以冲突数自加1。swap函数作用很简单,在确定交换后,冲突数会减小的情况下,交换两个数组元素。
下面就通过main函数来看一下,整个程序的流程:
int main()
{
int NumOfSwap;
while(scanf("%d", &num))
{
if(num==0)
break;
for(int i=0;i<num;i++)
board[i]=i; //fill out the array, then we can use random_shuffle
while(1)
{
random_shuffle(&board[0],&board[num]); //shuffle from board[0]~board[num-1]
NumOfConflicts=0;
memset(diagRDtoLU,0,sizeof(int)*601);
memset(diagLDtoRU,0,sizeof(int)*601);
ComputeConflicts();
NumOfSwap=1; //if NumOfSwap==0, even though random_shuffle create an array with 0 conflicts, the program is still in the loop.
while(NumOfConflicts)
{
NumOfSwap = 0;
for(int i=0;i<num-1;i++)
{
for(int j=i+1;j<num;j++)
{
int NoC; //NoC:number of conflicts
NoC=Delete(i,board[i],NumOfConflicts);
NoC=Delete(j,board[j],NoC);
NoC=Insert(i,board[j],NoC);
NoC=Insert(j,board[i],NoC);
if(NoC<NumOfConflicts)
{
swap(i,j);
NumOfConflicts=NoC;
NumOfSwap++;
}
else
{
NoC=Delete(i,board[j],NoC);
NoC=Delete(j,board[i],NoC);
NoC=Insert(i,board[i],NoC);
NoC=Insert(j,board[j],NoC);
}
}
}
if(NumOfSwap==0)
break;
}
if(NumOfSwap!=0)
break;
}
for(int i=0;i<num;i++)
cout<<board[i]<<" ";
}
return 0;
}
首先读入问题的维数,当维数为0时,程序退出。要构造一个随机数组,我使用random_shuffle()这个库函数,需要引入预编译头#Include<algorithm>,第一个参数用要随机排列的数组的起始位置,第二个参数指出结束位置,但要注意的是这个位置实际上是随机排列的最后一位的后一位,例如两个参数为&board[o]和&board[num],所以排列的是board[0]到board[num-1]这n个元素。在使用random_shuffle之前,我们必须先填充这个数组,我们就顺序填充0到num-1这num个数,这样做的好处就是,不管random_shuffle产生怎样的结果,这个结果在行和列上都不会有冲突,所以我们只用检测两种对角线上的冲突数就可以了。我先将要使用的几个变量初始化,然后使用ComputerConflicts计算当前的冲突数,如果冲突数不为0,那么就需要进行交换。进入到循环体中,现将交换次数初始化为0,因为当前还没有进行交换,然后从第一个元素开始,让它和相邻的元素开始交换,如果交换后冲突数小于原来的冲突数,那么,就认为当前的数组比之前的数组更优,所以就执行swap真正地交换两个元素,交换次数NumOfSwap加1。如果交换后的冲突数大于等于原来的冲突数,那么就废除这次交换。然后进入下一次循环。如果整个双层的for循环执行完之后,NumOfSwap==0,那么就意味着当前数组已没有进一步优化的可能,所以跳出while循环,重新使用random_shuffle()函数得到一个随机的排列,然后在进入到循环之中,重新寻找满足条件的解。如果
整个双层的for循环执行完之后,NumOfSwap!=0,那么也就说明在刚刚这个双重循环中,数组中的结果得到了优化,那么就回到while的判断条件处,看冲突数是否已经为0,如果为0,那么已经找到了解,如果不为0,那么继续在当前数组的基础上从头开始进行交换,直至最终找到没有冲突的排列,然后退出循环打印结果。