图的遍历(DFS,BFS,Topology Sort)

Posted 清水寺扫地僧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图的遍历(DFS,BFS,Topology Sort)相关的知识,希望对你有一定的参考价值。



1. 图的存储结构

  • 对于稠密图,使用邻接矩阵进行存储和表示。邻接矩阵即二维数组,数组中的每个元素分别表示一有向边的边权值,在实际使用时,我们在初始化时先按照字节设置为无穷大 ∞ \\infty ,实际使用memset()函数字节填充0x3f,为何使用该值见:编程中将无穷大常量的设定为0x3f3f3f3f。常用模板为:
#include <cstring> //为了使用memset()函数
#include <iostream>
using namespace std;
const int N = 100010;
int g[N][N];

...

int main() {
	memset(g, 0x3f, sizeof(g));
	...
}
  • 对于稀疏图,使用邻接链表。若是使用邻接矩阵的话会造成较大的空间浪费,这里对于邻接链表的构造,使用数组多链表的形式进行,也即将每个点与其所邻接的点构建一个链表,所有的点的链表组合起来就可以表示一个图。对于邻接链表,含有h[N](存储头节点下标索引(即对于idx),初始化将所有头节点索引设置为 − 1 -1 1),e[N](从 h [ t ] h[t] h[t] 进行遍历,则 e [ i ] e[i] e[i] 存储节点索引为 i i i 的节点的实际值,在实际使用中int j = e[i];取出节点 t t t 的邻接点),ne[N](存储链表中的下一节点,在遍历中i = ne[i]作为迭代方式),idx(记录数组中当前已用了多少索引值)四个要素,若是还要记录节点ti之间的距离,则还需w[N]( w [ i ] w[i] w[i] 表示由某个节点 t t t h [ t ] h[t] h[t] 遍历过来到节点 e [ i ] e[i] e[i],即由节点 e [ i ] e[i] e[i] 到节点 t t t 的边权值);同时最主要的操作是add(int a, int b, int c),表示的是增加一条由节点 a a a 指向节点 b b b 的边权值为 c c c 的边。常用模板为:
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010;
int h[N], e[N], ne[N], idx, w[N];

void add(int a, int b, int c) {
	e[idx] = b; //记录节点a有指向b的有向边
	ne[idx] = h[a]; //使用头插法将节点b插入到a的邻接链表当中,
	//将新的链表节点的next指针指向h[a]
	w[idx] = c; //记录节点a到节点b的距离/边权值
	h[a] = idx++; //更新节点a的邻接链表的头节点,同时完成了添加操作后idx须自加
}

...

int main() {
	memset(h, -1, sizeof(h));
	...
}


2. 图的遍历

2.1 图的DFS遍历

DFS是一个执著的人,每次搜索若不搜索到叶子节点,即遇到了死胡同绝不返回到上一步(也即是回溯)。对于DFS,我们知道一般使用的是栈这一数据结构,程序设计当中的递归就是使用递归栈来实现的,所以自然而言的我们想到使用递归方式进行图的DFS深度优先搜索遍历。同时为了记录图中的节点是否已经遍历过,所以我们需要增添一个bool st[N]数组对遍历情况加以记录。

DFS遍历的代码实现如下:

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
const int N = 100010;
int h[N], e[N], ne[N], idx; //数组含义见邻接链表的图表示方法
bool st[N]; //记录节点是否遍历过

void add(int a, int b) {
	e[idx] = b; ne[idx] = h[a]; h[a] = idx++;
}

void dfs(int u) {
	//将当前节点u标记为已经遍历过,然后对其子节点进行dfs遍历
	st[u] = true;
	//对节点u的的所有邻接点分别做dfs遍历
	for(int i = h[u]; i != -1; i = ne[i]) { 
		int j = e[i]; //取出与u节点相邻的节点j
		if(!st[j]) { //若是未遍历过,则进行dfs遍历
			dfs(j);
			printf("%d ", j);	
		}
	}
}

int main() {
	memset(h, -1, sizeof(h)); //初始化邻接链表的头节点们
	...
}

相关例题见:AcWing 846. 树的重心 (对于树的重心,可在DFS的搜索的过程中记录以遍历节点为根的树所含节点个数 s u m sum sum,同时可记录去掉该点后每个连通块中点的个数的最大值 r e s = m a x { d f s ( 1 ) , d f s ( 2 ) , . . . } res=max\\{dfs(1),dfs(2),...\\} res=max{dfs(1),dfs(2),...},再与 n − s u m n-sum nsum比较, r e s res res取两者中的最大值,最终结果取所有得到的 r e s res res当中的最小值即可)。


2.2 图的BFS遍历

BFS,即广度优先遍历,首先先和自己邻近的人打招呼,其次再和有一个中间人的人打招呼,以此类推,就像剥洋葱一样,只不过这次是从洋葱芯自内向外罢了。对于BFS常用的实现辅助数据结构为队列(queue),在遍历时,将头遍历节点从队列中取出并出队,再将头遍历节点的所有邻接点加入到队列当中去,如此进行循环,直到队列的队头和队尾的索引值相等/队列为空为止,即可实现图的BFS遍历。

BFS遍历的实现代码如下:

//这里不使用stl当中的queue容器实现,而是使用一个数组模拟队列的行为
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int N = 100010;
int h[N], e[N], ne[N], idx;
int q[N], hh, tt; //hh表示队列的头节点索引,tt表示队尾节点索引
bool st[N]; //记录节点是否遍历过

void add(int a, int b) {
	e[idx] = b; ne[idx] = h[a]; h[a] = idx++;
}

void bfs(int u) {
	st[u] = true;
	q[hh] = u;
	
	while(hh <= tt) { //hh<=tt判断队列不为空
		int t = q[hh++]; //将队头节点出队,并将队头索引值自增1
		
		//对队头节点的所有邻接点进行遍历
		for(int i = h[t]; i != -1; i = ne[i]) { 
			int j = e[i]; //取出队头邻接点的实际值,即节点j
			if(!st[j]) { //若是节点j尚未遍历过,则将其加入队列
				q[++tt] = j; //将节点j加入队列
				printf("%d ", j);
			}	
		}
	}		
}

int main() {
	memset(h, -1, sizeof(h));
	...
}

正是这个实现的过程,所以可以料想到,若是当图中的边的边权值都为相同的正值时,则根据BFS遍历的层次可以求得所遍历的点到出发点的最短距离。相关例题见:AcWing 844. 走迷宫(本人使用稠密图的表示方式实现的BFS遍历过程)。



3. 图的拓扑排序(Topology Sort)

若一个由图中所有点构成的序列 A A A 满足:对于图中的每条边 ( x , y ) (x,y) (x,y) x x x A A A 中都出现在 y y y 之前,则称 A A A 是该图的一个拓扑序列。可以理解为图中不含有环形链表。

对于图的拓扑排序,是有向无环图(DAG)的特有性质,即若一个图是有向无环图,则其必存在一拓扑排序,使得序列前面的点只有向后的出边而无来自后边点的入度。同时另一性质为,若图是有向无环图,则图中一定存在入度为0的节点。

例题见:848. 有向图的拓扑序列。求一个图的拓扑排序的方式为,在构造图的过程中,记录每个节点的入度。在求解时,首先遍历所有的图中节点,找出所有入度为 0 0 0的节点,将它们塞入队列当中,依次取出队头节点,将该节点的所有出边所指向的节点的入度减少(即删边操作),若是所指节点的入度变为 0 0 0,则将所指节点加入队列中。重复以上操作,直至队列为空,若是队列数组所用的索引数等于图中节点数,则说明有拓扑排序序列,且队列数组中所存储的序列即为拓扑序列之一,否则没有拓扑排序序列。使用的遍历方法为BFS遍历。

求解拓扑排序的代码实现如下:

 #include <iostream>
 #include <cstring>
 
 using namespace std;
 
 const int N = 100010, M = N * 2;
 int n, m;
 int h[N], e[M], ne[M], idx;
 int d[N];
 int q[N], tt, hh;
 
 void add(int a, int b) {
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
 }
 
 bool topologySort() {
 	//对图中的节点进行遍历,找出入度为0的点加入到队列当中
    for(int i = 1; i <= n; ++i) 
        if(!d[i]) q[tt++] = i; 
        
    while(hh <= tt) { //若队列不为空
        int t = q[hh++]; //取出队头元素,并将队头弹出
        for(int i = h[t]; i != -1; i = ne[i]) { //遍历队头节点的邻接点
            int j = e[i];
            d[j]--; //删减相应所指向点的入度
            if(!d[j]) q[tt++] = j; //若是入度为0,则加入到队列当中
        }
    }
    return tt == n; //若是曾存在于队列中的节点数等于图的节点数,则有拓扑排序序列
 }
 
 int main() {
    memset(h, -1, sizeof(h));
    cin >> n >> m;
    
    for(int i = 0; i < m; ++i) {
        int a, b;
        cin >> a >> b;
        d[b]++; //记录节点b的入度
        add(a, b); //添加a->b的有向边
    }
    
    if(topologySort())
        for(int i = 0; i < n; ++i) 
            cout << q[i] << " ";
    else puts("-1");
     
    return 0;
 }

以上是关于图的遍历(DFS,BFS,Topology Sort)的主要内容,如果未能解决你的问题,请参考以下文章

算法导论—无向图的遍历(BFS+DFS,MATLAB)

(王道408考研数据结构)第六章图-第三节:图的遍历(DFS和BFS)

图的DFS和BFS

图的遍历——DFS和BFS模板(一般的图)

图的遍历 (dfs与bfs)x

图的深度优先遍历DFS和广度优先遍历BFS(邻接矩阵存储)超详细完整代码进阶版