C/C++ N皇后问题

问题描述:

在国际象棋的棋盘上两个皇后若在同一行、列或斜线上则这两个皇后可以相互攻击。N皇后问题是指在N*N的棋盘上放置N个皇后保证任意两个皇后均不可相互攻击,求取在棋盘上放置N皇后的所有方案。

求解思路:

        N皇后问题求解,可以使用回溯法;所谓回溯法是一种深度优先遍历的应用。可以理解为为了达到某一位置、到达某一目的按如下方式处理:

  1. 在开始时选择一个存在方案一直前行

  2. 前行过程中若遇到多个分支,选择一个分支前行,直到目的达成或发现此路不通,回溯到第3

  3. 返回前行过程中最后经过的一个多分支节点,若存在下一分支节点选择下一分支继续前行,转到第2步;若该分支节点所有节点遍历完毕,则回溯到第3步【起点不可回溯,当起点的分支节点遍历完毕时表示完成所有可能尝试】

求解描述:

  1. 使用一个N*N的二维数组来表示棋盘,选取当前行curRow=0

  2. 选取当前列curCol = 0,转第3

  3. 检查位置(curRow,curCol)是否可以放置皇后

    若可以放置则放置皇后,转第4

    若不可放置皇后则转第5

  4. 判断curRow取值,

    curRow == N-1找到一组解打印;回溯到上一行,继续上一行的步骤5.

    curRow < N-1, 将下一行作为curRow,转第2

  5. 进行curCol++【向后偏移一列】,若curCol == N回溯到上一行的第5步,否则转第3


先上一个简单的求解方案,每次判定位置是否可以放置皇后都对行/列/斜线上的所有点实行探测:

#include 

//定义皇后的个数
#define MAX_QUEEN_NUM  8

//定义棋盘
int g_chaseBoard[MAX_QUEEN_NUM][MAX_QUEEN_NUM];

//进行位置有效性检查, 若位置在棋盘内则返回true,否则返回false
bool validPos(int rowNum, int colNum)
{
    return !(0 > rowNum || MAX_QUEEN_NUM - 1 < rowNum || 0 > colNum || MAX_QUEEN_NUM - 1 < colNum);
}

//判断位置是否可以放置皇后
//使用最易理解,未加优化的方式
bool checkPos(int rowNum, int colNum)
{
  for (int j = 0; j < MAX_QUEEN_NUM; j++)
  {
	  //判定同一行是否已经存在皇后
      //判定同一列是否已经存在皇后
	  if (g_chaseBoard[rowNum][j] != 0 || g_chaseBoard[j][colNum] != 0)
	  {
		  return false;
	  }

	  //右上左下斜线,右上节点检查
	  if (validPos(rowNum + j, colNum + j) && g_chaseBoard[rowNum + j][colNum + j] != 0)
	  {
		  return false;
	  }
	  
	  //右上左下斜线,左下节点检查
	  if (validPos(rowNum - j, colNum - j) && g_chaseBoard[rowNum - j][colNum - j] != 0)
	  {
		  return false;
	  }
	  
	  //左上右下斜线,左上节点检查
	  if (validPos(rowNum + j,  colNum - j) && g_chaseBoard[rowNum + j][colNum - j] != 0)
	  {
		  return false;
	  }
	  
	  //左上右下斜线,右下节点检查
	  if (validPos(rowNum - j, colNum + j) && g_chaseBoard[rowNum - j][colNum + j] != 0)
	  {
		  return false;
	 }
  }
  return true;
}

//打印棋盘上的N皇后位置
void printBoard()
{
	static Num  = 0;
	printf("the Num is %d:\n", Num++);
    for (int i = 0; i < MAX_QUEEN_NUM; i++)
    {
		for (int j = 0; j< MAX_QUEEN_NUM; j++)
		{
			printf("%d ", g_chaseBoard[i][j]);
		}
		printf("\n");
    }
}

//初始化棋盘
void initBoard()
{
    for (int i = 0; i < MAX_QUEEN_NUM; i++)
    {
		for (int j = 0; j< MAX_QUEEN_NUM; j++)
		{
			 g_chaseBoard[i][j] = 0;
		}
    }
}

//进行N皇后求解
void QueenSet(int RowNum)
{
   for (int ColNum = 0; ColNum < MAX_QUEEN_NUM; ColNum++)
   {
       if (checkPos(RowNum,ColNum))
       {
		   g_chaseBoard[RowNum][ColNum] = 1;
		   if (RowNum == MAX_QUEEN_NUM - 1)
		   {  //找到一组解,回溯到上层
			   printBoard();
			   //回溯到上层时需要将本节点数据清理掉
			   g_chaseBoard[RowNum][ColNum] = 0;
			   return;
		   }
		   else
		   {
			   //递归调用下一行处理
			   QueenSet(RowNum+1);
			   //递归结束,尝试下一节点前需要先将本节点数据清理掉
               g_chaseBoard[RowNum][ColNum] = 0;
		   }
       }
   }

}

void main()
{
   initBoard();
   QueenSet(0);
}

为了获取更优的求解方案,我们先来思考两个问题:

  1. N皇后问题的解必须用N*N的二维数组来表示吗?

  2. N皇后问题判断一个位置是否可以放置,真的需要对行、列、斜线上的所有节点实施判断吗?

 

先来回答第一个问题

N皇后问题的解只要记录下N个皇后的摆放位置就可以了,也就说只要记录下每行皇后的列位置即可,那么完全可以使用一维数组来表示N皇后问题的解;一维数组的下标表示行号,一维数组的元素值表示列号。

 

再来回答第二个问题:

  1. 行判定:根据我们的实现原理,使用递归+回溯方式,在求解过程中结果的Row中每行设置且仅设置一个皇后,在未到达的行还没有开始设置皇后;也就是说根本不需要行判定。

  2. 列判定:一共有N列,使用一维数组标识出该列是否已经存在皇后即可完成判定,无需对棋盘上某一列的所有位置进行判定。

  3. 斜线判定:对于一个N*N棋盘其左下右上和左上右下的斜线个数是固定的,且每个斜线上的位置也是固定的;使用一维数组标识出某一斜线上是否存在皇后也可完成判定,无需对斜线上的所有位置实施判断。

 

解答完两个问题可以得出这样的结论:

        进行N皇后问题可以使用如下的数据结构来求解:

  1. N个元素的一维数组RowRec,用于保存N皇后的解【数组元素使用INT类型】

  2. N个元素的一维数组ColFlg,用于标识列上是否存在皇后【数组可以使用bit位】

  3. 2N-1个元素的一维数组LURFlg,用于标识左下右上斜线上是否存在皇后【数组可以使用bit位】【为什么是2N-1个请往下看】

  4. 2N-1个元素的一维数组LDRFlg,用于标左上右下斜线上是否存在皇后【数组可以使用bit位】

要想正确使用LURFlgLDRFlg需要先清楚如何获棋盘上任一点对应的斜线,我们通过下幅图来作答:

C/C++ N皇后问题_第1张图片

左下右上斜线(LUR):同一斜线上的位置其坐标X-Y取值一致,从左到右斜线上上对应的值为-(N-1)~(N-1),共计2N-1个取值。

左上右下斜线(LDR):同一斜线上的位置其坐标X+Y取值一致,从左到右斜线上上对应的值为0~(2N-2),共计2N-1个取值。

 

下面我们就使用这套结构来进行N皇后问题的求解:

#include 

//定义皇后的个数
#define MAX_QUEEN_NUM  8

//定义皇后存放位置的数组,下标表示行号,值表示列号
int g_QueenPos[MAX_QUEEN_NUM];

//使用INT类型32个Bit来标识列上是否已经存在皇后;
int g_colPutFlg = 0;
//使用INT类型32个Bit来标识左下右上斜线是否已经存在皇后,这里限制了最多支持16皇后
int g_LURFlg = 0;
//使用INT类型32个Bit来标识左上右下斜线是否已经存在皇后;
int g_LDRFlg = 0;

//判断位置是否可以放置皇后, 可以放置皇后返回1,否则返回0
//使用位操作判断列、左斜和右斜上是否已经存在皇后
int checkPos(int rowNum, int colNum)
{
    return !(g_colPutFlg & 1 << colNum || g_LDRFlg & 1 << (rowNum + colNum) || g_LURFlg & 1 << (MAX_QUEEN_NUM-1 + rowNum - colNum));
}

//设置位置对应的列、斜线放置皇后标识
void setPos(int rowNum, int colNum)
{
	g_colPutFlg = g_colPutFlg | 1 << colNum ;
	g_LDRFlg = g_LDRFlg | 1 << (rowNum + colNum);
	g_LURFlg = g_LURFlg | 1 << (MAX_QUEEN_NUM-1 + rowNum - colNum);
}
//恢复位置对应的列、斜线放置皇后标识
void resetPos(int rowNum, int colNum)
{
	g_colPutFlg = g_colPutFlg & ~(1 << colNum);
	g_LDRFlg = g_LDRFlg & ~ (1 << (rowNum + colNum));
	g_LURFlg = g_LURFlg & ~ (1 << (MAX_QUEEN_NUM-1 + rowNum - colNum));
}


//打印棋盘上的N皇后位置
void printBoard()
{
	static Num  = 0;
	int i, j;
	printf("the Num is %d:\n", Num++);
    for (i = 0; i < MAX_QUEEN_NUM; i++)
    {
		for (j = 0; j< MAX_QUEEN_NUM; j++)
		{
			if (g_QueenPos[i] - 1 == j)
			{
				printf("%d ", g_QueenPos[i]);
			}
			else
			{
			    printf("0 ");
			}
		}
		printf("\n");
    }
}
//进行N皇后求解
void QueenSet(int RowNum)
{
   int ColNum;
   for (ColNum = 0; ColNum < MAX_QUEEN_NUM; ColNum++)
   {
       if (checkPos(RowNum,ColNum))
       {
		   g_QueenPos[RowNum] = ColNum + 1;
		   setPos(RowNum,ColNum);
		   if (RowNum == MAX_QUEEN_NUM - 1)
		   {  //找到一组解,回溯到上层
			   printBoard();
			   //回溯到上层时需要将本节点数据清理掉
			   resetPos(RowNum, ColNum);
			   return;
		   }
		   else
		   {
			   //递归调用下一行处理
			   QueenSet(RowNum+1);
			   //递归结束,尝试下一节点前需要先将本节点数据清理掉
               resetPos(RowNum, ColNum);
		   }
       }
   }

}

void main()
{
   QueenSet(0);
}


 


用自己的机器做了简单测试:

使用方案2进行14皇后求解耗时7;

使用方案1进行13皇后求解耗时15秒,进行14皇后求解耗时104秒。


总的来说回溯法只是说明了大的放向,要根据具体问题定义合适的结构求解方能获取较好的性能。



你可能感兴趣的:(算法&数据结构,N皇后,c++,优化,性能)