三种算法求解经典N皇后问题

三种算法求解经典N皇后问题

【问题描述】
八皇后问题是一个古老而著名的问题,是回溯算法的典型例题。该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。
要求扩展到任意的皇后数的情况,同时对源程序给出详细的注释。


看到n皇后问题首先想到的就是回溯法求解,这是一个非常经典的解法,另一个方案就是递归的思想。

至于第三种解法,算法思想也是采用了递归,但在数据结构方面,使用了非常少见的机器码存储,各种操作、判断的实现采用的是位运算,因此单独列出,详见下文。

【数据存储】
一维数组Q[],下标表示第i个皇后,也是当前放置的皇后所处行数,数组元素值Q[i]表示列的位置,所以棋盘上的点阵坐标可表示为 ( i , Q [ i ] ) (i, Q[i]) (i,Q[i])

1、递归求解n皇后

【算法思想】
首先判定当前位置 ( i , Q [ i ] ) (i, Q[i]) (i,Q[i])是否可以放置皇后,要保证同列、对角线上没有被放置其他皇后,(这里不去判断同行的原因是,在寻找位置时是按行顺序进行的,所以每一个新皇后同行必然不存在其他皇后),按照坐标判断 Q [ j ] = = Q [ i ] Q[j]==Q[i] Q[j]==Q[i], a b s ( Q [ j ] − Q [ i ] ) = = a b s ( j − i ) abs(Q[j]-Q[i])==abs(j-i) abs(Q[j]Q[i])==abs(ji) 即可。
然后进行递归求解,遍历每一行,只要满足放置条件,则可以进行下一个皇后的放置,n次放置即可求出一个解。
注: 完整代码在下面回溯法中一同贴出

2、 非递归回溯探测

【算法思想】
可用位置判定与上一个方案相同,依旧是先判断当前位置是否满足条件,满足就进入下一个行皇后的放置;遍历下一行满足条件的位置,如果当前行中所有位置都不满足条件,那么进行回溯,在上一行继续向后寻找位置重新放置皇后。
判断结束的标志,当回溯向前遍历了所有行,则结束程序。

public class Item_04 {

	static final int N = 8; //皇后的数量
	static int[] Q = new int[N+1]; 
	static int count = 0; //记录解的个数
	public static void main(String[] args) {
		long start = System.currentTimeMillis();
		
		NQueens(N);  //非递归
//		NQueensTrace(1, N); //递归
		
		System.out.println(N+"皇后问题解的个数: "+count);
		long end = System.currentTimeMillis();
		System.out.println("程序运行时间:"+(end-start)+"ms");
	}
	
	
	/**
	 * 判断位置(i,Q[i])是否可以放皇后
	 * @param i 
	 * @return
	 */
	private static boolean isPlace(int i) {
		int j = 1;
		if(i==1) {
			return true; //第一个皇后
		}
		while (j<i) {		//j=1~i-1是已放置了皇后的行
			//该皇后是否与以前皇后同列,位置(j,Q[j])与(i,Q[i])是否同对角线
			if((Q[j]==Q[i]) || (Math.abs(Q[j]-Q[i])==Math.abs(j-i))) 
				return false;
		    j++;
		}
		return true;
	}
	/**
	 * 非递归求解
	 * @param n 皇后的数量
	 */
	private static void NQueens(int n) {
		int i = 1;
		Q[i] = 0; //第一个皇后从(1,1)开始
		while(i>=1) { //回溯是否结束
			Q[i]++;
			while(!isPlace(i) && Q[i]<=n) {
				Q[i]++; //判断(i,Q[i])是否可以放置第i个皇后,若此行无法放置,则Q[i]循环结束后为n+1
			}
			if(Q[i]<=n) {
				if(i==n) {
					count++;
					out();
				}else {
					i++;
					Q[i]=0; //位置(i,Q[i])满足条件,转向下一行,放置第i+1个皇后
				}
			}else {
				i--; //Q[i]循环结束后为n+1,回溯
			}
		}
	}
	
	/**
	 * 递归求解n皇后问题
	 * @param tmp 当前需要排序的皇后所在行
	 * @param n 皇后的数量
	 */
	private static void NQueensTrace(int tmp, int n) {
		if(tmp>n){ //一次放置结束
			count++;
	        out();
		}
		else{	     
			for(int i=1;i<=n;i++){ //遍历当前行
	            Q[tmp]=i;
	            if(isPlace(tmp)){  //该位置可以放置
	            	NQueensTrace(tmp+1,n); //放置下一个皇后
	            }
	        }
		}
	}
	
	/**
	 * 输出m皇后的解
	 */
	private static void out() {
		System.out.println();
		System.out.println("第"+count+"个解:");
		for(int i=1;i<=N;i++) {
			System.out.print("("+i+","+Q[i]+")" + "  ");
		}
		System.out.println();
	}
}

3、位运算求解

前两种方案算法思想较为常见,思路也比较简单,但是当n的数值较大时,无论是递归求解还是非递归求解,都将花费大量的时间,8皇后问题仅仅92种解法递归就消耗了3毫秒,到16皇后问题时,14772512个解消耗时间就达到了5分钟左右。
就这个问题,我查找了资料,发现了位运算的方法,求解16皇后的程序在我的电脑上运行仅仅需要19秒左右。以下是这个算法的详细介绍。
【算法思想】
和普通算法一样,这是一个递归过程,程序一行一行地寻找可以放皇后的地方。过程带三个参数,row、ld和rd,表示三个方向(列、斜对角)可用位置,把它们三个并起来,得到该行所有的禁位,取反后就得到所有可以放的位置(用pos来表示)。以下是图解row、ld、rd在寻找合适位置pos时判断的情况。
此时, r o w = 00001000 , l d = 00000000 , r d = 01000000 row=00001000,ld=00000000,rd=01000000 row=00001000ld=00000000rd=01000000,pos=upperlim&~(row | ld | rd)
三种算法求解经典N皇后问题_第1张图片
取反后POS在计算机中以补码形式存在,按位与后,得到最右边的1,也就是当前要放皇后的位置。递归调用时,每个参数都加上了一个禁位,但两个对角线方向的禁位对下一行的影响需要平移一位。最后,如果递归到某个时候发现row=11111111了,说明八个皇后全放进去了,此时程序从第1行跳到第11行,找到的解的个数加一。

#include 
#include 
#include 

long sum = 0, upperlim = 1;

void test(long row, long ld, long rd) //row、ld、rd分别表示列和两条对角线上的皇后冲突 
{
   if (row != upperlim) //row为11111111这样即结束一次求解 
   {
	  long pos = upperlim & ~(row | ld | rd); //三个约束先或运算获得三个方向都为0(都没有冲突)的状态,取反,再进行与运算, 
	                                          //得到的值pos就是所有可以放置皇后的位置 
	  while (pos)
	  {
		 long p = pos & -pos; // 取反后POS在计算机中以补码形式存在,按位与后,得到最右边的1,也就是当前要放皇后的位置 
		 pos -= p;  //把这个1从POS中取出 
		 test(row + p, (ld + p) << 1, (rd + p) >> 1); //三个方向的约束全部加上p,表示p位置被标记已放置皇后,在后来的操作中将被禁用 
	  }
   } else
  sum++; //寻找到一个解 
}

int main(int argc, char *argv[])
{
   time_t tm;
   int n = 16;

   if (argc != 1)
  	  n = atoi(argv[1]);
   tm = time(0);
   if ((n < 1) || (n > 32))
   {
	  printf(" 只能计算1-32之间\n");
	  exit(-1);
   }
   printf("%d 皇后\n", n);
   upperlim = (upperlim << n) - 1;

   test(0, 0, 0);
   printf("共有%ld种排列, 计算时间%d秒 \n", sum, (int) (time(0) - tm));
}

三种算法运行时间截图(因电脑状态不同,不同电脑可能会有细微差距,有兴趣的话可以将代码复制下来在自己电脑上跑一遍)

  • 递归16皇后
    三种算法求解经典N皇后问题_第2张图片
  • 非递归16皇后
    三种算法求解经典N皇后问题_第3张图片
  • 位运算
    三种算法求解经典N皇后问题_第4张图片
    部分内容参考Matrix大佬的文章,这里贴上文章链接,有兴趣的话可以看一看。

你可能感兴趣的:(算法)