图的遍历(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
(记录数组中当前已用了多少索引值)四个要素,若是还要记录节点t
和i
之间的距离,则还需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 n−sum比较, 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)的主要内容,如果未能解决你的问题,请参考以下文章