图的遍历算法是图的基本算法中一种,也是图的拓扑排序算法,关键路径算法等算法的基础。那么什么是图的遍历呢:图的遍历指从图中某一顶点出发,按照某种方法对途中所有顶点访问且仅访问一次。而根据图的搜素路径的方向,分为两种遍历图的路径:深度优先搜素和广度优先搜素,简称DFS和BFS。
DFS算法
深度优先搜素算法,顾名思义,尽可能深入搜素。总的来说,深度优先搜素算法总是对最近才发现的节点v的出发边进行搜素,直到该结点的所有出发边都被发现,一旦结点v的所有出发边都被发现,搜素则“回溯”到v的前驱结点,来搜索该前驱结点的出发边,一直到从源结点开始可以达到的所有结点都被发现为止。若存在未发现的点,则从未发现的点里选出一个点作为新结点,进行以上过程,直至所有的点被发现为止。
上面讲的可能有点抽象,我来举个例子吧,对下面的无向图:
DFS算法的实现方法:
可以看出图中从A结点作为起点出发,依次遍历B,C,F结点,而F结点无后继结点,开始回溯到B结点,再从B结点出发,遍历E,G,D结点,而D的后继结点A结点已经被遍历,再次回溯至G结点,遍历H,I结点,此时结点已全部遍历完成,搜素结束,退出。
显然,根据图的遍历路径和“回溯”算法的特性,DFS遍历图是一个递归的过程。
下面,给出DFS算法的一般模板:
int n,m; //全局变量
bool vis[M][N]; //辅助数组,用来访问标记
char map[M][N];
int dir[8][2]={0,1,0,-1,1,0,-1,0,-1,-1,-1,1,1,-1,1,1}; //对于八个方向的方向向量
//int dir[4][2]={0,1,0,-1,1,0,-1,0}; //对于四个方向的方向向量
void dfs(int x,int y);
bool In(int x,int y);
void dfs(int x,int y) //结点的二维坐标
{
vis[x][y]=1; //标记结点已被访问过
if(map[x][y]==G) //满足目标态
{ ...... //执行操作
return;
}
for(int i=0;i<8;i++)
{
int nx=x+dir[i][0]; //对结点8个方向展开搜索
int ny=y+dir[i][1];
if(In(nx,ny)) //边界条件
dfs(nx,ny); //递归搜索
}
}
bool In(int x,int y)
{
if(vis[x][y]==1&&map[x][y]==...) //已标记,则返回
return false;
if(x<0||y<0||x>=n||y>=m) //结点越过边界,返回
return false;
return true;
}
下面举个例子来说明DFS算法的具体计算方法吧。
Time Limit: 1000MS | Memory Limit: 65536K | |
Total Submissions: 42815 | Accepted: 21181 |
Description
Given a diagram of Farmer John's field, determine how many ponds he has.
Input
* Lines 2..N+1: M characters per line representing one row of Farmer John's field. Each character is either 'W' or '.'. The characters do not have spaces between them.
Output
Sample Input
10 12 W........WW. .WWW.....WWW ....WW...WW. .........WW. .........W.. ..W......W.. .W.W.....WW. W.W.W.....W. .W.W......W. ..W.......W.
Sample Output
3
代码分析:此题选自POJ2386题,这是一个标准的连通块问题,又称“种子填充”问题(floodfill),此题最简单的算法为DFS(当然BFS也是可以做的,大家可以思考一下)。对此题,首先,对整个图进行扫描,一旦发现'W',开始搜索,并标记已扫描结点,再对该结点四周八个方向进行扫描,并同步标记,一旦存在一个结点与之前结点不构成连通图,则返回,sum自增,直至扫描完整个图,此时,可得到连通块数目。
代码如下:
#include <iostream>
#include <cstdio>
#define N 105
#define M 105
using namespace std;
int n,m;
char vis[M][N];
int dir[8][2]={0,1,0,-1,1,0,-1,0,-1,-1,-1,1,1,-1,1,1};
void dfs(int x,int y);
bool In(int x,int y);
int main()
{
int i,j,sum;
scanf("%d%d",&n,&m);
for(i=0;i<n;i++)
scanf("%s",vis[i]);
sum=0;
for(i=0;i<n;i++)
{
for(j=0;j<m;j++)
{
if(vis[i][j]=='W')
{
++sum;
dfs(i,j);
}
}
}
printf("%d\n",sum);
return 0;
}
void dfs(int x,int y)
{
vis[x][y]='.';
for(int i=0;i<8;i++)
{
int nx=x+dir[i][0];
int ny=y+dir[i][1];
if(In(nx,ny))
dfs(nx,ny);
}
}
bool In(int x,int y)
{
if(vis[x][y]=='.')
return false;
if(x<0||y<0||x>=n||y>=m)
return false;
return true;
}
适用范围:DFS适用于查找所有解问题,例如连通块问题,拓扑排序等问题。优点明显,代码简洁易懂,空间复杂度低,适用于规模比较大的图的求解。
BFS算法
广度优先算法,类似于树的层次遍历算法。该算法扫描图时,先从图中一个顶点v出发,访问v,再访问v各个方向上未访问的邻接点,再从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,不断重复这个步骤,直至所有的顶点都被访问到。
讲的有些抽象吧,下面我用上面所讲的无向图进行说明。
BFS算法的实现方法:
分析:从A结点出发,先访问B,D,E结点,再分别从B,D,E三个结点出发,访问C,G结点,再从C,G结点出发,访问F,H结点,最后从H结点出发,访问I结点,此时,所有结点已搜索完毕,退出。
由图可以看出,广度优先搜索尽可能对横向搜索,而且需要保存已访问的结点,满足先访问的结点其邻接点应先访问,故我们可以想到,实现该算法应引入队列保存已访问的结点。
BFS算法的一般模板:
bool vst[maxn][maxn]; // 访问标记
int dir[4][2]={0,1,0,-1,1,0,-1,0}; // 方向向量
struct State // BFS 队列中的状态数据结构
{
int x,y; // 坐标位置
int count; // 搜索步数统计器
};
State vis[maxn];
bool CheckState(State s) // 约束条件检验
{
if(!vst[s.x][s.y] && ...) // 满足条件
return 1;
else // 约束条件冲突
return 0;
}
void bfs(State st)
{
queue <State> q; // BFS 队列
State now,next; // 定义2 个状态,当前和下一个
st.count=0; // 计数器清零
q.push(st); // 入队
vst[st.x][st.y]=1; // 访问标记
while(!q.empty())
{
now=q.front(); // 取队首元素进行扩展
if(now==G) // 出现目标态,此时为Step_Counter 的最小值,可以退出即可
{
...... // 做相关处理
return;
}
for(int i=0;i<4;i++)
{
next.x=now.x+dir[i][0]; // 按照规则生成 下一个状态
next.y=now.y+dir[i][1];
next.count=now.count+1; // 计数器加1
if(CheckState(next)) // 如果状态满足约束条件则入队
{
q.push(next);
vst[next.x][next.y]=1; //访问标记
}
}
q.pop(); // 队首元素出队
}
return;
}
下面举个例子来说明
迷宫的最短路径
给定一个大小为N×M的迷宫。迷宫由通道和墙壁组成,每一步可以向邻接的上下左右四个的通道移动。请求出从起点到终点所需的最小步数。请注意,本题假定从起点一定可以移动到终点。(N,M≤100)('#', '.' , 'S', 'G'分别表示墙壁、通道、起点和终点)
输入:
10 10
#S######.#
......#..#
.#.##.##.#
.#........
##.##.####
....#....#
.#######.#
....#.....
.####.###.
....#...G#
输出:
22
说明:题目选自挑战程序设计竞赛
分析:此题是一个标准的利用BFS算法求最短路径问题,根据BFS算法的特点:从近到远,层次遍历,易求得最短路径。首先将起始路径最短长度全部初始化为最大值,从起点开始,路径最短长度赋值为0。开始不断搜索,直到终点或队列为空,若此时最短长度仍为INF,则无最短路径,反之,可得从起点到终点的最短路径。
代码如下:
#include <iostream>
#include <cstdio>
#include <queue>
#define N 105
#define M 105
using namespace std;
const int INF=100000000;
int m,n;
int gx,gy;
int sx,sy;
char maze[N][M];
int d[N][M];
int dir[4][2]={0,1,0,-1,1,0,-1,0};
struct State
{
int x,y;
};
bool CheckState(State s)
{
if(s.x>=0&&s.x<n&&s.y>=0&&s.y<m&&maze[s.x][s.y]!='#'&&d[s.x][s.y]==INF)
return 1;
else
return 0;
}
void bfs(State st)
{
queue <State> q;
State now,next;
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
d[i][j]=INF;
}
q.push(State{sx,sy});
d[sx][sy]=0;
while(!q.empty())
{
now=q.front();
q.pop();
if(now.x==gx&&now.y==gy)
break;
for(int i=0;i<4;i++)
{
next.x=now.x+dir[i][0];
next.y=now.y+dir[i][1];
if(CheckState(next))
{
q.push(next);
d[next.x][next.y]=d[now.x][now.y]+1;
}
}
}
}
int main()
{
int i,j;
State vis;
scanf("%d%d",&n,&m);
for(i=0;i<n;i++)
scanf("%s",maze[i]);
for(i = 0; i < n; i++)
{
for (j = 0; j < M; j++)
{
if (maze[i][j] == 'S')
{
sx = i;
sy = j;
}
if (maze[i][j] == 'G')
{
gx = i;
gy = j;
}
}
}
bfs(vis);
printf("%d\n",d[gx][gy]);
return 0;
}