用STL实现先深搜索及先宽搜索
——
N
皇后问题例子
前两天在sharpdew的BLOG (
http://blog.youkuaiyun.com/sharpdew) 上看到关于N皇后问题的文章,sharpdew的算法很漂亮,效率很高。我只是想从测试的角度出发试一下我的DFS/BFS算法,所以试着写了一下。不过我可以告诉大家,我的程序比sharpdew的至少慢了好几倍,我想原因是STL容器(如vector、stack、queue等)在搜索树的大小变大时效率下降。毕竟sharpdew的算法全都使用了long类型来处理,效率不可同日而语。
N皇后问题说的是,在一个NxN的棋盘中放置N个皇后(国际象棋的),要求每个皇后之间不能相互攻击(即同一行、同一列、同一条斜线上都不能有两个皇后)。举个例子,N=8时,以下状态是一个解:
我不准备输出所有解答,只是搜索一下对于指定的N,共有多少解答。和sharpdew的程序一样,我也把N限定在32以下,由于N=1、2、3时,显然没有解,所以我假定N在4-32之间。
与我们在数独程序中所做的一样,我们需要的是一个可以配合DFS/BFS的状态类,我把它命名为QueenState。除了nextStep()和isTarget()之外,它还需要什么呢?首先是数据成员,即如何表示一个棋盘状态。Sharpdew的程序中使用的是long中的bit,非常精巧,但是也非常难懂(所以,你可以看到sharpdew的程序里有很多注释,即使如此,你也得非常小心和认真地看才能弄懂)。我想用简单一些的表示方法,所以我用了一个int数组,每个元素表示棋盘的一行中皇后所在的位置,行、列均从0起算。如a[0] = 0表示第一行的皇后在第一列,a[1] = 7则表示第二行的皇后在第八列。显然棋盘的每行只能有一个皇后,所以这种表示方法是可行。当然我们还要表示皇后的位置未确定的状态,我用-1来表示。
所以,QueenState的数据成员有:表示棋盘大小的n_,表示皇后位置的数组lines_。本来这已足够,但是为了便于我们查找和处理,我还增加了一个int来表示当前棋盘中已确定皇后位置的行数,命名为cur_line_。现在,QueenState的定义如下:
class QueenState
{
public:
QueenState(int n);
void nextStep(vector<QueenState>& vq) const;
bool isTarget() const;
private:
int n_, cur_line_;
int lines_[MAXQUEENNUMBER];
};
我们来从简单的开始写,首先是构造函数,它很简单,对三个数据成员进行初始化就是了。n_是传进来的,cur_line_初始化为0,数组中的所有元素初始化为-1,如下:
QueenState(int n) : n_(n), cur_line_(0)
{
fill_n(lines_, n_, -1);
}
接下来是isTarget(),它更简单,只要检查一下cur_line_看看是否所有行都已确定了皇后的位置。如下:
bool isTarget() const
{
return cur_line_ == n_;
}
最后是nextStep(),它稍微复杂一些。我们要根据前cur_line_行中的皇后位置来确定第cur_line_+1行中可以放置皇后的位置。方法是,从第一行开始,遍历所有已确定了皇后位置的行,每迭代一行,就根据该行的皇后位置排除掉第cur_line_+1行中那些冲突的列(包括直线和斜线冲突)。遍历结束后,剩下的未被排除的位置就是可以放置皇后的位置。我们根据这些位置逐个生成状态空间树的下一层结点,就可返回了。我们使用一个bool数组来记录某个位置是否被排除。
void QueenState::nextStep(vector<QueenState>& vq) const
{
QueenState newState(n_);
bool pos[MAXQUEENNUMBER];
fill_n(pos, n_, true);
for (int i = 0; i < cur_line_; ++i)
{
pos[lines_[i]] = false; //
与已有皇后不能同列
int j = lines_[i] + (cur_line_ - i); //
不能同一斜线
if (j < n_)
{
pos[j] = false;
}
j = lines_[i] - (cur_line_ - i);
if (j >= 0)
{
pos[j] = false;
}
}
for (int i = 0; i < n_; ++i)
{
if (pos[i])
{
newState = *this;
newState.lines_[cur_line_] = i; //
下一行皇后的一个可能位置
++newState.cur_line_; //
已确定行数加一
vq.push_back(newState);
}
}
}
QueenState就是这么多了,接下来是那个DFS/BFS要求的结果处理函数。我在前面说了,我不打算对搜索到的结果进行任何处理,只是简单累计一下结果的数量,而这一点DFS/BFS已经代劳了。所以,我们只要简单地返回一个false来要求DFS/BFS继续搜索就行了。
bool continueSearch(const QueenState&)
{
return false;
}
最后就是主程序了,我们要做的就是,从命令行读入棋盘的大小,生成一个初始化棋盘状态,然后调用DFS/BFS函数模板,取回调用结果并显示,完了。
int main(int argc, char *argv[])
{
if (argc < 2)
{
cerr << "Usage: queen <Queen_number>" << endl;
return 1;
}
int n = atoi(argv[1]);
if (n < MINQUEENNUMBER || n > MAXQUEENNUMBER)
{
cerr << "Queen_number too large or too small" << endl;
return 1;
}
cout << "Queen_number = " << n << endl;
QueenState initState(n);
int total = DepthFirstSearch(initState, &continueSearch);
cout << "Total = " << total << endl;
return 0;
}
从前面的分析可以看出,皇后问题的搜索是从第一行开始,一行一行向下的,状态空间树中的每一层结点都比上一层结点多了一行确定的皇后位置,所以不存在搜索过程中状态重复的情况。这一点与数独问题的解法一样,所以我们的DFS/BFS可以正常运行。下一次,我们将面对可能出现结点状态重复的情况,这需要对目前这个简单的DFS/BFS算法进行一定的改进。