专栏
第四节——搜索
深度优先搜索
在第三节——枚举中我们留下了一个问题,输入一个数 n,输出 1~n 的全排列。这里我们先将这个问题形象化,举个例子。假如有编号为 1、2、3 的 3 个小球和编号为 1、2、3 的 3 个箱子。现在需要将这 3 个小球分别放到 3 个箱子里面,并且每一个箱子有且只能放一个小球。那么一共有多少种不同的方法呢?
好了,现在轮到小明出马。小明手拿 3 个小球,首先走到了 1 号箱子面前。此时小明心里想:我是先放 1 号小球,还是先放 2 号小球,还是先放 3 号小球呢?现在要生成的是全排列,很显然这三种情况都需要去尝试。小明说那我们约定一个顺序吧:每次到一个箱子面前时,都先放 1 号,再放 2 号,最后放 3 号小球。说完小明走到了 1 号箱子前,将 1 号小球放到第 1 个箱子中。
放好之后小明往后走一步,来到了 2 号 箱子面前。本来按照之前的约定的规则,每到一个新的箱子面前,要按照 1号、2号、3号小球的顺序来放。但是现在小明手中只剩下 2 号小球和 3 号小球了,于是小明将 2 号小球放入了 2 号盒子中。放好之后小明再往后走一步,来到了 3 号箱子面前。
现在小明已经来到了 3 号箱子面前,按照之前的约定,还是应该按照 1号、2号、3号小球的顺序来放。但是现在小明手中只有 3 号小球了,于是只能往 3 号箱子里面放了 3 号小球。放好后,小明再往后走一步,来到了 4 号箱子面前。咦!没有第 4 个箱子,其实我们并不需要第 4 个箱子,因为手中的小球已经放完了。
我们发现当小明走到第 4 个箱子的时候,已经完成了一种排列,这个排列就是前面 3 个箱子中的小球的号码,即“1 2 3”。
是不是到此就结束了呢?肯定没有!产生了一种排列之后小明需要立即返回。现在小明需要退一步重新回到 3 号箱子面前。
好!现在小明已经回到了 3 号箱子面前,需要取回之前放在 3 号箱子中的小球,再去尝试看看还能否放别的小球,从而产生一个新的排列。于是小明取回了 3 号小球。当小明再想往 3 号箱子放别的小球的时候,却发现手中仍然只有 3 号小球,没有别的选择。于是小明不得不再往回退一步,回到了 2 号箱子面前。
小明回到 2 号箱子后,收回了 2 号小球。现在小明手里面有两个小球了,分别是 2 号小球和 3 号小球。按照之前约定的顺序,现在需要往 2 号 箱子中放 3 号小球(上一次放的是 2 号小球)。放好之后小明又向后走一步,再次来到了 3 号箱子面前。
小明再次来到 3 号箱子后,将手中仅剩的 2 号小球放入了 3 号箱子。又来到 4 号箱子面前。当然了,这里并没有 4 号箱子。此时又产生了一个新的排列“1 3 2”。
接下来按照刚才的步骤去模拟,便会依次生成所有排列:“2 1 3”、“2 3 1”、“3 1 2”和“3 2 1”。
说了半天,这么复杂的过程如何用程序实现呢?我们现在来解决最基本的问题:如何往箱子里面放小球。每一个箱子都可能放 1 号、2 号或者 3 号小球,这需要一一去尝试,这里一个 for 循环就可以解决。
for(i=1;i<=n;i++)
{
a[step]=i; //将i号小球放入到第step个箱子中
}
这里数组 a 是用来表示箱子的,变量 step 表示当前正处在第 step 个箱子面前。a[step]=i; 就是将第 i 号小球放入到第 step 个箱子中。这里有一个问题那就是,如果一个小球已经放到别的箱子中了,那么此时就不能再放入同样的小球到当前箱子中,因为此时手中已经没有这个小球了。因此还需要一个数组 book 来标记哪些牌已经使用了。
for(i=1;i<=n;i++)
{
if(book[i]==0) //book[i]等于0表示i号小球仍然在手上
{
a[step]=i; //将i号小球放入到第step个箱子中
book[i]=1; //将book[i]设为1,表示i号小球已经不在手上
}
}
OK,现在已经处理完第 step 个箱子了,接下来需要往下走一步,继续处理第 step+1 个箱子。那么如何处理第 step+1个箱子呢?处理方法其实和我们刚刚处理第 step 个箱子的方法是相同的。因此很容易就想到把刚才的处理第 step 个箱子的代码封装成一个函数,我们为这个函数起个名字,就叫做 dfs 吧,如下。
void dfs(int step)//step表示现在站在第几个箱子面前
{
for(i=1;i<=n;i++)
{
//判断小球i是否还在手上
if(book[i]==0) //book[i]等于0表示i号小球仍然在手上
{
a[step]=i; //将i号小球放入到第step个箱子中
book[i]=1; //将book[i]设为1,表示i号小球已经不在手上
}
}
}
把这个过程写成函数后,刚才的问题就好办了。在处理完第 step 个箱子之后,紧接着处理第 step+1 个箱子,处理第 step+1 和箱子的方法就是 dfs(step+1)。
void dfs(int step)//step表示现在站在第几个箱子面前
{
for(i=1;i<=n;i++)
{
//判断小球i是否还在手上
if(book[i]==0) //book[i]等于0表示i号小球仍然在手上
{
a[step]=i; //将i号小球放入到第step个箱子中
book[i]=1; //将book[i]设为1,表示i号小球已经不在手上
dfs(step+1); //这里通过函数的递归调用来实现(自己调用自己)
book[i]=0; //这是非常重要的一步,一定要将刚才尝试的小球收回,才能进行下一次尝试
}
}
}
上面的代码中的 book[i]=0 这条语句非常重要,这句话的作用是将箱子中的小球收回,因为在一次摆放尝试结束返回的时候,如果不把刚才放入箱子的小球收回,那将无法再进行下一次摆放。还剩下一个问题,就是什么时候该输出一个满足要求的序列呢。其实当我们处理到第 n+1 个箱子的时候(即 step 等于n+1),那么说明前 n 个箱子都已经放好小球了,这里将 1~n 个箱子中的小球的编号打印出来就可以了,如下。注意!打印完毕要一定 return,不然这个程序就会永无止境地运行下去了,想一想为什么吧。
void dfs(int step)//step表示现在站在第几个箱子面前
{
if(step==n+1)//如果站在第n+1个箱子面前,则表示前n个箱子已经放好小球
{
//输出一种排列(1~n号箱子中的小球的编号)
for(i=1;i<=n;i++)
printf("%d",a[i]);
printf("\n");
return; //返回之前的一步(最近一次调用dfs函数的地方)
}
for(i=1;i<=n;i++)
{
//判断小球i是否还在手上
if(book[i]==0) //book[i]等于0表示i号小球仍然在手上
{
a[step]=i; //将i号小球放入到第step个箱子中
book[i]=1; //将book[i]设为1,表示i号小球已经不在手上
dfs(step+1); //这里通过函数的递归调用来实现(自己调用自己)
book[i]=0; //这是非常重要的一步,一定要将刚才尝试的小球收回,才能进行下一次尝试
}
}
}
完整代码如下。
#include <stdio.h>
int a[10],book[10],n;//此处特别说明一下:C语言的全局变量在没有赋值以前默认为0,因此这里的book数组无需全部再次赋初始值值0
void dfs(int step)//step表示现在站在第几个箱子面前
{
int i;
if(step==n+1)//如果站在n+1个盒子面前,则表示前n个箱子已经放好小球
{
//输出一种排列(1~n号箱子中的小球编号)
for(i=1;i<=n;i++)
printf("%d",a[i]);
printf("\n");
return;//返回之前的一步(最近一次调用dfs函数的地方)
}
//此时站在第step个箱子面前,应该放哪个小球呢?
//按照1、2、3...n的顺序一一尝试
for(i=1;i<=n;i++)
{
//判断扑克牌i是否还在手上
if(book[i]==0) //book[i]等于0表示i号小球在手上
{
//开始尝试使用小球i
a[step]=i; //将i号小球放入到第step个箱子中
book[i]=1; //将book[i]设为1,表示i号小球已经不在手上
//第step个箱子已经放好小球,接下来需要走到下一个箱子面前
dfs(step+1); //这里通过函数的递归调用来实现(自己调用自己)
book[i]=0; //这是非常重要的一步,一定要将刚才尝试的小球收回,才能进行下一次尝试
}
}
return;
}
int main()
{
scanf("%d",&n);//输入的时候要注意n为1~9之间的整数
dfs(1);//首先站在1号箱子面前
getchar();getchar();
return 0;
}
这个简单的例子,核心代码不超过20行,却饱含深度优先搜索(Depth First Search,DFS)的基本模型。理解深度优先搜索的关键在于解决“当下该如何做”。至于“下一步如何做”则与“当下该如何做”是一样的。比如我们在这里写的 dfs(step) 函数的主要功能就是解决当你再第 step 个箱子的时候你该怎么办。通常的方法就是把每一种可能都去尝试一遍(一般使用 for 循环来遍历)。当前这一步解决后便进入下一步 dfs(step+1)。下一步的解决方法和当前这布的解决方法是完全一样的。下面的代码就是深度优先搜索的基本模型。
void dfs(int step)
{
判断边界
尝试每一种可能 for(i=1;i<=n;i++)
{
继续下一步 dfs(step+1);
}
返回
}
每一种尝试就是一种“扩展”。每次站在一个箱子面前的时候,其实都有 n 种扩展方法,但是并不是每种扩展都能够成功。
好了,我想我们箱子应该可以用新学的算法重新解决上一节□□□+□□□=□□□这个问题了。
这相当于你手中有编号为 1~9 的九个小球,然后将这个九个小球放到9个箱子中,并使得□□□+□□□=□□□成立。其实就是判断一下 a[1] * 100+a[2] * 10+a[3]+a[4] * 100+a[5] * 10+a[6]==a[7] * 100+a[8] * 10+a[9] 这个等式是否成立。
#include <stdio.h>
int a[10],book[10],total=0;
void dfs(int step)//step表示现在站在第几个箱子面前
{
int i;
if(step==10)//如果站在第10个箱子面前,则表示前9个箱子已经放好小球
{
//判断是否满足不等式□□□+□□□=□□□
if(a[1]*100+a[2]*10+a[3]+a[4]*100+a[5]*10+a[6]==a[7]*100+a[8]*10+a[9])
{
//如果满足要求,可行解+1,并打印这个解
total++;
printf("%d%d%d+%d%d%d=%d%d%d\n",a[1],a[2],a[3],a[4],a[5],a[6],a[7],a[8],a[9]);
}
return; //返回之前的一步(最近调用的地方)
}
//此时站在第step个箱子面前,应该放哪个小球呢?
//按照1、2、3......n的顺序一一尝试
for(i=1;i<=9;i++)
{
//判断小球i是否还在手上
if(book[i]==0)// book[i]为0表示小球还在手上
{
//开始尝试使用小球i
a[step]=i;
book[i]=1;
//第step个盒子已经放置好小球,走到下一个箱子面前
dfs(step+1); //这里通过函数的递归调用来实现(自己调用自己)
//这里是非常重要的一步,一定要将刚才尝试的小球收回,才能进行下一次尝试
book[i]=0;
}
}
return;
}
int main()
{
dfs(1);//首先站在第一个箱子面前
printf("total=%d",total/2); //为什么要除以2之前已经说过
getchar();getchar();
return 0;
}
迷宫解救
有一天,小红一个人去玩迷宫。但是方向感很不好的小红很快就迷路了。小明得知后便要立即去解救无助的小红。小明当然是有备而来,已经弄清楚了迷宫的地图,现在小明要以最快的速度去解救小红。问题就此开始了······
迷宫由 n 行 m 列的单元格组成(n 和 m都小于等于 50),每个单元格要么是空地,要么是障碍物。你的任务是帮助小明找到一条从迷宫的起点同通往小红所在位置的最短路径。注意障碍物是不能走的,当然小明也不能走到迷宫之外。
首先我们可以用一个二维数组来存储这个迷宫,刚开始的时候,小明处于迷宫的入口处(1,1),小红在(p,q)。其实,就是找从(1,1)到(p,q)的最短路径。如果你是小明,你该怎么办呢?小明最开始在(1,1),他只能往右走或者往下走,但是小明是应该往右走呢还是往下走呢。此时要是能有两个小明就好了,一个向右走,另外一个向下走。但是现在只有一个小明,所以只能一个一个地去尝试。我们可以先让小明往右边走,直到走不通的时候再回到这里,再去尝试另外一个方向。我们这里规定一个顺序,按照顺时针的方向来尝试(即按照右、下、左、上的顺序去尝试)。
我们先来看看小明一步之内可以达到的点有哪些?只有(1,2)和(2,1)。根据刚才的策略,我们先往右边走,小明来到了(1,2)这个点。来到(1,2)之后小明又能到达哪些新的点呢?只有(2,2)这个点。因为(1,3)是障碍物无法达到,(1,1)是刚才来的路径中已经走过的点,也不能走,所以只能到(2,2)这个点。但是小红并不在(2,2)这个点上,所以小明还得继续往下走,直至无路可走或者找到小红为止。请注意!此处并不是一找到小红就结束了。因为刚才只尝试了一条路,而这条路并不一定是最短的。刚才很多地方在选择方向的时候都有多种选择,因此我们需要返回到这些地方继续尝试往别的方向走,直到把所有可能都尝试一遍,最后输出最短的一条路径。
现在我们尝试用深度优先搜索来实现这个方法。先来看dfs()函数如何写。dfs()函数的功能是解决当前应该怎么办。而小明处在某个点的时候需要处理的是:先检查小明是否已经到达小红的位置,如果没有到达则找出下一步可以走的地方。为了解决这个问题,此处 dfs()函数只需要维护 3 个参数,分别是当前这个点的 x 坐标、y 坐标以及当前以及走过的步数 step。dfs()函数定义如下。
void dfs(int x,int y,int step)
{
return 0;
}
判断是否已经到达小红的位置这一点很好实现,只需要判断当前的坐标和小红的坐标是否相等就可以了,如果相等则表明已经到达小哈的位置,如下。
void dfs(int x,int y,int step)
{
//判断是否到底小红的位置
if(x==p && y==q)
{
//更新最小值
if(step<min)
min=step;
return;//请注意这里的返回很重要
}
return 0;
}
如果没有到达小红的位置,则找出下一步可以走的地方。因为有四个方向可以走,根据我们之前的约定,按照顺时针的方向来尝试(即按照右、下、左、上的顺序尝试)。这里为了编程方便,我定义了一个方向数组 next,如下。
int next[4][2]={{ 0, 1},//向右走
{ 1, 0},//向下走
{ 0, -1},//向左走
{ -1, 0}};//向上走
通过这个方向数组,使用循环就很容易获得下一行的坐标。这里将下一步的横坐标用 tx 存储,纵坐标用 ty 存储。
for(k=0;k<=3;k++)
{
//计算的下一个点的坐标
tx=x+next[k][0];
ty=y+next[k][1];
}
接下来我们就要对下一个点(tx,ty)进行一些判断。包括是否越界,是否为障碍物,以及这个点是否已经在路径中(即避免重复访问一个点)。需要用 book[tx] [ty] 来记录格子(tx,ty)是否已经在路径中。
如果这个点符合所有的要求,就对这个点进行下一步的扩展,即 dfs(tx,ty,step+1),注意这里是step+1,因为一旦你从这个点开始继续往下尝试,就意味着你的步数已经增加了 1。代码实现如下。
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]p[ty]==0 && book[tx][ty]==0)
{
book[tx][ty]=1;//标记这个点已经走过
dfs(tx,ty,step+1);//开始尝试下一个点
book[tx][ty]=0;//尝试结束,取消这个点的标记
}
}
好了,来看下完整的代码吧。
#include <stdio.h>
int n,m,p,q,min=99999999;
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 ;//请注意这里的返回很重要
}
//枚举4种走法
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;
//读入n和m,n为行,m为列
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;//标记起点已经在路径中,放防止后面重复走
//第一个参数是起点的x坐标,第二个参数是起点的y坐标,第三个参数是初始步数为0
dfs(startx,starty,0);
//输出最短步数
printf("%d",min);
getchar();getchar();
return 0;
}
可以输入以下数据进行验证。第一行有两个数 n m。n表示迷宫的行,m表示迷宫的列。接下来 n 行 m 列为迷宫,0 表示空地,1 表示障碍物。最后一行 4 个数,前两个数为迷宫入口的 x 和 y 坐标。后两个为救援地的 x 和 y坐标。
5 4
0 0 1 0
0 0 0 0
0 0 1 0
0 1 0 0
0 0 0 1
1 1 4 3
运行结果是:
7
广度优先搜索
在上面迷宫解救的行动中,我们使用了深度优先搜索的方法。这里我将介绍另外一种方法来解决这个问题——广度优先搜索(Breadth First Search,BFS),也称为宽度优先搜索。
我们还是用一个二维数组来存储这个迷宫。最开始的时候小明在迷宫(1,1)处,他可以往右走或者往下走。在上一节中我们的方法是,先让小明往右边走,然后一直尝试下去,直到走不通的时候再回到这里。这样是深度优先,可以通过函数的递归实现。现在介绍另外一种方法:通过“一层一层”扩展的方法来找到小哈。扩展时每发现一个点就将这个点加入到队列中,直至走到小红的位置(p,q)时为止,具体如下。
最开始小明在入口(1,1)处,一步之内可以到达的点有(1,2)和(2,1)。
但是小红并不在这两个点上,那小明只能通过(1,2)和(2,1)这两点继续往下走。比如现在小明走到了(1,2)这个点,之后他又能够到达哪些新的点呢?有(2,2)。再看看通过(2,1)又可以到达哪些点呢?可以到达(2,2)和(3,1)。此时你会发现(2,2)这个点既可以从(1,2)到达,也可以从(2,1)到达,并且都只使用了2步。为了防止一个点多次被走到,这里需要一个数组来记录一个点是否已经被走到过。
此时小明2步可以走到的点就全部走到了,有(2,2)和(3,1),可是小红并不在这两个点上。没有别的办法,还得继续往下尝试,看看通过(2.2)和(3,1)这两个点还能到达哪些新的没有走到过的点。通过(2,2)这个点我们可以到达(2,3)和(3,2),通过(3,1)可以到达(3,2)和(4,1)。现在3步可以到达的点有(2,3)、(3,2)和(4,1),依旧没有到达小红的所在点,我们需要重复刚才的方法,直到到达小红所在点为止。
回顾一下刚才的算法,可以用一个队列来模拟这个过程。这里我们还是用一个结构体来实现队列。
struct note
{
int x;//横坐标
int y;//纵坐标
int s;//步数
};
struct note que[2501]; //因为地图大小不超过50*50,因此队列扩展不会超过2500个
int head,tail;
int a[51][51]={0};//用来存储地图
int book[51][51]={0};//数组book的作用是记录哪些点已经在队列中了,防止一个点被重复扩展,并全部初始化为0.
//最开始的时候需要进行队列初始化,即队列设置为空。
head=1;
tail=1;
//第一步将(1,1)加入队列,并标记(1,1)已经走过。
que[tail].x=1;
que[tail].y=1;
que[tail].s=0;
tail++;
book[1][1]=1;
然后从(1,1)开始,先尝试往右走到达了(1,2)。
tx=que[head].x;
ty=que[head].y+1;
选哟判断(1,2)是否越界。
if(tx<1 || tx>n || ty<1 || ty>m)
continue;
再判断(1,2)是否为障碍物或者已经在路径中。
if(a[tx][ty]==0 && book[tx][ty]==0)
{
}
如果满足上面的条件,则将(1,2)入队,并标记该点已经走过。
//把这个点标记为已经走过
book[tx][ty]=1;//注意宽搜每个点只入队一次,所以和深搜不同,不需要将book数组还原
//插入新的点到队列中
que[tail].x=tx;
que[tail].y=ty;
que[tail].s=que[head].s+1;//步数是父亲的步数+1
tail++;
接下来还要继续尝试往其他方向走。这里还是规定一个顺序,即按照顺时针的方向来尝试(也就是以右、下、左、上的顺序尝试)。我们发现从(1,1)还是可以到达(2,1),因此也需要将(2,1)也加入队列,代码实现与刚才对(1,2)的操作是一样的。
对(1,1)扩展完毕后,其实(1,1)现在对我们来说已经没有用了,此时我们将(1,1)出队。出队的操作,很简单就一句话,如下。
head++;
接下来我们需要在刚才新扩展出的(1,2)和(2,1)这两个点的基础上继续向下探索。到目前为止我们已经扩展出从起点出发一步以内可以到达的所有点。因为还没有到达小哈的所在位置,所以还需要继续。
(1,1)出队之后,现在队列的 head 正好指向了(1,2)这个点,现在我们需要通过这个点继续扩展,通过(1,2)可以到达(2,2),并将(2,2)也加入队列。
(1,2)这个点已经处理完毕,对我们来说也没有用了,于是将(1,2)出队。(1,2)出队之后,head指向了(2,1)这个点。通过(2,1)可以到达(2,2)和(3,1),但是因为(2,2)已经在队列中,因此我们只需要将(3,1)入队。
到目前为止我们已经扩展出从起点出发2步以内可以到达的所有点,可是依旧没有到达小红的所在位置,因此还需要继续,直至走到小哈所在点,算法结束。
为了方便向四个方向扩展,与上一节一样这里需要一个 next 数组。
int next[4][2] = { { 0, 1},//向右走
{ 1, 0},//向下走
{ 0, -1},//向左走
{ -1, 0} };//向上走
完整的代码实现如下。
#include <stdio.h>
struct note
{
int x;//横坐标
int y;//纵坐标
int f;//父亲在队列中的编号,本题不要求输出路径,可以不需要f
int s;//步数
};
int main()
{
struct note que[2501]; //因为地图大小不超过50*50,因此队列扩展不会超过2500个
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++)
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;
flag=0;//用来标记是否到达目标点,0表示暂时还没有到达,1表示到达
//当队列不为空的时候循环
while(head<tail)
{
//枚举4个方向
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数组还原
book[tx][ty]=1;
//插入新的点到队列中
que[tail].x=tx;
que[tail].y=ty;
que[tail].f=head;//因为这个点是从head扩展出来的,所以它的父亲是head,本题目不需要求路径,因此本句可省略
que[tail].s=que[head].s+1;//步数是父亲的步数+1
tail++;
}
//如果到目标点了,停止扩展,任务结束,退出循环
if(tx==p &&ty==q)
{
//注意下面两句话的位置千万不要写颠倒了
flag=1;
break;
}
}
if(flag==1)
break;
head++;//注意这地方千万不要忘记,当一个点扩展结束后,head++才能对后面的点再进行扩展
}
//打印队列末尾最后一个点(目标点)的步数
//注意tail是指向队列队尾(即最后一位)的下一个位置,所以这需要-1
printf("%d",que[tail-1].s);
getchar();getchar();
return 0;
}
可以输入以下数据进行验证。第一行有两个数 n m。n表示迷宫的行,m表示迷宫的列。接下来 n 行 m 列为迷宫,0 表示空地,1 表示障碍物。最后一行 4 个数,前两个数为迷宫入口的 x 和 y 坐标。后两个为救援地的 x 和 y坐标。
5 4
0 0 1 0
0 0 0 0
0 0 1 0
0 1 0 0
0 0 0 1
1 1 4 3
运行结果是:
7
再解炸弹人
还记得我们在第三节留下的问题吗?
按照第3节的方法,将炸弹放置在(1,11)处,最多可以消灭11个敌人(注意这里是从 0 行 0 列开始计算的)。但小人其实是无法走到(1,11)的。所以正确的答案应该是将炸弹放在(7,11)处,可以消灭10个敌人。那这样的问题又该如何解决呢?解决这个问题的关键就在于找出哪些点是小人可以到达的。我们可以使用本章学习的广度优先搜索或者深度优先搜索来枚举出所有小人可以到达的点,然后在这些可以到达的点上来分别统计可以消灭的敌人数。
先来看如何用广度优先搜索来枚举出所有小人可以到达的点。首先从小人的所在点(3,3)开始扩展,先将点(3,3)入队,并且计算出将炸弹放置在该点能够消灭的敌人数(计算消灭的敌人数与之前的方法相同)。然后通过(3,3)这个点可以扩展出(3,4)、(4,3)、(3,2)和(2,3),并将这些点入队,然后分别计算出在每个点放置炸弹可以消灭的敌人数。接下来再通过(3,4)进行扩展……直到把所有能到达的点全部扩展完毕,广搜结束。最后输出扩展到的点中消灭最多敌人数的那个点的坐标以及消灭的敌人数。
此题我们仍然需要一个结构体来实现队列。这里只需要x和y来记录坐标。
struct note
{
int x;//横坐标
int y;//纵坐标
};
struct note que[401]; //假设地图大小不超过20*20,因此队列扩展不会超过400个
int head,tail;
char a[20][20]; //用来存储地图
int book[20][20]={0}; //定义一个标记数组并全部初始化为0
上面的数组 book 的作用是记录哪些点已经在队列中了,防止一个点被重复扩展。最开始的时候需要进行队列初始化,即将队列设置为空。
head=1;
tail=1;
往队列插入小人的起始坐标(startx,starty),并标记(startx,starty)已经在队列中了。
que[tail].x=startx;
que[tail].y=starty;
tail++;
book[startx][starty]=1;
统计将炸弹放在该点(startx,starty)可以消灭多少敌人。统计的方法与第3章的方法是相同的。此处将求“在某个点(i,j)放置炸弹能够消灭的敌人数”写成一个函数,函数名为 getnum,方便以后调用。
sum=getnum(startx,starty);
为了方便宽度优先搜索时朝四个方向扩展,这里也需要一个next数组。
int next[4][2] = { { 0, 1},//向右走
{ 1, 0},//向下走
{ 0, -1},//向左走
{ -1, 0} };//向上走
接下来便开始扩展,也就是广度优先搜索的核心部分。
while(head<tail)
{
//枚举4个方向
for(k=0;k<=3;k++)
{
//尝试走的下一个点的坐标
tx=que[head].x+next[k][0];
ty=que[head].y+next[k][1];
//判断是否越界
if(tx<0 || tx>n-1 || ty<0 || ty>m-1)
continue;
//判断是否为平地或者曾经走过
if(a[tx][ty]=='.' && book[tx][ty]==0)
{
//每个点只入队一次,所有需要标记这个点已经走过
book[tx][ty]=1;
//插入新扩展的点到队列中
que[tail].x=tx;
que[tail].y=ty;
tail++;
//统计当前扩展的点可以消灭的敌人总数
sum=getnum(tx,ty);
//更新max的值
if(sum>max)
{
//如果当前统计出所能消灭的敌人数大于max,则更新max,并用mx和my记录该点坐标
max=sum;
mx=tx;
my=ty;
}
}
}
head++;//注意这地方千万不要忘记,当一个点扩展结束后,必须要head++才能对后面的点进行扩展
}
以上就是基本的实现过程了。完整的代码如下。
#include <stdio.h>
struct note
{
int x;//横坐标
int y;//纵坐标
};
char a[20][21]; //用来存储地图
int getnum(int i,int j)
{
int sum,x,y;
sum=0;//sum用来计数(可以消灭的敌人数),所以需要初始化为0
//将坐标i,j复制到两个新变量x,y中,以便之后向上下左右四个方向统计可以消灭的敌人数
//向上统计可以消灭的敌人数
x=i;y=j;
while(a[x][y]!='#')//判断的点是不是墙,如果不是墙就继续
{
//如果当前的点是敌人,则进行计数
if(a[x][y]=='G')
sum++;
//x--的作用是继续向上统计
x--;
}
//向下统计可以消灭的敌人数
x=i;y=j;
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
//x++的作用是继续向下统计
x++;
}
//向左统计可以消灭的敌人数
x=i;y=j;
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
//y--的作用是继续向左统计
y--;
}
//向右统计可以消灭的敌人数
x=i;y=j;
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
//y++的作用是继续向右统计
y++;
}
return sum;
}
int main()
{
struct note que[401]; //假设地图大小不超过20*20,因此队列扩展不会超过400个
int head,tail;
int book[20][20]={0}; //定义一个标记数组并全部初始化为0
int i,j,k,sum,max=0,mx,my,n,m,startx,starty,tx,ty;
//定义一个用于表示走的方向的数组
int next[4][2] = { { 0, 1},//向右走
{ 1, 0},//向下走
{ 0, -1},//向左走
{ -1, 0} };//向上走
//读入n和m,n表示有多少行字符,m表示每行有多少列
scanf("%d %d %d %d",&n,&m,&startx,&starty);
//读入n行字符
for(i=0;i<=n-1;i++)
scanf("%s",a[i]);
//队列初始化
head=1;
tail=1;
//往队列中插入小人的起始坐标
que[tail].x=startx;
que[tail].y=starty;
tail++;
book[startx][starty]=1;
sum=getnum(startx,starty);
mx=startx;
my=starty;
//当队列不为空的时候循环
while(head<tail)
{
//枚举4个方向
for(k=0;k<=3;k++)
{
//尝试走的下一个点的坐标
tx=que[head].x+next[k][0];
ty=que[head].y+next[k][1];
//判断是否越界
if(tx<0 || tx>n-1 || ty<0 || ty>m-1)
continue;
//判断是否为平地或者曾经走过
if(a[tx][ty]=='.' && book[tx][ty]==0)
{
//每个点只入队一次,所有需要标记这个点已经走过
book[tx][ty]=1;
//插入新扩展的点到队列中
que[tail].x=tx;
que[tail].y=ty;
tail++;
//统计当前扩展的点可以消灭的敌人总数
sum=getnum(tx,ty);
//更新max的值
if(sum>max)
{
//如果当前统计出所能消灭的敌人数大于max,则更新max,并用mx和my记录该点坐标
max=sum;
mx=tx;
my=ty;
}
}
}
head++;//注意这地方千万不要忘记,当一个点扩展结束后,必须要head++才能对后面的点进行扩展
}
printf("将炸弹放置在(%d,%d)处,可以消灭%d个敌人\n",mx,my,max);
getchar();getchar();
return 0;
}
可以输入以下数据进行验证。第一行 2 个整数为 n m,分别表示迷宫的行和列,接下来的 n行 m列为地图。
13 13 3 3
#############
#GG.GGG#GGG.#
###.#G#G#G#G#
#.......#..G#
#G#.###.#G#G#
#GG.GGG.#.GG#
#G#.#G#.#.#.#
##G...G.....#
#G#.#G###.#G#
#...G#GGG.GG#
#G#.#G#G#.#G#
#GG.GGG#G.GG#
#############
运行结果是:
将炸弹放置在(7,11)处,最多可以消灭10个敌人
当然也可以用深度优先搜索来做,也是从小人所在点开始向右走。每走到一个新点就统计该点可以消灭的敌人数,并从该点继续尝试往下走,直到无路可走的时候返回,再尝试走其他方向,直到将所有可以走到的点都访问一遍,程序结束。请参考以下代码。
void dfs(int x,int y)
{
//计算当前这个点可以消灭的敌人总数
sum=getnum(x,y);
//更新max的值和该点的坐标
if(sum>max)
{
max=sum;
mx=x;
my=y;
}
//枚举4个方向
for(k=0;k<=3;k++)
{
//下一个结点的坐标
tx=x+next[k][0];
ty=y+next[k][1];
//判断是否越界
if(tx<0 || tx>n-1 || ty<0 || ty>m-1)
continue;
//判断是否围墙或者曾经走过
if(a[tx][ty]=='.' && book[tx][ty]==0)
{
book[tx][ty]=1;//标记这个点已走过
dfs(tx,ty);//开始尝试下一个点
}
}
return ;
}
完整的代码如下。
#include <stdio.h>
char a[20][21];
int book[20][20],max,mx,my,n,m;
int getnum(int i,int j)
{
int sum,x,y;
sum=0;//sum用来计数(可以消灭的敌人数),所以需要初始化为0
//将坐标i,j复制到两个新变量x,y中,以便之后向上下左右四个方向统计可以消灭的敌人数
//向上统计可以消灭的敌人数
x=i;y=j;
while(a[x][y]!='#')//判断的点是不是墙,如果不是墙就继续
{
//如果当前的点是敌人,则进行计数
if(a[x][y]=='G')
sum++;
//x--的作用是继续向上统计
x--;
}
//向下统计可以消灭的敌人数
x=i;y=j;
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
//x++的作用是继续向下统计
x++;
}
//向左统计可以消灭的敌人数
x=i;y=j;
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
//y--的作用是继续向左统计
y--;
}
//向右统计可以消灭的敌人数
x=i;y=j;
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
//y++的作用是继续向右统计
y++;
}
return sum;
}
void dfs(int x,int y)
{
//定义一个用于表示走的方向的数组
int next[4][2] = { { 0, 1},//向右走
{ 1, 0},//向下走
{ 0, -1},//向左走
{ -1, 0} };//向上走
int k,sum,tx,ty;
//计算当前这个点可以消灭的敌人总数
sum=getnum(x,y);
//更新max的值和该点的坐标
if(sum>max)
{
max=sum;
mx=x;
my=y;
}
//枚举4个方向
for(k=0;k<=3;k++)
{
//下一个结点的坐标
tx=x+next[k][0];
ty=y+next[k][1];
//判断是否越界
if(tx<0 || tx>n-1 || ty<0 || ty>m-1)
continue;
//判断是否围墙或者曾经走过
if(a[tx][ty]=='.' && book[tx][ty]==0)
{
book[tx][ty]=1;//标记这个点已走过
dfs(tx,ty);//开始尝试下一个点
}
}
return ;
}
int main()
{
int i,startx,starty;
//读入n和m,n表示有多少行字符,m表示每行有多少列
scanf("%d %d %d %d",&n,&m,&startx,&starty);
//读入n行字符
for(i=0;i<=n-1;i++)
scanf("%s",a[i]);
//从小人所站的位置开始尝试
book[startx][starty]=1;
mx=startx;
my=starty;
dfs(startx,starty);
printf("将炸弹放置在(%d,%d)处,可以消灭%d个敌人\n",mx,my,max);
getchar();getchar();
return 0;
}
参考:《啊哈!算法》