问题描述:
N皇后问题是指在N*N的国际象棋棋盘上放上N个皇后,她们之间互相不能攻击,用回溯算法得出这个问题的所有解。
解决思路:
1、理解回溯算法:
作为五大经典算法,回溯算法的地位之高不言而喻,在面对需要一步一步解决的问题时(例如:下棋、迷宫、最佳调度),它是一种非常通用的解题方法。这种算法概括起来就是一种类似枚举的搜索尝试过程,它的基本思想就是在空间中摸索,遇到不满足约束条件时,回退到之前导致这个结果的节点并另做选择,直到搜索出结果为止。
2、关于错误的理解
回溯问题非常容易地被错误的理解为这种感觉:
1、走一步
2、如果这步对:继续走
3、否则:返回上一步,选择另一步
这里“步”的概念非常容易被混淆,回溯问题追溯的是“步”,但不是“一步”,而是一系列的步数(一组选择),就说如果你如果做错了什么事情,你应该回到的是你开始做这件事情的时候(而不是前一步),并且在那里选择另外一条路去尝试。
这听起来有些匪夷所思,但这就是我们来解决这个问题思想的核心所在。
我们来看一段代码,看看我们如何实现这个思想。
bool Walk-1(走的坐标)//第一次递归
{
if(判断还能不能走的函数)
{
return 不能走; //退出递归基准情况 1
}
if(走到终点了)
{
return 到终点!//退出递归基准情况 2
}
if(Walk(下一步))//看看下步能不能走
{
return 能走;
}
if(walk(另一步))
{
return 能走;
}
}
bool Walk-2(走的坐标)//第二次递归
{
if(判断还能不能走的函数)
{
return 不能走; //退出递归基准情况 1
}
if(走到终点了)
{
return 到终点!//退出递归基准情况 2
}
if(Walk(下一步))//看看下步能不能走
{
return 能走;
}
if(walk(另一步))
{
return 能走;
}
}
.......
bool Walk-n(走的坐标)//第n次递归
{
if(判断还能不能走的函数)//如果这里判断为不能走,则上一个递归的walk n-1会得到“不能走”,上上个递归也会得到“不能走”
{
return 不能走; //于是一层一层的walk全都返回“不能走”,最后回溯到walk-1,于是walk-1就不走“下一步”了,它就会去尝试走"另一步"
}
if(走到终点了)
{
return 到终点!//退出递归基准情况 2
}
if(Walk(下一步))//再看看下步能不能走
{
return 能走;
}
if(Walk(另一步))
{
return 能走;
}
}
这里我是用递归的办法来实现这个算法。这是一种非常特殊的递归,它在if语句中调用走下一步递归函数,这里的if(Walk(下一步))就好像可以返回未来的结果,并且通过这个结果来改变当前一步的进程,真正做到了你如果做错了什么事情,你就会回到开始做这件事情的时候。
“if(递归函数(下一步))”这个可以作为回溯问题的一个模板。(想回溯问题想不通的时候感觉就往这个里面套就好了^_^)
所以我们现在要解决N皇后问题,就明确了回溯算法的解决思路:
第一步:放下皇后
第二步:判断是否可以放,如果可以放就到第三步,否则就第四步。
第三步:判断是否放到最后一行,如果不是,递归进入下一行,回到第一步;如果是,打印一个解,返回失败,递归返回之前的节点。
第四步:判断是否放到最后一列,如果不是,递归进入下一列,回到第一步;如果是,就返回失败,递归回到上一个节点。
第五步:判断是否所有的可能性都下完,如果是,返回成功,递归结束;如果否,什么都不做。
#include<iostream>
#define NQ 7 //皇后的数量
using namespace std;
char NQueen[NQ][NQ];//皇后的二维数组
int Index = 0;//数打出多少个
bool judge(int row,int column)//判断皇后是否互相攻击
{
if(row==0)//下第一步肯定没人攻击
return true;
for(int i = 0;i < NQ;i++)//判断斜对角攻击
{
for(int j = 0;j < NQ;j++)
{
if(!(row==i&&column==j))
if(row+column==i+j||row-column==i-j||column-row==j-i)
if(NQueen[i][j]=='Q')
return false;
}
}
for(int i = 0;i < row;i++)//判断列攻击
{
if(NQueen[i][column]=='Q')
return false;
}
for(int i = 0;i < column;i++)//判断行攻击(其实没必要)
{
if(NQueen[row][i]=='Q')
return false;
}
return true;
}
void print()//打印函数
{
for (int i = 0;i<NQ;i++)
{
for (int j = 0;j < NQ;j++)
{
cout<<NQueen[i][j];
}
cout<<endl;
}
}
bool WalkNQ(int row,int column)//以皇后落子的位置作为参数
{
if(row == NQ)//基准情况1、当行数不断增加到达最大值时
{
Index++;//计数君增加
cout<<Index<<endl;//打印计数君
print();//打印地图
cout<<endl;
return false;//若只需要一种情况,此处return true即可
//到底继续返回失败可以使程序不断回溯,直至最后一种情况
}
if(column== NQ)//基准情况2、若列数不断增加大于棋盘范围(意为无子可下)
{
return false;//返回失败
}
NQueen[row][column] = 'Q';//放下一个皇后
if(judge(row,column))//如果判断可以走
{
if(WalkNQ(row+1,0))//试探下一行能不能走(核心!!) (从第一列开始下)
{
return true;
}
}
NQueen[row][column] = '*';//如果不能走,放下的Q变成棋盘
return WalkNQ(row,column+1);//走下一列试试看
}
int main()
{
for (auto &i : NQueen)//给全部的NQueen赋值 (C++11新特性)
{
for (auto &j : i)
{
j = '*';
}
}
WalkNQ(0,0);//初始位置
return 0;
}
整个基本流程都在代码上打好注释了,看代码即可。
要提的是此处的递归函数WalkNQ的基准情况(Base Condition)设置相当巧妙,在得到一个解的时候(第一个基准情况),不返回true,而是打印该时刻的数组,直到回溯到最后一种情况的时候才结束递归。这种设置多种基准情况的形式可以使回溯算法穷举结果。这个确实有点难理解,但是理解透彻了做很多题会轻松很多。
3、判断皇后互相攻击的函数
解决完回溯算法的函数,我们最后来看看判断皇后是否互相攻击的函数。
判断同行同列攻击就不用说了,判断斜对角的攻击比较复杂。
这里有个挺有意思的规律:
凡是在斜对角上的坐标,行列相加或相减都会等于皇后所在位置的行列相加或交错相减,这个很拗口,我把图和代码放上来。
for(int i = 0;i < NQ;i++)//判断斜对角攻击
{
for(int j = 0;j < NQ;j++)
{
if(!(row==i&&column==j))//小心!他自己本身的位置也是满足判断是否被攻击的公式的!要剔除他本身的位置。
if(row+column==i+j||row-column==i-j||column-row==j-i)
if(NQueen[i][j]=='Q')/
return false;
}
}
if(row+column==i+j||row-column==i-j||column-row==j-i)
if(NQueen[i][j]=='Q')/
return false;
}
}
解决了这个问题,基本上整个N皇后问题就迎刃而解了!
感觉这种解决问题的思路同样适用于最佳调度和迷宫问题,下次可以再写一篇。
代码写得还蛮OK,改改添添应该可以弄出很多玩法,欢迎调戏!
水平尚不足,欢迎指正批评!