1、深度优先搜索
定义:从起始点一条道走到黑,到头没路了,就退一步,看看有没有路,有路继续走到黑,没路就再退一步。退退退……
引子例题:我们经过n个盒子,并且每个里面放入我们手中的n张不同的牌,共有多少种排列方式呢?
程序如下
#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——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;
}
题目:最基本的救人问题,路上有障碍物,怎样走最短呢?
分析:首先我们是用dfs,那么我们就要判断1.我们是否已经到达了要到的位置,2.我们可以设立一个控制方向的变量,使得可以向上下左右移动,3.我们每次获得的坐标要使其用递归思想调用自身,来进行下一次操作,每扩展一次,我们就给它的步数加1;4.我们每次尝试之后,要使该点坐标标记取消,以便进行下一次判断。
代码实现分析如下:
#include<stdio.h>
#include<alograthm>
int n,m,p,q,min=99999999;
int a[51][51],book[51][51];
boid 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",&startx,&staty,&p,&q);//读入起始点
book[startx][starty]=1;//起点标记已经走过
dfs(startx,starty,0);//从起点开始遍历
printf("%d",min);//输出最小值
getchar();getchar();
return 0;
}
2、广度优先搜索。
定义:广度优先搜索也叫做宽度优先搜索,BFS,我们是通过一层一层的扩展找到最小值。每次找到所有比之前只差一步的值。
分析:实则我们可以用一个队列来模拟整个过程每次判断若我们走到该点符合要求,则我们每次在同一层里符合的值我们将其入队,然后这一层的全部遍历后,我们将上一层的出队。同时我们也需要一个和刚才一样的next方向数组。
代码实现分析如下:
#include<stdio.h>
#include<algorithm>
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;
cin>>n>>m;
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
cin>>a[i][j];
cin>>stratx>>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[tx][ty]=1;//标记该点已走过
//因为BFS中,所有的点入队只有一次,所以我们不需要给它还原
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;//停止for循环
}
}
if(flag == 1)
break;//停止while循环
head++;//注意不要忘记,当一个点扩展结束后,不要忘记把head+1,这样才可以继续扩展后面的点
}
cout<<que[tail-1].s;//tail是指队列队尾的下一个位置,所以需要-1;
getchar();getchar();
return 0;
}
3.讲解
因为该科目是考试重点,所以我还是找到了一些不错的题,抄在上面分享一下。
1.记不记得我们之前在枚举里找到的一个炸弹人的问题,但那道题实际上是有缺陷的,因为我们枚举的答案,有可能是在一个我们无法到达的地方,所以这里我们将要用深搜和广搜分别实现这道题。思路不必多说,无非是每次判断你所能到达的点,并且判断是否更新最大值即可
广搜代码如下:
#include<stdio.h>
#include<algorithm>
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=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 not que[401];
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}};
cin>>n>>m>>startx>>starty;//读入
for(i=0;i<=n-1;i++)
cin>>a[i];
//队列初始化
head=1;
tail=1;
//向队列插入起始点的坐标
que[tail].x=startx;
que[tail].y=starty;
tail++;
book[startx][starty]=1;
max=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++,这样才能使后面的点进行扩展。
}
cout<<mx<<" "<<my<<" "<<max;
getchar();getchar();
return 0;
}
深搜代码分析如下:
#include<stdio.h>
#include<algorithm>
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;//同上
x=i;y=j;//统计该点可以消灭的敌人数,之后该函数内同上
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
x--;
}
x=i;y=j;
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
x++;
}
x=i;y=j;
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
y--;
}
x=i;y=j;
while(a[x][y]!='#')
{
if(a[x][y]=='G')
sum++;
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);//计算该点杀敌总数
if(xum>max)//更新max的值
{
//如果当前的点统计出的所能消灭的敌人数大于max,则更新max,并
//用mx和my标记当前的点
max=sum;
mx=x;
my=y;
}
for(k=0;k<=3;k++)
{
tx=x+next[k][0];//枚举四个方向,tx,ty为下一点的坐标
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;
cin>>n>>m>>startx>>starty;
for(i=0;i<=n-1;i++)
for(j=0;j<=m-1;j++)
cin>>a[i][j];
book[startx][starty]=1;//从小人站的位置开始尝试。
max=getnum(startx,starty);
mx=startx;
my=starty;
dfs(startx,starty);
cout<<"("<<mx<<","<<my<<")"<<max;
getchar();
getchar();
return 0;
}
2.宝岛探险
我们标记一个航拍图,其中部分若为陆地则为1-9的数组,若为海洋则为0,现在我们将某人扔在一块陆地上,请计算出该岛的陆地面积(只要相连即可)。
分析:其实我们只需要从起始点开始遍历,只要扩展出的点大于数值0,则将其入队列,直到队列扩展完毕,所有被加入到队列的点的总数就是小岛的面积。
代码实现分析如下:
#include<stdio.h>
#include<algorithm>;
struct note
{
int x;//横坐标
int y;//纵坐标
};
int main()
{
struct not que[2501];
int head,tail;
int a[51][51];
int book[51][51]={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}};//上
cin>>n>>m>>startx>>starty;//读入n行n列和起始点的坐标
//读入地图
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
cin>>a[i][j];
//队列初始化
head=1;
tail=1;
//队列内插入起始点坐标
que[tail].x=startx;
que[tail].y=starty;
tail++;
book[startx][starty]=1;
sum=1;
//队列不为空的时候循环
while(head<tail)
{
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)
{
sum++;//计算岛屿的面积
book[tx][ty]=1;//标记该点已经经历过
que[tail].x=tx;//将其横纵坐标加入队列,
que[tail].y=ty;
tail++;
}
}
head++;//该点所有的扩展结束,可以开始扩展下一点
}
cout<<sum;//输出岛屿的大小。
getchar();getchar();
return 0;
}
以上使我们用广搜的思想来解决此问题,那么,深搜可以吗,答案是肯定的,容我卖一个关子,大家可以在看下一个程序的时候,思考一下如果我要计算有多少岛屿,每个的大小呢??和这一个有异曲同工之妙哦。
#include<stdio.h>
int a[51][51]
int book[51][51],n,m,sum;
void dfs(int x,int y)
{
int next={{0,1},{1,0},{0,-1},{-1,0}};//定义方向数组
int k,tx,ty;
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 && cook[tx][ty]==0)//判断是否为陆地
{
sum++
book[tx][ty]=1;//标记已经走过
dfs(tx,ty);//开始尝试下一个点
}
}
return ;
}
int main()
{
int i,j,startx,starty;
scanf{"%d %d %d %d",&n,&m,&startx,&starty);
for(i=1;i<=n;j++)//读入地图
for(j=1;j<=m;j++)
scanf("%d",&a[i]a[j]);
book[startx][starty]=1;
sum=1;
dfs(startx,starty);//从起始点开始
printf("%d\n",sum);//输出岛屿的大小
getchar();getchar();
return 0;
}
是否对刚才的问题有了启发呢??没有,不要紧,很正常,我也没有,但代码很清晰,看过后你就知道了,不过插入一点,上面这种方法叫做着色法,以某个点为原点,向周围的点进行着色,我们可以对上面的程序在dfs函数中加入一个参数color,color表示该岛屿需要的颜色,请看下面加粗的部分:
#include<stdio.h>
int a[51][51]
int book[51][51],n,m,sum;
void dfs(int x,int y,**int color**)
{
int next={{0,1},{1,0},{0,-1},{-1,0}};//定义方向数组
int k,tx,ty;
**a[x][y]=color;//对这个格子染色**
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 && cook[tx][ty]==0)//判断是否为陆地
{
sum++
book[tx][ty]=1;//标记已经走过
**dfs(tx,ty,color);//开始尝试下一个点**
}
}
return ;
}
int main()
{
int i,j,startx,starty;
scanf{"%d %d %d %d",&n,&m,&startx,&starty);
for(i=1;i<=n;j++)//读入地图
for(j=1;j<=m;j++)
scanf("%d",&a[i]a[j]);
book[startx][starty]=1;
sum=1;
dfs(startx,starty,-1);//从起始点开始
for(i=1;i<=n;i++)
{
for(j=1;j<=m;j++)
{
printf("%3d",a[i][j]);
}
printf("\n");
}
getchar();getchar();
return 0;
}
终于要回归主题了,我们想知道有多少独立的小岛怎么办呢?很简单,只需要对地图上每一个大于0的点减小一遍深搜就可以,因为等于0的点是海洋,小于0的就是染色的小岛,我们可以从(1,1)遍历到(n,m),对每个点尝试染色。
#include<stdio.h>
int a[51][51]
int book[51][51],n,m,sum;
void dfs(int x,int y,int color)
{
int next={{0,1},{1,0},{0,-1},{-1,0}};//定义方向数组
int k,tx,ty;
a[x][y]=color;//对这个格子染色
//枚举四个方向
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 && cook[tx][ty]==0)//判断是否为陆地
{
sum++
book[tx][ty]=1;//标记已经走过
dfs(tx,ty,color);//开始尝试下一个点
}
}
return ;
}
int main()
{
int i,j,num;
scanf{"%d %d",&n,&m);
for(i=1;i<=n;j++)//读入地图
for(j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for(i=1;i<=n;j++)
{
for(j=1;j<=m;j++)
{
if(a[i][j]>0)
{
num--;
book[i][j]=1;
dfs(i,j,num);//从起始点开始
}
}
}
for(i=1;i<=n;i++)
{
for(j=1;j<=m;j++)
{
printf("%3d",a[i][j]);
}
printf("\n");
}
//输出小岛的个数
printf("%d\n"-num)
getchar();getchar();
return 0;
}
3.水管工游戏。
P.S.这道题比较难以理解,我会尽量写的清楚一点。
题目:一块n*m的矩形土地,然后里面有n*m个小正方形,水管将从(1,1)即左上角边缘到右下角右部边缘。水管有直管和弯管两种,中间还会有树木,请通过旋转寻找一个连通的管道系统,若有则输出路径,否则就输出impossible。
分析:因为一共两种关子,直管和弯管,且直管2种上下和左右,弯管有四种:左下,下右,右上,上左,((上左)为由上到左),我们要判断是否可以和这个单位正方形的一侧即进水口相连,且下一个是否会出界,然后我们可以通过入水口和正方形内的管道的直或弯来判断旋转使用或者返回。同时为了方便,我们用1来表示进水口在左侧,2表示上面,3表示右侧,4表示下面
首先我们看一下如何解决直管:
dfs()函数的写法如下:
void dfs(int x,int y,front)//x,y为坐标,front为进水口方向
{
//判断是否出界
if(x<1 || x>n || y<1 ||y>m)
return;
if(book[x][y]==1)//判断是否已经使用管道
return;
book[x][y]=1;//标记使用
//若为直管
if( a[x][y]>=5&&a[x][y]<=6)
{
if(front==1)//进水口在左侧
{
dfs(x,y+1,1);//使用5号
}
if(front==2)//进水口在上面
{
dfs(x+1,y,2);//使用6号
}
if(front==3)//进水口在右侧
{
dfs(x,y-1,3);//用5号
}
if(front==4)//进水口在下面
{
dfs(x,y+1,1);//用6号
}
}
book[x][y]=0;//取消标记
return;
}
大家是不是有一点蒙,怎么判断的5号6号啊,大家可以画图看看,假设最后一个,front=4,此时水是从上面来的,只能是6号直管。而我们通过这个可以判断出我们递归后的下一个的进水口是哪个方向。以上是直管,弯管其实也类似,而我们要输出路径,就加入到一个栈里面,完整的代码实现如下:
#include<stdio.h>
int a[51][51];
int book[51][51],n,m,flag=0;
struct note
{
int x;
int y;
}s[100];
int top=0;
void dfs(int x,int y,int front)
{
int i;
//判断是否到了终点
if(x==n && y==m+1)//之所以为y=m+1,是因为:到达(n,m)之后,下一个点的进水口一定是左侧,所以y一定会+1;所以在下一次遍历中,若y=m+1,则说明该管道恰好与进水口相连。
{
flag=1;//找到了铺设方案
for(i=1;i<=top;i++
printf("(%d,%d)",s[i].x,s[i].y);
return;
}
if( x<1 || x>n || y<1 || y>m)
return;
if(book[x][y]==1)//判断是否已使用管道
return;
book[x][y]=1;//标记使用
//将当前尝试的坐标入栈
top++;
s[top].x=x;
s[top].y=y;
//若为直管
if( a[x][y]>=5&&a[x][y]<=6)
{
if(front==1)//进水口在左侧
{
dfs(x,y+1,1);//使用5号
}
if(front==2)//进水口在上面
{
dfs(x+1,y,2);//使用6号
}
if(front==3)//进水口在右侧
{
dfs(x,y-1,3);//用5号
}
if(front==4)//进水口在下面
{
dfs(x,y+1,1);//用6号
}
}
//若为弯管
if( a[x][y]>=1 && a[x][y]<=4)
{
if(front==1)//进水口在左侧
{
dfs(x+1,y,2);//使用3号
dfs(x-1,y,4);//使用4号
}
if(front==2)//进水口在上面
{
dfs(x,y+1,1);//使用1号
dfs(x,y-1,3);//使用4号
}
if(front==3)//进水口在右侧
{
dfs(x-1,y,4);//使用1号
dfs(x+1,y,2);//使用2号
}
if(front==4)//进水口在下面
{
dfs(x,y+1,1);//使用2号
dfs(x,y-1,3);//使用3号
}
}
book[x][y]=0;//取消标记
top--;//将当前尝试的坐标出栈
return;
}
int main()
{
int i,j,num=0;
scanf("%d %d",&n,&m);
//读入地图
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
scanf("%d"&a[i][j]);
//开始搜索初始位置为(1,1),进水口也为1;
dfs(1,1,1);
if(flag == 0)
printf("impossible\n");
else
总结:我太累了,一天我去,真服了,有没有人发个红包安慰一下,蓝瘦,香菇,喝杯臻选淡定一下,
编者自述:你猜我想说什么呢??我靠,为了素质,我不抱粗口,MDZZ,你丫的。我其实是个文静的美男子的,以上字皆与我无关,不要影响我的形象!