zoukankan      html  css  js  c++  java
  • 《啊哈算法》——搜索

      这篇文章我们将通过一些实例来初步理解两种搜索算法:dfs、bfs。

      按照惯例,我们依然首先给出一个具体问题来导入:给出一个n x m的矩阵迷宫map,人在迷宫上的某个点map[i][j],可以上下左右移动,但是一些点标记为不可经过。那么现在给出起点和终点的矩阵下的坐标,我们能否找到一条起点到终点的路径?需要移动多少步?

      深度优先搜索:

      我们先通过之前一个我们曾经见到过的较为简单的“生成全排列”问题来初步认识一下dfs的思想。

      我们将问题更加的形象化,即我们假设生成全排列是一个将1~n个数字放到A~N个桶里的过程,这里以n=4为例吧,假设一次我们放数字的过程得到了2143这个全排列,在将3放入D盒中,我们期望能够继续生成其余的全排列,于是我们将3从D中拿出来,看有没有其余选择,发现并没有得到另外一种全排列的可能性,那么我们再将4从C盒中取出,这时,我们便可以将3放入C中,4放入D中来得到一个新的全排列。

      上面其实描述了一个简单的回溯过程,我们更加抽象的来理解dfs过程,即它将一个问题分成n个步骤,第i个步骤有a[i]个选择,那么我们在完成第i个步骤时,先任选a[i]中的一个,随后开始第i+1个步骤,一直到第n个步骤,我们枚举玩完第n个步骤的a[n]个方法后,开始向上回溯,即我们在进行第n-1个步骤有a[n-1]-1个没有遍历到的方法当中的一个,然后再进行第n个步骤,枚举a[n]中方法,很显然,这种算法能够搜索到完成这个问题所采取的n个步骤的不同方法组合的所有情况。

      那么针对生成全排列,我们尝试用dfs的思路进行重写。

      简单的参考代码如下。

    #include<cstdio>
    using namespace std;
    int a[10] , book[10] , n;
    
    void dfs(int step)//给第step箱子填充数字
    {
         int i;
         if(step == n + 1)//生成了一种全排列,输出结果
         {
             for(i = 1;i <= n;i++)
                  printf("%d",a[i]);
    
             printf("
    ");
                return;
         }
    
          for(i = 1;i <= n;i++)//当前情况的方法
          {
                if(book[i] == 0)
                {
                      a[step] = i;//选择一种方法
                      book[i] = 1;//标记这个数字已经使用过
    
                      dfs(step + 1);//进行深度优先,即开始给第step + 1个箱子填数
                      book[i] = 0;//回溯到填充第step箱子的步骤,清除标记
                }
          }
            return;
    
    }
    
    int main()
    {
         scanf("%d",&n);
         dfs(1);
    }

      那么基于这层铺垫,我们来看一看如何用dfs处理我们在文章一开始提出的问题。

      我们从起点开始,每一步其实有四个选择,即上、下、左、右,当然这里排除走出边界的、有障碍物的情况,假设这里我们从map[sx][sy]开始,(map是用于储存图的邻接矩阵)每当走了一步,假设我们向右走了一步,到达map[i][j+1],我们深度优先,继续从map[i][j+1]开始继续往下走...直到走到某个点map[i'][j'],发现四处无路可走,那么便开始回溯到路径中上一个点,去走那些没有走过的方向。这里需要注意的一个问题是,我们从map[i][j]走到了map[i][j+1],那么再从map[i][j+1]开始走的时候,会面临又回到map[i][j]的情况,这里只需在深搜的时候标记已经走过的点便可以轻松处理了。

      简单的参考代码如下。

    #include<cstdio>
    using namespace std;
    int n,m,p,q,Min = 9999999;
    int a[51][51],book[51][51];
    void dfs(int x,int y,int step)
    {
         int next[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};
         int tx,ty,k;
         if(x == p && y == q)
         {
              if(step < Min)
                   Min = step;
              return;
         }
    
         for(k=0;k<=3;k++)//四个方向
         {
             tx=x+next[k][0];
             ty=y+next[k][1];
    
             if(tx < 1 || tx>n ||ty<1||ty>m)//越界
                  continue;
             if(a[tx][ty]==0&&book[tx][ty] == 0)
             {
                  book[tx][ty] = 1;  //标记访问过
                  dfs(tx,ty,step+1);//深度优先的搜索
                  book[tx][ty] = 0;
             }
         }
    
         return;
    }
    
    int main()
    {
         int i , j , startx , starty;
         scanf("%d%d",&n,&m);
         for(i = 1;i <= n;i++)
              for(j=1;j<=m;j++)
                scanf("%d",&a[i][j]);
    
         scanf("%d %d %d %d",&startx,&starty,&p,&q);
    
         book[startx][starty] = 1;
         dfs(startx,starty,0);
    
         printf("%d",Min);
    }

      宽度优先搜索:

      其实通过前面对深度优先搜索的介绍,这里我们对比来看就很容易理解宽度优先搜索的思路。

      简单的说,假设我们完成搜索需要进行n个步骤,记第i个步骤有a[i]个方法。

      深搜给出的思路是,先使用a[i]给出的完成第i个步骤的一个方法,然后紧接着去完成第i+1个步骤,对于那些没有用到的方法,深搜在搜到底部之后没有其余方法后,会回溯回来再重新a[i]中的其余方法,以此来构造出新的方法。

      而宽搜给出的思路是,完成第i个步骤,即将a[i]种方法全部列出来,这显示出当前(进行i个步骤)的所有情况,然后进行第i+1个步骤,采用相同的策略。一直到第n个步骤,宽搜将列举出所有的可能情况,然后完成搜索。

      那么我们现在面临的一个问题,如何编码来实现宽搜的这一系列操作呢?我们可以看到,我们在完成对第i个步骤的a[i]种情况的列举之后,之前i-1个步骤所出现的情况似乎已经与我们没有关系了,因为我们在进行第i+1个步骤的时候,仅仅需要基于完成第i个步骤后所有结果即可。想一想,以这种尾部添加元素,头部删除元素的操作......对,就是我们在前面文章曾经介绍过的队列结构。

      基于这种宽搜过程,我们只需判断第j个步骤之后,是否出现我们想要的结果。可以看到,每宽搜一次,都要删除头部元素,可能添加尾部元素。而当头部元素一直删除而尾部却没有添加多少,即队列为空的时候,依然没有出现我们想要的结果,那么这显然表明我们在图中找不到这样一个从起点到达终点的路径。

      基于上面的算法分析,我们进行简单的编程实现。

    #include<cstdio>
    using namespace std;
    
    struct note
    {
        int x;
        int y;
        int f;
        int s;
    };
    
    int main()
    {
         struct note que[2501];
         int a[51][51] = {0},book[51][51] = {0};
    
         int next[4][2] = {{0,1},     //四个方向
                           {1,0},
                           {0,-1},
                           {-1,0}};
         int head , tail;
         int i , j , k , n , m , startx , starty , p , q , tx , ty , flag;
    
         scanf("%d %d",&n,&m);
         for(i = 1;i <= n;i++)//读入map
              for(j = 1;j <= m;j++)
                  scanf("%d",&a[i][j]);
    
         scanf("%d%d%d%d",&startx,&starty,&p,&q);
    
          head = 1;
          tail = 1;//初始化队列
          que[tail].x = startx;
          que[tail].y = starty;
          que[tail].f = 0;
          que[tail].s = 0;
           tail++;
           book[startx][starty] = 1;
          while(head < tail) //bfd算法核心部分
          {
                 for(k = 0;k <= 3;k++)
                 {
                     tx = que[head].x + next[k][0];
                     ty = que[head].y + next[k][1];
    
                     if(tx < 1 || tx > n || ty < 1 || ty > m)
                           continue;
                     if(a[tx][ty] == 0 && book[tx][ty] == 0)//入队操作
                     {
                          book[i][j] = 1;
                          que[tail].x = tx;
                          que[tail].y = ty;
                          que[tail].f = head;
                          que[tail].s = que[head].s + 1;
                          tail++;
                     }
                   if(tx == p && ty == q)
                      {
                        flag = 1;
                        break;
                      }
                 }
    
    
    
               if(flag == 1) 
                   break;
               head++;   //弹出队首元素
          }
    
          printf("%d",que[tail-1].s);
    
          return 0;
    }
  • 相关阅读:
    洛谷 1339 最短路
    洛谷 1330 封锁阳光大学 图论 二分图染色
    洛谷 1262 间谍网络 Tarjan 图论
    洛谷 1373 dp 小a和uim之大逃离 良心题解
    洛谷 1972 莫队
    洛谷 2158 数论 打表 欧拉函数
    洛谷 1414 数论 分解因数 水题
    蒟蒻的省选复习(不如说是noip普及组复习)————连载中
    关于筛法
    关于整数划分的几类问题
  • 原文地址:https://www.cnblogs.com/rhythmic/p/5520036.html
Copyright © 2011-2022 走看看