飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 4208|回复: 4

《算法三部曲》 之 搜索算法(下篇)——首发于飘云阁

[复制链接]

该用户从未签到

发表于 2011-2-10 10:35:44 | 显示全部楼层 |阅读模式
本帖最后由 Knuth学徒 于 2011-3-26 18:10 编辑

注:此贴首发于飘云阁
Content 目录:
搜索算法 (上篇):https://www.chinapyg.com/viewthread.php?tid=61759
搜索算法 (中篇):https://www.chinapyg.com/viewthread.php?tid=61783


      今天终于要完成第一部曲了,有点感慨,有点激动,看到了(上中)两篇在热门帖子里均排在前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 = ' ' 表示空地

  1. #include<cstdio>
  2. #include<queue>
  3. #include<cstring>
  4. #include<cstdlib>
  5. using namespace std;

  6. #define  N 8
  7. #define  M 8

  8. bool  hash[N][M];
  9. char  map[N][M];

  10. struct Node{
  11.         int x,y;
  12.         int step;
  13. };
  14. int  EndX,EndY,StartX,StartY;

  15. queue<Node> q;  //实现广搜的队列

  16. bool  is_end(int x,int y)   //判断是否到终点
  17. {
  18.         return ((x == EndX) && (y == EndY));
  19. }

  20. const int direction[4][2] = {(0,-1) , (0, 1) , (-1, 0), (1, 0 )}
  21. // 四个方向的数组,分别为"上下左右"

  22. bool  is_legal( int x , int y )  //判断位置是否越界或者是否是墙壁
  23. {
  24.       if( x < 0 || x >= N || y < 0 || y >= M) //越界
  25.       {
  26.              return false;
  27.       }
  28.       else if( map[x][y] == '#' ){   //墙壁
  29.              return false;
  30.       }
  31.       else
  32.       {
  33.              return true;
  34.       }
  35. }

  36. void input(){      //输入迷宫地图
  37.        for(int i = 0; i < N ; ++i ){
  38.             for(int j = 0; j < M ; ++j){
  39.                  scanf("%c", &map[i][j]);
  40.             }
  41.        }
  42.        printf("再输入起点坐标\n");
  43.        scanf("%d %d", &StartX,&StartY);
  44.        printf("再输入终点坐标\n");
  45.        scanf("%d %d",&EndX, &EndY);
  46. }

  47. int  Breadth_First_Search( int x, int y)
  48. {
  49.        while(!q.empty()) q.pop();   // 初始化清空队列
  50.        Node node;                          //根节点
  51.        node.x = x; node.y = y;
  52.        node.step = 0;                     //深度为0
  53.        hash[x][y] = true;
  54.        q.push_back(node);          //根节点压入队尾
  55.        while(!q.empty())         //若队列非空
  56.        {
  57.                Node tmp;
  58.                tmp = q.front();  //取出队首元素
  59.                q.pop();
  60.                if(is_end(tmp.x,tmp.y)) //如果到了终点
  61.                {
  62.                         return tmp.step;
  63.                }
  64.                for(int k=0;k<4;++k)
  65.               //扩展子节点——四个方向
  66.                {
  67.                         int tmpX = tmp.x + direction[k][0];
  68.                         int tmpY = tmp.y + direction[k][1];

  69.                         if(is_legal( tmpX , tmpY ) && !hash[tmpX][tmpY])
  70.                         //如果该点合法并且没有被访问过
  71.                         {
  72.                                  hash[tmpX][tmpY] = true;
  73.                                  Node temp;
  74.                                  temp.x = tmpX;
  75.                                  temp.y = tmpY;
  76.                                  temp.step = tmp.step + 1;
  77.                                  q.push_back(temp);   //将该点压入队列
  78.                          }
  79.                 }
  80.         }
  81.         return  -1;      //没有解
  82. }

  83. int main()
  84. {
  85.          input();
  86.          memset(hash,false,sizeof(hash));
  87.          int result;
  88.          result = Breadth_First_Search(StartX,StartY);
  89.          if(result >= 0)
  90.          {
  91.                   printf("最少需要 %d 步到达终点\n", result);
  92.          }
  93.          else
  94.          {
  95.                   printf("起点和终点不连通\n");
  96.          }
  97.          return 0;
  98. }
复制代码
以上代码就是根据广搜的定义和迷宫的具体结构写出来的,如果要理解 "逐层展开"的含义,建议将上面代码中的 Breadth_First_Search() 函数弄懂。

       到这里,本来是应该结束了,不过我突然想起来,记得 Nisy  老师曾经在群里 提过一个面试题,当时好像群里没什么人给出解答,这道题就是经典的广搜。

       那我把 当时的题目的大意重新叙述下,如果记忆有差错还是请 Nisy老师来指正:

      
        给你一个中国象棋的棋盘,给你一个起点,再给你一个终点,问你:如果在棋盘上的起点放一个"马”,按照跳马步的方式至少要几步才能跳到终点。


       大致意思就是这样,其实这道面试题说难不难,说简单也不简单,如果以前没有看过我写的这篇搜索算法,也许就会被难倒。  但是,自从我们接触了广度优先搜索以后,发现这题和迷宫问题 有异曲同工之妙,我们只需将大致框架稍微做些修改就可以了。

       首先是搜索树的修改:
       根节点: 起点
       根节点的子节点:从起点按照马步的跳法能够跳到的位置

       任意节点:该节点表示的位置
       任意节点的子节点:从该节点按照马步的跳法能够跳到的位置集合

       然后再按照广搜的实现方式就能解决了。

       不过这里有个问题,假设终点位置在当前位置的右上角,那么我们此时还需不需要往左下角搜索呢?

       由于我们要求的最小步数,如果往反方向跳的话,必然会增加步数,所以我们这里可以针对具体情况对这个算法进行优化——就是所谓的“搜索剪枝”

       剪枝方法:
       终点坐标(EndX, EndY) ,当前位置坐标( CurX, CurY )
       在扩展的8个子节点中,至少有6个可以被删去。

       那么如何判断哪6个方向可以被去掉呢?   就是所谓的“反方向跳”或者更准确的说是“偏离正方向的位置”

       根据 数学中的解析几何的知识:(PS:这里体会到高中的数学知识不是白学的了吧?   其实算法的本质就是数学,要想做一个好的程序师,数学功底是必须的。)
                    
               方向向量n的终点 如果在 矩形( (EndX,EndY) , (CurX,EndY) , (CurX, CurY) , (EndX, CurY) ) 之外,就是偏离了正确方向,需要剪去。

               判断一个点是否在矩形内的方法是: 该点的横坐标的范围在 矩形的两竖直边内  且  该点的纵坐标的范围在矩形的两水平边内。

       接下来就可以提高相当高的效率了,不知道多少朋友想到了这个剪枝呢?

       但是别高兴太早, 也许面试官可能更加的挑剔,需要你再剪枝,继续提高效率,那么这时候我们想想还有哪里会浪费效率的呢?

       我们假设 起点和终点 的位置距离很远,这时候如果就算进行了“偏离方向”的剪枝,也许还会有非常多的无用节点产生,那么我想到了一个办法是:

                先固定让马从起点 按照终点的方向 日字形 跳跃若干步,直到 离开终点位置不远了。
                那么什么叫“不远了”,我们这里可以自己设置一个阀值,比如离开在终点位置的半径为2的圆内。
                这时候只要当前位置 在这个范围圆外,我们就可以按照一个位置进行跳动,直到了这个圆内,再按照普通的广搜来搜索。

       可以惊奇的发现,在圆外时,扩展的效率竟然是不加任何剪枝的广搜的 8倍, 是 加了“偏离方向”剪枝的广搜的 2倍!

       由此不得不感叹剪枝的重要性。    算法就在于优化,在于如何用最小的时空复杂度来完成最大规模的问题。

       我想,分析到这里,这道面试题就可以算是被我们完虐了。

///////////////////////////后记/////////////////////////////////////
       最后: 算法三部曲的第一部曲 终于完成了,当然了,第一部曲也是最简单的。

       感谢阅读,感谢关注。

       转载请注明出处。
////////////////////////////////////////////////////////////////////
PYG19周年生日快乐!

该用户从未签到

 楼主| 发表于 2011-2-10 10:39:05 | 显示全部楼层
自己顶一个
PYG19周年生日快乐!

该用户从未签到

发表于 2011-2-15 10:30:15 | 显示全部楼层
很强大!!!
PYG19周年生日快乐!
  • TA的每日心情
    开心
    2016-6-1 22:50
  • 签到天数: 1 天

    [LV.1]初来乍到

    发表于 2014-12-22 20:14:01 | 显示全部楼层
    好好学习天天向上
    PYG19周年生日快乐!
  • TA的每日心情
    无聊
    2016-5-16 19:11
  • 签到天数: 4 天

    [LV.2]偶尔看看I

    发表于 2016-5-16 23:21:59 | 显示全部楼层
    提示: 作者被禁止或删除 内容自动屏蔽
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

    快速回复 返回顶部 返回列表