当前位置:首页 » 《随便一记》 » 正文

一文搞懂图的存储与遍历

6 人参与  2023年03月31日 10:49  分类 : 《随便一记》  评论

点击全文阅读


目录

一、图的相关概念1.1 简单图1.2 邻域1.3 度数1.4 路径1.5 连通1.5.1 无向图1.5.2 有向图 二、图的存储2.1 直接存边2.2 邻接矩阵2.3 邻接表2.4 链式前向星 三、图的遍历3.1 图的深度优先遍历3.2 图的广度优先遍历3.3 其他存图方式的BFS/DFS实现3.3.1 直接存边3.3.2 邻接矩阵3.3.3 邻接表 References

一、图的相关概念

图 G G G 是一个二元组 G = ( V , E ) G=(V,E) G=(V,E)。其中 V V V 是非空集,称为点集, V V V 中的每个元素称为顶点节点。 E E E 为各节点之间边的集合,称为边集。图 G G G 的点数 ∣ V ∣ |V| ∣V∣ 称作 G G G 的

当 V V V 和 E E E 都是有限集合时,称 G G G 为有限图。当 V V V 或 E E E 是无限集合时,称 G G G 为无限图

根据边是否有向可将图分为:无向图有向图混合图

无向图: E E E 中的每个元素都是一个无序二元组 ( u , v ) (u,v) (u,v),称作无向边,其中 u , v ∈ V u,v\in V u,v∈V。记 e = ( u , v ) e=(u,v) e=(u,v),则 u u u 和 v v v 称为 e e e 的端点;有向图: E E E 中的每个元素都是一个有序二元组 ( u , v ) (u,v) (u,v),有时也写作 u → v u\to v u→v,称作有向边。记 e = ( u , v ) e=(u,v) e=(u,v),称 u u u 为 e e e 的起点, v v v 为 e e e 的终点,起点和终点也称为 e e e 的端点;混合图: E E E 中既有有向边,又有无向边。

若 G G G 的每条边都被赋予一个数作为该边的,则称 G G G 为赋权图。如果这些权都是正实数,则称 G G G 为正权图

1.1 简单图

自环: 对 E E E 中的边 e = ( u , v ) e=(u,v) e=(u,v),若 u = v u=v u=v,则称 e e e 是一个自环。

重边: 若存在 e 1 , e 2 ∈ E e_1,e_2\in E e1​,e2​∈E 使得 e 1 = e 2 e_1=e_2 e1​=e2​,则称它们是(一组)重边。

简单图: 若一个图中没有自环和重边,则它被称为简单图。

多重图: 图中存在自环或重边。

? 在无向图中 ( u , v ) (u,v) (u,v) 和 ( v , u ) (v,u) (v,u) 算一组重边,但在有向图中不算。

1.2 邻域

在无向图 G = ( V , E ) G=(V,E) G=(V,E) 中,若对 u , v ∈ V u,v\in V u,v∈V,存在边 ( u , v ) (u,v) (u,v),则称 u u u 和 v v v 是相邻的。

一个顶点 v ∈ V v \in V v∈V 的邻域是所有与之相邻的顶点所构成的集合,记作 N ( v ) N(v) N(v)。一个点集 S S S 的邻域定义如下:

N ( S ) = ⋃ v ∈ S N ( v ) N(S)=\bigcup_{v\in S} N(v) N(S)=v∈S⋃​N(v)

1.3 度数

与一个顶点 v v v 关联的边的条数称作该顶点的,记作 d ( v ) d(v) d(v)。

对于无向简单图,有 d ( v ) = ∣ N ( v ) ∣ d(v)=|N(v)| d(v)=∣N(v)∣。根据 d ( v ) d(v) d(v) 的取值可对图中的顶点进行分类:

d ( v ) = { 0 , v   是孤立点 1 , v   是叶节点 / 悬挂点 2 k , v   是偶点 2 k + 1 , v   是奇点 ∣ V ∣ − 1 , v   是支配点 d(v)= \begin{cases} 0, &v\,是孤立点 \\ 1, &v\,是叶节点/悬挂点 \\ 2k,&v\,是偶点 \\ 2k+1,&v\,是奇点 \\ |V|-1,&v\,是支配点 \\ \end{cases} d(v)=⎩ ⎨ ⎧​0,1,2k,2k+1,∣V∣−1,​v是孤立点v是叶节点/悬挂点v是偶点v是奇点v是支配点​

? 自环 ( v , v ) (v,v) (v,v) 将对 d ( v ) d(v) d(v) 产生 2 2 2 的贡献。

对于图 G G G,所有节点的度数的最小值称为 G G G 的最小度,记作 δ ( G ) \delta (G) δ(G);最大值称为最大度,记作 Δ ( G ) \Delta (G) Δ(G)。即: δ ( G ) = min ⁡ v ∈ G d ( v ) \delta (G) = \min_{v \in G} d(v) δ(G)=minv∈G​d(v), Δ ( G ) = max ⁡ v ∈ G d ( v ) \Delta (G) = \max_{v \in G} d(v) Δ(G)=maxv∈G​d(v)。若 δ ( G ) = Δ ( G ) = k \delta(G)=\Delta(G)=k δ(G)=Δ(G)=k,即图中每个顶点的度数都是一个固定的常数 k k k,则称 G G G 为 k − k- k−正则图

握手定理(图论基本定理):对任何无向图,由于每条边都会贡献两个度,所以 ∑ v ∈ V d ( v ) = 2 ∣ E ∣ \sum_{v\in V}d(v)=2|E| ∑v∈V​d(v)=2∣E∣。

下面考虑有向图。以一个顶点 v v v 为起点的边的条数称为该顶点的出度,记作 d + ( v ) d^+(v) d+(v)。以一个顶点 v v v 为终点的边的条数称为该节点的入度,记作 d − ( v ) d^-(v) d−(v)。显然 d + ( v ) + d − ( v ) = d ( v ) d^+(v)+d^-(v)=d(v) d+(v)+d−(v)=d(v)。

由于每条边都会贡献一个出度和一个入度,所以

∑ v ∈ V d + ( v ) = ∑ v ∈ V d − ( v ) = ∣ E ∣ \sum_{v\in V}d^+(v)=\sum_{v\in V}d^-(v)=|E| v∈V∑​d+(v)=v∈V∑​d−(v)=∣E∣

1.4 路径

途径:途径是连接一连串顶点的边的序列,可以为有限或无限长度。形式化地说,一条有限途径 w w w 是一个边的序列 e 1 , e 2 , ⋯   , e k e_1, e_2, \cdots, e_k e1​,e2​,⋯,ek​,使得存在一个顶点序列 v 0 , v 1 , ⋯   , v k v_0, v_1, \cdots, v_k v0​,v1​,⋯,vk​ 满足 e i = ( v i − 1 , v i ) e_i = (v_{i-1}, v_i) ei​=(vi−1​,vi​)。这样的途径可以简写为 v 0 → v 1 → v 2 → ⋯ → v k v_0 \to v_1 \to v_2 \to \cdots \to v_k v0​→v1​→v2​→⋯→vk​。通常来说,边的数量 k k k 被称作这条途径的长度(如果边是带权的,长度通常指途径上的边权之和)。

:对于一条途径 w w w,若 e 1 , e 2 , ⋯   , e k e_1, e_2, \cdots, e_k e1​,e2​,⋯,ek​ 两两互不相同,则称 w w w 是一条迹。

路径:对于一条迹 w w w,若其连接的点的序列中点两两不同,则称 w w w 是一条路径。

? 以上三者的区别在于:途径的边和顶点都可以重复;迹的边不能重复,但顶点可以重复;路径的边和顶点都不能重复。

回路:对于一条迹 w w w,若 v 0 = v k v_0 = v_k v0​=vk​,则称 w w w 是一条回路。

环/圈:对于一条回路 w w w,若 v 0 = v k v_0 = v_k v0​=vk​ 是点序列中唯一重复出现的点对,则称 w w w 是一个环。

⚠️ 关于路径的定义在不同地方可能有所不同,如,「路径」可能指本文中的「途径」,「环」可能指本文中的「回路」。

1.5 连通

1.5.1 无向图

对于一张无向图 G = ( V , E ) G = (V, E) G=(V,E),对于 u , v ∈ V u, v \in V u,v∈V,若存在一条途径使得 v 0 = u , v k = v v_0 = u, v_k = v v0​=u,vk​=v,则称 u u u 和 v v v 是连通的。由定义,任意一个顶点和自身连通,任意一条边的两个端点连通。

若无向图 G G G 满足其中任意两个顶点均连通,则称 G G G 是连通图, G G G 的这一性质称作连通性

若 H H H 是 G G G 的一个连通子图,且不存在 F F F 满足 H ⊊ F ⊆ G H\subsetneq F \subseteq G H⊊F⊆G 且 F F F 为连通图,则称 H H H 是 G G G 的一个连通块/连通分量

1.5.2 有向图

对于一张有向图 G = ( V , E ) G = (V, E) G=(V,E),对于 u , v ∈ V u, v \in V u,v∈V,若存在一条途径使得 v 0 = u , v k = v v_0 = u, v_k = v v0​=u,vk​=v,则称 u u u 可达 v v v。由定义,任意一个顶点可达自身,任意一条边的起点可达终点。

若有向图 G G G 满足其中任意两个顶点互相可达,则称 G G G 是强连通的。若有向图 G G G 的边替换为无向边后可以得到一张连通图,则称原来这张有向图是弱连通的。

类似可得强连通分量弱连通分量的定义。

二、图的存储

图的输入格式通常为:

? 第一行包含两个整数,分别是 n n n 和 m m m,代表 ∣ V ∣ |V| ∣V∣ 和 ∣ E ∣ |E| ∣E∣。接下来 m m m 行,每行包含三个整数,分别是 a a a, b b b 和 c c c,代表起点,终点和边权。

2.1 直接存边

使用一个 vector 来存边,数组中的每个元素都包含了一条边的起点与终点(带边权的图还包含边权)。

struct Edge {    int a, b, c;};int main() {    int n, m;    cin >> n >> m;    vector<Edge> edges(m);  // 也可以先声明为全局变量,然后通过resize调整    for (int i = 0; i < m; i++)        cin >> edges[i].a >> edges[i].b >> edges[i].c;    return 0;}

复杂度分析:

查询是否存在某条边: O ( m ) O(m) O(m);遍历一个点的所有出边: O ( m ) O(m) O(m);遍历整张图: O ( n m ) O(nm) O(nm);空间复杂度: O ( m ) O(m) O(m)。

应用:

直接存边的遍历效率低下,一般不用于遍历图。

2.2 邻接矩阵

使用一个二维数组 adj 来存边,其中 adj[u][v] = 1 表示存在 u u u 到 v v v 的边,adj[u][v] = 0 表示不存在。如果是带边权的图,可以在 adj[u][v] 中存储 u u u 到 v v v 的边权。

int g[N][N];int main() {    int n, m;    cin >> n >> m;    while (m--) {        int a, b, c;        cin >> a >> b >> c;        g[a][b] = c;    }    return 0;}

复杂度分析:

查询是否存在某条边: O ( 1 ) O(1) O(1);遍历一个点的所有出边: O ( n ) O(n) O(n);遍历整张图: O ( n 2 ) O(n^2) O(n2);空间复杂度: O ( n 2 ) O(n^2) O(n2)。

应用:

邻接矩阵只适用于没有重边的情况;邻接矩阵在稀疏图上的效率很低,所以一般只会在稠密图上使用邻接矩阵。

2.3 邻接表

使用一个支持动态增加元素的数据结构构成的数组,如 vector<int> adj[N] 来存边,其中 adj[u] 存储的是点 u u u 的所有出边的相关信息(终点、边权等)。

vector<pair<int, int>> adj[N];int main() {    int n, m;    cin >> n >> m;    while (m--) {        int a, b, c;        cin >> a >> b >> c;        adj[a].emplace_back(b, c);    }    return 0;}

邻接表有很多种实现方式,我们还可以单独定义一个结构体或者开两个 vector<int> 型数组分别用来存储终点和边权。

复杂度分析:

查询是否存在 u u u 到 v v v 的边: O ( d + ( u ) ) O(d^+(u)) O(d+(u));遍历点 u u u 的所有出边: O ( d + ( u ) ) O(d^+(u)) O(d+(u));遍历整张图: O ( n + m ) O(n+m) O(n+m);空间复杂度: O ( m ) O(m) O(m)。

应用:

存各种图都很适合,除非有特殊需求;尤其适用于需要对一个点的所有出边进行排序的场合。排序之后,查边的复杂度降至 O ( log ⁡ d + ( u ) ) O(\log d^+(u)) O(logd+(u))。

2.4 链式前向星

之前的邻接表是基于 vector 来实现的,如果把 vector 换成用数组实现的链表,效率将会提高很多,而这样实现的邻接表又被称为链式前向星。

? 链式前向星和哈希表中的拉链法非常相似。

int h[N], e[N], ne[N], idx;  // e相当于val,ne相当于nxt,具体请参考上方的超链接int w[N];  // 用来存储每条边的权重// 向图中添加a到b的有向边,权重为cvoid add(int a, int b, int c) {    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;}int main() {    memset(h, -1, sizeof(h));  // 使用链式前向星必须进行初始化    int n, m;    cin >> n >> m;    while (m--) {        int a, b, c;        cin >> a >> b >> c;        add(a, b, c);    }    return 0;}

复杂度分析:

因其本质仍是邻接表,所以复杂度和2.3节中的相同。

应用:

存各种图都很适合,但不能快速查询一条边是否存在,也不能方便地对一个点的出边进行排序;优点是边是带编号的,有时会非常有用。

三、图的遍历

接下来提到的图均使用链式前向星来存储。

3.1 图的深度优先遍历

深度优先遍历也叫深度优先搜索(Depth First Search),遍历是手段,搜索是目的,通过遍历所有可能的情况以达到搜索的目的,因此「遍历」和「搜索」可以看作是两个的等价概念。

「一条路走到底,不撞南墙不回头」是对DFS的最直观描述。DFS最显著的特征在于其递归调用自身。DFS会对其访问过的点打上访问标记,在遍历图时跳过已打过标记的点,以确保每个点仅访问一次

DFS实现:

// 从节点u开始进行深度优先搜索void dfs(int u) {    st[u] = true;  // 给u打上已访问的标记    for (int i = h[u]; ~i; i = ne[i]) {        int v = e[i];        if (!st[v]) dfs(v);    }}

DFS的时间复杂度为 O ( n + m ) O(n+m) O(n+m),空间复杂度为 O ( n ) O(n) O(n)。

3.2 图的广度优先遍历

广度优先遍历(Breadth First Search)每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。这样做的结果是,BFS算法找到的路径是从起点开始的最短合法路径。

算法过程可以看做是图上火苗传播的过程:最开始只有起点着火了,在每一时刻,有火的节点都向它相邻的所有节点传播火苗。

BFS实现:

// 从节点u开始进行广度优先搜索void bfs(int u) {    queue<int> q;    st[u] = true, q.push(u);    while (!q.empty()) {        auto t = q.front();        q.pop();        for (int i = h[t]; ~i; i = ne[i]) {            int v = e[i];            if (!st[v]) st[v] = true, q.push(v);        }    }}

在BFS的过程中,也可以记录一些额外的信息。例如,我们可以开一个 d 数组用于记录起点到某个节点的最短距离,再开一个 p 数组记录是从哪个节点走到当前节点的。

void bfs(int u) {    queue<int> q;    st[u] = true, q.push(u);    d[u] = 0, p[u] = -1;  // 假设节点的编号均为正整数    while (!q.empty()) {        auto t = q.front();        q.pop();        for (int i = h[t]; ~i; i = ne[i]) {            int v = e[i];            if (!st[v]) {                st[v] = true, q.push(v);                d[v] = d[t] + 1, p[v] = t;            }        }    }}

假设路径终点是 x,那么从 ux 的最短路径长度就是 d[x],相应地,我们可以根据 p 数组还原出这条路径:

vector<int> restore(int x) {    vector<int> path;    for (int v = x; ~v; v = p[v])        path.push_back(v);    reverse(path.begin(), path.end());    return path;}

BFS的时空复杂度与DFS相同。

3.3 其他存图方式的BFS/DFS实现

3.3.1 直接存边

BFS:

void bfs(int u) {    queue<int> q;    st[u] = true, q.push(u);    while (!q.empty()) {        auto t = q.front();        q.pop();        for (auto &[a, b, c]: edges)            if (a == t && !st[b])                st[b] = true, q.push(b);    }}

DFS:

void dfs(int u) {    st[u] = true;    for (auto &[a, b, c]: edges)        if (a == u && !st[b]) dfs(b);}

3.3.2 邻接矩阵

假设图中节点的编号从 1 1 1 开始。

BFS:

void bfs(int u) {    queue<int> q;    st[u] = true, q.push(u);    while (!q.empty()) {        auto t = q.front();        q.pop();        for (int j = 1; j <= n; j++) {            if (g[t][j] && !st[j])                st[j] = true, q.push(j);        }    }}

DFS:

void dfs(int u) {    st[u] = true;    for (int j = 1; j <= n; j++)        if (g[u][j] && !st[j])            dfs(j);}

3.3.3 邻接表

BFS:

void bfs(int u) {    queue<int> q;    st[u] = true, q.push(u);    while (!q.empty()) {        auto t = q.front();        q.pop();        for (auto &[b, c]: adj[t])            if (!st[b]) st[b] = true, q.push(b);    }}

DFS:

void dfs(int u) {    st[u] = true;    for (auto &[b, c]: adj[u]) {        if (!st[b]) dfs(b);    }}

References

[1] https://oi-wiki.org/graph/


点击全文阅读


本文链接:http://zhangshiyu.com/post/57522.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1