用STL实现先深搜索及先宽搜索算法

用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算法。这个话题就留到下一次再写吧。
 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值