| 
注册时间2011-2-7
阅读权限10
最后登录1970-1-1UID72137 周游历练 
 
 该用户从未签到 | 
 
| 本帖最后由 Knuth学徒 于 2011-3-26 18:10 编辑 
 注:此贴首发于飘云阁
 
 
 今天终于要完成第一部曲了,有点感慨,有点激动,看到了(上中)两篇在热门帖子里均排在前2名,我很欣慰。    也许很多朋友们以前不注重基本的算法,也许他们本来认为程序只是某种语言的架构,但是我希望我的 算法三部曲成为热门帖之后会有更多人看到它,认识到原来算法那么具有逻辑性,具有指导性,那么我的努力就没有白费。
 
 搜索算法本来作为一个很基础的算法,但是其中蕴含的思想却是可以细细体味的。 今天,我就深入讨论下 广度优先搜索 以及它的应用。
 
 中篇里我已经稍微介绍了广搜的概念——围绕根节点逐层展开,我也抽象了一棵搜索树来解决迷宫问题。  但是具体的实现方式我还没有讲,那么,我现在来介绍下广搜是如何实现的,以及他和深搜最本质的区别。
 
 广度优先搜索: 在搜索树中以层次遍历 方式遍历整棵树
 
 如果知道层次遍历的话,那么就不需要细讲了,如果不知道的话,我可以简要的介绍下:,所谓层次遍历就是指:
 
 先访问第一层的根节点,然后再从左至右逐个访问 第二层的子节点,然后再从左往右逐个访问 第三层的子节点,……逐层深入,直到找到目标节点或者遍历完整棵树。
 
 那么,我们如何按照层次展开呢?     可以发现在访问的过程中,我们假设先访问 第n层的第1个节点,记为A节点,那么我们必然也是最先访问 第n+1层的A节点的子节点,同样的,如果我们在A节点后访问到 第n层的第3个节点,记为B节点,那么我们必然也是在第n+1层 在访问了A的子节点之后 访问了B的子节点。
 
 于是,一个很明显的 “先进先出” 关系便出现了。
 
 一说到“先进先出”,最先想到的就应该是“队列”——先压入队尾的元素将会先从队首弹出。
 
 你们还记得 队列的实现方式么?     如果学过数据结构的同学也许还有印象,如果没学过,也没关系,我关于队列写了详细的 C++类模板,具体参照:https://www.chinapyg.com/viewthr ... &extra=page%3D1
 
 现在,就用队列来实现广搜,步骤如下:
 
 首先把 搜索树的根节点 压入队尾—— queue.push_back( root )
 然后取出队头元素 ,此时就是根节点,访问之,然后将根节点的 k 个子节点按顺序压入队尾—— for(k个子节点 A_k){ queue.push_back( A_k );}
 然后再从队头取出元素,访问,然后再将它的 N 个子节点压入。。。。。。
 
 于是,解决走迷宫的最少步数 的算法就没问题了,接着我们需要一个良好的数据结构:
 
 用结构体: struct Node{
 int  x,y;  //表示该点的横纵坐标
 int step;  //表示该节点到根节点的深度——即是该点到起点所需的最短步数
 };
 
 用bool型的哈希数组 hash[N][M]来判重
 用char型的地图数组 map[N][M]存储迷宫     // map = '#' 表示墙壁,map = ' ' 表示空地
 以上代码就是根据广搜的定义和迷宫的具体结构写出来的,如果要理解 "逐层展开"的含义,建议将上面代码中的 Breadth_First_Search() 函数弄懂。复制代码
#include<cstdio>
#include<queue>
#include<cstring>
#include<cstdlib>
using namespace std;
#define  N 8
#define  M 8
bool  hash[N][M];
char  map[N][M];
struct Node{
        int x,y;
        int step;
};
int  EndX,EndY,StartX,StartY;
queue<Node> q;  //实现广搜的队列
bool  is_end(int x,int y)   //判断是否到终点
{
        return ((x == EndX) && (y == EndY));
}
const int direction[4][2] = {(0,-1) , (0, 1) , (-1, 0), (1, 0 )} 
// 四个方向的数组,分别为"上下左右"
bool  is_legal( int x , int y )  //判断位置是否越界或者是否是墙壁
{
      if( x < 0 || x >= N || y < 0 || y >= M) //越界
      {
             return false;
      }
      else if( map[x][y] == '#' ){   //墙壁
             return false;
      }
      else
      {
             return true;
      }
}
void input(){      //输入迷宫地图
       for(int i = 0; i < N ; ++i ){
            for(int j = 0; j < M ; ++j){
                 scanf("%c", &map[i][j]);
            }
       }
       printf("再输入起点坐标\n");
       scanf("%d %d", &StartX,&StartY);
       printf("再输入终点坐标\n");
       scanf("%d %d",&EndX, &EndY);
}
int  Breadth_First_Search( int x, int y)
{
       while(!q.empty()) q.pop();   // 初始化清空队列
       Node node;                          //根节点
       node.x = x; node.y = y;
       node.step = 0;                     //深度为0
       hash[x][y] = true;
       q.push_back(node);          //根节点压入队尾
       while(!q.empty())         //若队列非空
       {
               Node tmp;
               tmp = q.front();  //取出队首元素
               q.pop();
               if(is_end(tmp.x,tmp.y)) //如果到了终点
               {
                        return tmp.step;
               }
               for(int k=0;k<4;++k) 
              //扩展子节点——四个方向
               {
                        int tmpX = tmp.x + direction[k][0];
                        int tmpY = tmp.y + direction[k][1];
                        if(is_legal( tmpX , tmpY ) && !hash[tmpX][tmpY]) 
                        //如果该点合法并且没有被访问过
                        {
                                 hash[tmpX][tmpY] = true;
                                 Node temp;
                                 temp.x = tmpX; 
                                 temp.y = tmpY; 
                                 temp.step = tmp.step + 1;
                                 q.push_back(temp);   //将该点压入队列
                         }
                }
        }
        return  -1;      //没有解
}
int main()
{
         input();
         memset(hash,false,sizeof(hash));
         int result;
         result = Breadth_First_Search(StartX,StartY);
         if(result >= 0)
         {
                  printf("最少需要 %d 步到达终点\n", result);
         }
         else
         {
                  printf("起点和终点不连通\n");
         }
         return 0;
}
 到这里,本来是应该结束了,不过我突然想起来,记得 Nisy  老师曾经在群里 提过一个面试题,当时好像群里没什么人给出解答,这道题就是经典的广搜。
 
 那我把 当时的题目的大意重新叙述下,如果记忆有差错还是请 Nisy老师来指正:
 
 
         给你一个中国象棋的棋盘,给你一个起点,再给你一个终点,问你:如果在棋盘上的起点放一个"马”,按照跳马步的方式至少要几步才能跳到终点。
 大致意思就是这样,其实这道面试题说难不难,说简单也不简单,如果以前没有看过我写的这篇搜索算法,也许就会被难倒。  但是,自从我们接触了广度优先搜索以后,发现这题和迷宫问题 有异曲同工之妙,我们只需将大致框架稍微做些修改就可以了。
 
 首先是搜索树的修改:
 根节点: 起点
 根节点的子节点:从起点按照马步的跳法能够跳到的位置
 
 任意节点:该节点表示的位置
 任意节点的子节点:从该节点按照马步的跳法能够跳到的位置集合
 
 然后再按照广搜的实现方式就能解决了。
 
 不过这里有个问题,假设终点位置在当前位置的右上角,那么我们此时还需不需要往左下角搜索呢?
 
 由于我们要求的最小步数,如果往反方向跳的话,必然会增加步数,所以我们这里可以针对具体情况对这个算法进行优化——就是所谓的“搜索剪枝”
 
 剪枝方法:
 终点坐标(EndX, EndY) ,当前位置坐标( CurX, CurY )
 在扩展的8个子节点中,至少有6个可以被删去。
 
 那么如何判断哪6个方向可以被去掉呢?   就是所谓的“反方向跳”或者更准确的说是“偏离正方向的位置”
 
 根据 数学中的解析几何的知识:(PS:这里体会到高中的数学知识不是白学的了吧?   其实算法的本质就是数学,要想做一个好的程序师,数学功底是必须的。)
 
 方向向量n的终点 如果在 矩形( (EndX,EndY) , (CurX,EndY) , (CurX, CurY) , (EndX, CurY) ) 之外,就是偏离了正确方向,需要剪去。
 
 判断一个点是否在矩形内的方法是: 该点的横坐标的范围在 矩形的两竖直边内  且  该点的纵坐标的范围在矩形的两水平边内。
 
 接下来就可以提高相当高的效率了,不知道多少朋友想到了这个剪枝呢?
 
 但是别高兴太早, 也许面试官可能更加的挑剔,需要你再剪枝,继续提高效率,那么这时候我们想想还有哪里会浪费效率的呢?
 
 我们假设 起点和终点 的位置距离很远,这时候如果就算进行了“偏离方向”的剪枝,也许还会有非常多的无用节点产生,那么我想到了一个办法是:
 
 先固定让马从起点 按照终点的方向 日字形 跳跃若干步,直到 离开终点位置不远了。
 那么什么叫“不远了”,我们这里可以自己设置一个阀值,比如离开在终点位置的半径为2的圆内。
 这时候只要当前位置 在这个范围圆外,我们就可以按照一个位置进行跳动,直到了这个圆内,再按照普通的广搜来搜索。
 
 可以惊奇的发现,在圆外时,扩展的效率竟然是不加任何剪枝的广搜的 8倍, 是 加了“偏离方向”剪枝的广搜的 2倍!
 
 由此不得不感叹剪枝的重要性。    算法就在于优化,在于如何用最小的时空复杂度来完成最大规模的问题。
 
 我想,分析到这里,这道面试题就可以算是被我们完虐了。
 
 ///////////////////////////后记/////////////////////////////////////
 最后: 算法三部曲的第一部曲 终于完成了,当然了,第一部曲也是最简单的。
 
 感谢阅读,感谢关注。
 
 转载请注明出处。
 ////////////////////////////////////////////////////////////////////
 | 
 |