- 「数据结构详解·一」树的初步
- 「数据结构详解·二」二叉树的初步
- 「数据结构详解·三」栈
- 「数据结构详解·四」队列
- 「数据结构详解·五」链表
- 「数据结构详解·六」哈希表
- 「数据结构详解·七」并查集的初步
- 「数据结构详解·八」带权并查集 & 扩展域并查集
- 「数据结构详解·九」图的初步
注意:本章主要讲解图的存储和图的遍历。
1. 图的定义、构成和术语
图(Graph),我们可以把它定义成一个二元组 G = ( V , E ) G=(V,E) G=(V,E)。其中 V V V 代表 Vertices Set \text{Vertices Set} Vertices Set,即顶集,顶集中的元素称为顶点(Vertex),写作 V ( G ) V(G) V(G); E E E 代表 Edges Set \text{Edges Set} Edges Set,即边集,边集中的元素称为边(Edge),写作 E ( G ) E(G) E(G)。 V , E V,E V,E 无交集。 E E E 中的元素也是二元组,为 ( x , y ) (x,y) (x,y),且 x , y ∈ V x,y\in V x,y∈V。
图还有一种三元组的定义,这里不再赘述,感兴趣的读者可以自行查阅。
以上定义可能较为形式化,较难理解。读者一会可以通过下面的例子来理解。
图还分为有向图和无向图——也就是说,有向图的边有方向,即有向边,无向图的边无方向,即无向边。特殊地,如果一个图的边带有权值,则称为带权图。
以下分别展示了有向图、无向图、无向带权图。同时我会用形式语言、通俗语言和举例来帮助大家更好理解。
-
G
G
G 中点集
V
V
V 的大小,称作图
G
G
G 的阶(Order)。
一个图中顶点的个数,就是一个图的阶。
图 1 ∼ 3 1\sim 3 1∼3 的阶都是 6 6 6。 - 当存在图
G
′
=
(
V
′
,
E
′
)
G'=(V',E')
G′=(V′,E′),其中
V
′
∈
V
,
E
′
∈
E
V'\in V,E'\in E
V′∈V,E′∈E,则
G
′
G'
G′ 称作图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E) 的子图(Sub-Graph)。
从一个图中“抠”下来的图(即两个图具有包含关系),则被包含的图是另一个图的子图。
图 1 ∼ 3 1\sim 3 1∼3 分别都是各自的子图。 - 当存在图
G
′
G'
G′ 是图
G
G
G 的子图,且
V
(
G
′
)
=
V
(
G
)
V(G')=V(G)
V(G′)=V(G),则称为图
G
′
G'
G′ 是图
G
G
G 的生成子图(Spanning Sub-Graph)。
一个图是另一个图的子图,且这两个图顶点数相同,则这个子图是另一个图的生成子图。
图 1 1 1 中,若去掉了 1 → 5 , 1 → 3 1\to 5,1\to 3 1→5,1→3,则所产生的图就是原图的生成子图。 - 与一个顶点
V
V
V 相关联的边的条数,称为
V
V
V 的度(Degree),记作
d
(
V
)
d(V)
d(V)。
在一个图中的一个顶点,和它相连的边的条数,就是它的度。
图 2 2 2 中, 5 5 5 的度为 4 4 4, 2 2 2 的度为 2 2 2, 1 1 1 的度为 3 3 3。 - 对于有向图,与一个顶点
V
V
V 相关联的边中,以
V
V
V 为终点的边的个数,称为
V
V
V 的入度(In-Degree),这些边称为 入边(In-Edge);与一个顶点
V
V
V 相关联的边中,以
V
V
V 为起点的边的个数,称为
V
V
V 的出度(Out-Degree),这些边称为出边(Out-Edge)。
有向图中的一个顶点,所有指向它的边的个数,就是它的入度,这些边是入边;除了这些边以外和它相连的边的条数,就是它的出度,这些边是出边。
图 1 1 1 中, 5 5 5 的入度为 3 3 3,出度为 1 1 1; 6 6 6 的入度为 3 3 3,出度为 0 0 0。 - 若一条边的两个顶点相同,则这条边是一个自环(Loop)(并查集的初始化其实就是自环)。
- 图中的一条闭合的路径,称为环(Circuit)。
以上是一些基本的概念,其他大多数概念可以通过逐步的学习理解。
另外注意:树是一种特殊的图。
2. 图的存储
和 「数据结构详解·一」树的初步 的第二部分相同。
无向图连接
u
,
v
u,v
u,v,就是 g[u][v]=g[v][u]=1;
;有向图就是 g[u][v]=1;
。
但是这里再补充一部分内容。
2-1. 边表
不常用。主要在 Kruskal(一种最小生成树算法,将在以后讲到)等算法中用到。
直接用结构体存,
g
i
=
(
u
,
v
)
g_i=(u,v)
gi=(u,v)。对于无向图,表示
u
−
v
u-v
u−v;对于有向图,表示
u
→
v
u\to v
u→v。
struct node{
int u,v;
}g[100005];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>a[i].u>>a[i].v;
}
//...
}
对于寻找与结点 x x x 相连的边,需要 O ( m ) O(m) O(m) 的时间,显然很慢,故不常用。
2-2. 带权图的存储
邻接矩阵只要将 g[u][v]=1;
变为 g[u][v]=w;
即可,但是缺点是无法存重边(比如,输入中给出了 u=1 v=2 w=3
和 u=1 v=2 w=-2
,意味着
1
,
2
1,2
1,2 间有两条边)。另外,矩阵初始化要变为
+
∞
+\infin
+∞ 或
−
∞
-\infin
−∞ 以表示两条边未连接(因为可能有负权边)。
邻接表只要用 pair 或结构体来代替 vector 中的内容(对于 a x a_x ax 有 y , z y,z y,z 分别代表连接的边和权值),无需担心重边。
边表和邻接表类似,修改结构体的内容即可。
3. 图的遍历
所有代码均为邻接表存储。
3-1. 深度优先遍历(DFS)
和 「数据结构详解·一」树的初步 4 − 1 \mathbf{4-1} 4−1 类似,但是由于图的特性,我们要记录 f i f_i fi 表示其是否走过。
void dfs(int p)//p 为当前节点编号
{
if(f[p]) return;//走过了
cout<<p<<' ';
f[p]=1;
for(auto i:g[p])
{
dfs(i);
}
}
3-2. 广度优先遍历(BFS)
和 「数据结构详解·一」树的初步 4 − 3 \mathbf{4-3} 4−3 类似,但是由于图的特性,我们要记录 f i f_i fi 表示其是否走过。
queue<int>q;
void bfs()
{
q.push(root);//root 为遍历的起始节点
while(!q.empty())
{
int x=q.front();
q.pop();
if(f[x]) continue;
f[x]=1;
cout<<x<<' ';
for(auto i:g[x])
{
q.push(i);
}
}
}
4. 例题详解
4-1. Luogu P5318 【深基18.例3】查找文献
只要将本文章的 3 \mathbf{3} 3 的内容套用即可。
4-2. Luogu P3916 图的遍历
如果我们直接暴力对于每个点查找,那是必定超时的。
题目问的是每个点所到达编号最大的点,那我们可以先反向建图,然后编号从大到小搜索,第一次搜索到的点就是答案(因为第二次搜到时编号因为是越来越小,因此不会比之前大)。
那这样的话,每个点只会遍历的到一次,时间复杂度由
O
(
(
n
+
m
)
n
)
O((n+m)n)
O((n+m)n) 变为了
O
(
n
+
m
)
O(n+m)
O(n+m)。
参考代码:
#include<bits/stdc++.h>
using namespace std;
vector<int>g[100005];
int n,m,ans[100005];
void dfs(int fr,int p)
{
if(ans[p]) return;//搜过了
ans[p]=fr;
for(auto i:g[p])
{
dfs(fr,i);
}
}
int main()
{
cin>>n>>m;
while(m--)
{
int u,v;
cin>>u>>v;
g[v].push_back(u);//反向建图
}
for(int i=n;i>=1;i--)
{
dfs(i,i);
}
for(int i=1;i<=n;i++)
{
cout<<ans[i]<<' ';
}
return 0;
}
5. 巩固练习
- Luogu B3643 图的存储
- Luogu B3613 图的存储与出边的排序