用STL实现先深搜索及先宽搜索
先深搜索
和先宽搜索算法是对问题状态空间树(state space tree)进行搜索的两种方法。问题状态空间树是指用树的结构来表示所有的问题状态,树中的每一个结点确定了所求解问题的一个问题状态(problem state);树中有些结点代表的是答案状态(answer state),即是那些满足求解条件的状态。要从一棵问题状态空间树中找出满足求解条件的答案状态结点,办法有两种:先深搜索法DFS(depth first search)
和先宽搜索法BFS(breadth first search)。
两种搜索方法都可以确保找到问题的答案(如果有的话),其主要差异在于采用不同的搜索策略,从而对于不同的问题,两种搜索方法的效率可能有所不同。另外,如果要求找出最低代价(如最少的步数)的解法,则通常只能使用BFS。不过这些都不是这篇文章所关心的,大家如果有兴趣,可以找找相关的算法书籍。
这里要讨论的是,如何使用Template和STL来实现一个较为通用(当然也还不是万能的)DFS和BFS。
首先,我们要关注的是问题状态空间树,通常这棵
树非常之大,结点非常之多,建立一棵完整的状态空间树通常需要耗费大量的时间,甚至会远远超出从这棵树中搜索出答案的时间。试想一下,如果我们可以用程序建立出这么一棵完整的状态空间树,那么我们只要在建立的时候稍微多做一点事情,即在生成每个结点时检查一下,不就可以轻而易举地找出所有答案了吗?所以,先将整棵状态空间树建立起来,再进行搜索的方法是行不通的。
更为合理的方法是:边建立状态空间树,边进行搜索。我的想法是:用一个容器来表示状态空间树,用算法逐步生成树的下一层结点,每生成一个结点就放入容器中;同时,每次从容器中取出一个结点检查其是否满足条件,如果是则表示找到一个答案状态结点,否则就以该结点为输入生成下一步的状态结点。开始时,容器中只有一个结点,既代表初始问题的状态结点,以此为搜索算法的起点进行迭代来找答案,根据问题的不同要求,我们可以在找到一个答案后停止搜索,也可以继续搜索以找出所有答案。
如果用容器来表示状态空间树,DFS和BFS对容器有什么不同的要求呢?我们来看一下图示:
假如我们有初始问题状态A,从A可以生成下一层(或者说下一步)的状态B、C、D,同样从B可以生成下一层的状态E、F、G。对于DFS,我们的搜索顺序应该是A、B、E…,结合我们前面所说的边生成树边搜索的方法,这些状态的生成次序应该是A、B、C、D、E、F、G…,所以用
栈(stack)
来作为存放状态树的容器就最合适了。我们把初始状态A入
栈,然后按以下方法进行迭代:从栈中取出一个结点(状态),根据该状态生成下一层结点(所有可能的下一步状态),逐一压入
栈中(同一层的结点入
栈的顺序不太要紧,为了易于表述,我们假设以D、C、B的顺序入
栈);如此类推,取出B,生成E、F、G入
栈。对于状态是否满足解答条件的判断可以在入栈前或出栈后进行,当然出于效率的考虑,我们建议在入栈前进行判断,这样符合条件的答案状态就可以不必入栈了。
我们再来看看BFS,正确的搜索顺序应该是A、B、C、D、E…,这个顺序与状态的生成顺序完全相同,所以这次应该用队列(queue)
来作为存放状态树的容器了。我们把初始状态A放入队列,然后按以下方法进行迭代:从队列中取出一个结点(状态),根据该状态生成下一层结点(所有可能的下一步状态),逐一放入队列中;如此类推,取出B,生成E、F、G放入队列。同样,我们在把状态放入队列前进行是否满足解答条件的判断,这样符合条件的答案状态就可以不必放入队列了。
通过以上的分析,我们已经有了算法的大概轮廓了。但我们还缺少三样东西,一个是如何从一个状态生成下一步状态,另一个是如何判断是否得到了答案状态,还有一个就是得到答案状态后如何办。显然,这些东西都是与具体问题相关的,最好的办法就是把它们作为模板参数,这样的话我们的算法就可以有最广泛的适用范围了。
那么,我们是否需要三个模板参数呢?个人认为,前两样东西属于如何构造一个具体问题的解法,而最后一样东西则是
指找到答案后的处理方法。所以,我把前两者封装成一个类(对应于状态空间树中的结点,该结点知道如何生成下一层结点,也知道自己是否满足解答条件),而把后者实现成一个函数对象。
#include <stack>
#include <vector>
using
std::stack;
using
std::vector;
template
<class T1, class T2>
int
DepthFirstSearch(const T1& initState, const T2& afterFindSolution)
// initState :
初始化状态,类T1应提供成员函数nextStep()和isTarget(),
// nextStep(vector<T1>&)
用于返回下一步可能的所有状态,
// isTarget()
用于判断当前状态是否符合要求的答案;
// afterFindSolution :
仿函式
,在找到一个有效答案后调用之,它接受一个const T1&,
//
并返回一个Boolean值,true表示停止搜索,false表示继续找
// return :
找到的答案数量
{
int n = 0;
stack<T1> states;
states.push(initState);
vector<T1> nextStates;
bool stop = false;
while (!stop && !states.empty())
{
T1 s = states.top();
states.pop();
nextStates.clear();
s.nextStep(nextStates);
for (typename vector<T1>::iterator i = nextStates.begin();
i != nextStates.end(); ++i)
{
if (i->isTarget())
{ //
找到一个目标状态
++n;
if (afterFindSolution(*i)) //
处理结果并决定是否停止搜索
{
stop = true;
break;
}
} else { //
不是目标状态,放入搜索队列中
states.push(*i);
}
}
}
return n;
}
程序比较简单,相信大家能够看懂。关键有以下几点:
1. 类T1由算法使用者提供,它必须具有可以表示问题状态的数据成员,并提供两个成员函数:void n
extStep(vector<T1>&)和
boolean
isTarget();前者以自身状态为起点,返回所有可能的下一步状态,由于可能的下一步状态数量不定,所以需要用vector<T1>&来返回;后一个成员函数则比较简单,返回一个
boolean值来判断自身状态是否满足解答条件。
2. 类T2为函数指针类型或函数对象类型,该函数接受一个const T1&参数,并返回一个
boolean值,传入的参数即为搜索算法找到的一个答案状态,函数可以按自己的方法处理它(如打印到终端,或写入到文件等),然后返回一个
boolean值来表示是否继续搜索其它答案(有一些问题只要求找到一个答案即可,而另一些问题则要求找出所有答案)。
3. stack<T1> states就是用于存入状态空间树的
栈容器,就象我们前面所分析的那样,使用栈容器可以很好地模拟出先深搜索DFS。
看过了DFS,相信大家都知道BFS也会和DFS差不了多少。以下是我的代码:
#include <queue>
#include <vector>
using
std::queue;
using
std::vector;
template
<class T1, class T2>
int
BreadthFirstSearch(const T1& initState, const T2& afterFindSolution)
// initState :
初始化状态,类T1应提供成员函数nextStep()和isTarget(),
// nextStep()
用vector<T1>返回下一步可能的所有状态,
// isTarget()
用于判断当前状态是否符合要求的答案;
// afterFindSolution :
仿函式
,在找到一个有效答案后调用之,它接受一个const T1&,
//
并返回一个Boolean值,true表示停止搜索,false表示继续找
// return :
找到的答案数量
{
int n = 0;
queue<T1> states;
states.push(initState);
vector<T1> nextStates;
bool stop = false;
while (!stop && !states.empty())
{
T1 s = states.front();
states.pop();
nextStates.clear();
s.nextStep(nextStates);
for (typename vector<T1>::iterator i = nextStates.begin();
i != nextStates.end(); ++i)
{
if (i->isTarget())
{ //
找到一个目标状态
++n;
if (afterFindSolution(*i)) //
处理结果并决定是否停止搜索
{
stop = true;
break;
}
} else { //
不是目标状态,放入搜索队列中
states.push(*i);
}
}
}
return n;
}
它和DFS几乎一模一样,除了把stack换成了queue,把top()换成了front(),所以我想也不用再作解释了吧。
为了检查算法的有效性,我选了前段时间很热门的一个游戏——
数独
sudoku,实
作出一个简单的解法,测试了一下这两个DFS和BFS算法。这个话题就留到下一次再写吧。