《啊哈算法》——搜索

  这篇文章我们将通过一些实例来初步理解两种搜索算法: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("\n");
            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;
}

 

posted on 2016-05-23 15:05  在苏州的城边  阅读(358)  评论(0编辑  收藏  举报

导航