问题描述:
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; } }
解决了这个问题,基本上整个N皇后问题就迎刃而解了!
感觉这种解决问题的思路同样适用于最佳调度和迷宫问题,下次可以再写一篇。
代码写得还蛮OK,改改添添应该可以弄出很多玩法,欢迎调戏!
水平尚不足,欢迎指正批评!