[知识点] 8.2 图的存储与遍历

总目录 > 8 图论 > 8.2 图的存储与遍历

前言

写到这里,不免想起当年高一的 NOIP2014 因为把邻接链表给写错了而差 5 分错过一等奖导致一路再起不能。

子目录列表

1、概述

2、边存储

3、邻接矩阵

4、邻接表

5、图的遍历

 

8.2 图的存储与遍历

1、概述

数据结构往往有两种存储方式 —— 顺序和链式。顺序采用数组,链式多采用链表,图虽然结构复杂,但其实也是以这两种思路对其进行存储,只不过更为麻烦。

首先,图的读入方式基本上是给定图的点数 n边数 m,然后对于 m 条边,每条边给定 u 和 v

下面以有向赋权图举例(无向图直接视作双向边即可)

 

2、边存储

① 概念

直接将给定的 m 组 (u, v) 用数组存下来。

② 示例

③ 优劣势

优势:

最小生成树(请参见:8.3 最小生成树)中的 Kruskal 算法中,因为需要将边按边权排序,需要直接存边。

劣势:

遍历效率极低,时间复杂度高达 O(n * m),除特别需求基本不会采用。

④ 代码

void add(int o, int _u, int _v, int _w) {
    u[o] = _u, v[o] = _v, w[o] = _w;
}

 

3、邻接矩阵

① 概念

使用一个二维数组 a 来存储边,其中 a[u][v] = 1(无权图)/ 权值(赋权图) 时表示 u 和 v 之间存在一条边,= 0 时则不存在。根据权值范围,也可以用 -1 或其他方式表示不存在边。属于顺序存储结构。

② 示例

③ 优劣势

优势:

构建简单,能 O(1) 查询两点之间是否存在边。

劣势:

空间复杂度过高 —— O(n ^ 2),尤其在稀疏图中浪费了大量空间,n 在 5000+ 时空间就高达 128MB 的上限了。同时,邻接矩阵无法判断重边,遍历效率 O(n ^ 2) 同样不理想。

④ 代码

void add(int u, int v, int w) {
    a[u][v] = w;
}

 

4、邻接表

① 概念

使用 n 个可动态调整元素的数据结构构成的数组来存边,其中第 u 个数组的第 i 个元素表示以点 u 为起点的第 i 个终点。如果是赋权图,则需要使用结构体或类来同时存储终点与边权,必要的话可以存更多信息。属于链式存储结构

对于 C++,可以使用 STL 中的 vector;普适性更强的则是使用邻接链表(链式前向星),即使用 n 个链表来存储。

② 示例

③ 构建步骤

以使用链表为例。首先,对每个读入的边赋予一个编号 [1, m],以便于链表的建立与访问。建立一个类 Edge,存储每条边的终点 v边的权值 w,以及预留的链表中后继边的指针 nxt(不需要使用指针变量,但本质上是指针)。建立一个 h 数组,其中 h[i] 表示以结点 i 为起点的边的链表头编号,初始为 0。每读入一条边 (u, v),则更新第 u 条链表,将边存入 Edge 并链到给链表的链表头,其后继边的编号即原本链表头的边的编号 h[u],再更新链表头编号 h[u] 为当前的 Edge。

觉得麻烦的可以使用 vector,效率会较低,但更易理解。

④ 优劣势

优势:

基本能应用到所有场合,遍历时间复杂度 O(n + m),空间复杂度 O(m) 都是无与伦比的。

劣势:

构建与遍历等操作使用起来会比邻接矩阵麻烦,不能 O(1) 查询边的存在。

其他:

如果需要对以某个点为起点的所有出边进行某种排序,则只能使用 vector。

⑤ 代码(使用链表)

1 class Edge {
2 public:
3     int v, w, nxt;
4 } e[MAXM];
5 
6 void add(int u, int v, int w) {
7     tot++, e[tot] = (Edge) {v, w, h[u]}, h[u] = tot;
8 }

 

5、图的遍历

在第三章的 3.1  DFS / BFS 搜索 中,我们已经介绍了 DFS / BFS,广义上它们用于进行各种搜索,而狭义本属于树与图的一种遍历算法,这里再以图的视角回顾一次。

DFS,深度优先搜索,指在对树或图的遍历中,优先访问深度更深的结点。对于树而言,其实就是先序遍历,这里不提;对于图而言,DFS 相当于每次访问到一个新的结点后,马上访问其第一个后继结点,直到没有后继再回溯,再访问下一个后继结点,每次访问打上访问标记

BFS,深度优先搜索,指在对树或图的遍历中,优先访问当前深度的结点。对于树而言,其实就是层次遍历,这里不提;对于图而言,DFS 相当于每次访问到一个新的结点后,会将其后继结点全部访问一次,再逐一访问其后继结点,每次访问打上访问标记。

以上面的图为例,体现 DFS 与 BFS 的访问顺序的不同(假设从结点 1 开始访问):

以邻接链表为图的存储方式,两种算法时间复杂度均为 O(n + m),空间复杂度均为 O(n)。

当然,遍历只是一个框架,实际运用中,是在遍历的过程中求得图上的各种信息以输出或进行后续操作,比如求最短路径,连通块,最小环等等。

下面给出 DFS 与 BFS 最基础的遍历代码:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 #define MAXN 1005
 5 #define MAXM 10005
 6 
 7 int h[MAXN], tot, n, m, u, v, w, vis[MAXN]; 
 8 
 9 class Edge {
10 public:
11     int v, w, nxt;
12 } e[MAXM];
13 
14 void add(int u, int v, int w) {
15     tot++, e[tot] = (Edge) {v, w, h[u]}, h[u] = tot;
16 }
17 
18 void dfs(int o) {
19     for (int x = h[o]; x; x = e[o].nxt) {
20         int v = e[x].v;
21         if (!vis[v]) vis[v] = 1, dfs(v);
22     }
23 }
24 
25 void bfs() {
26     int head = 1, tail = 2, q[MAXN];
27     q[1] = vis[1] = 1;
28     while (head != tail) {
29         int o = q[head];
30         for (int x = h[o]; x; x = e[o].nxt) {
31             int v = e[x].v;
32             if (!vis[v]) {
33                 vis[v] = 1;
34                 q[tail] = v, tail++;
35             }
36         }
37         head++;
38     }
39 }
40 
41 int main() {
42     cin >> n >> m;
43     for (int i = 1; i <= m; i++) {
44         cin >> u >> v >> w;
45         add(u, v, w), add(v, u, w);
46     }
47     vis[1] = 1, dfs(1);
48     memset(vis, 0, sizeof(vis));
49     bfs();
50     return 0;
51 }

 

posted @ 2020-06-03 23:58  jinkun113  阅读(628)  评论(0编辑  收藏  举报