图的概念、存储与遍历
一、图
图 (Graph) 是由顶点的非空集合和顶点 (结点) 之间边的集合组成,通常表示为 G = ( V , E ) G =(V,E) G=(V,E) ;
其中 G G G 表示一个图, V V V 是图 G G G 中的顶点集合 (Vertex Set) , E E E 是图 G G G 中边的集合 (Edge Set) ;
二、概念
1. 有向图
如果边 x → y x \rightarrow y x→y 在边集中,同时 y → x y \rightarrow x y→x 不在边集中,则称图 G = ( V , E ) G=(V, E) G=(V,E) 为有向图;
2. 无向图
如果边 x → y x \rightarrow y x→y 在边集中,同时 y → x y \rightarrow x y→x 也在边集中,则称图 G = ( V , E ) G=(V, E) G=(V,E) 为无向图;
3. 结点的度
无向图中与结点相连的边的数目,称为结点的度;
节点编号 | 度 |
---|---|
1 | 3 |
2 | 2 |
3 | 3 |
4 | 1 |
5 | 4 |
6 | 3 |
4. 结点的入度
有向图中以结点为终点的有向边的数目为此节点的入度;
节点编号 | 入度 |
---|---|
1 | 0 |
2 | 0 |
3 | 1 |
4 | 1 |
5 | 3 |
6 | 3 |
5. 结点的出度
有向图中以结点为起点的有向边的数目为此节点的出度;
节点编号 | 入度 |
---|---|
1 | 3 |
2 | 2 |
3 | 2 |
4 | 0 |
5 | 1 |
6 | 0 |
6. 权值
边的权值即为边的大小或者长度;
7. 连通图
某图中任意两个顶点之间都连通,则称此图是连通图;
8. 联通分量
非连通图的极大连通子图称为连通分量;
9. 强连通分量
非连通图的最大连通子图称为强连通分量;
10. 回路
如果图中存在一个结点,使得从该结点出发沿着边走能够回到起始结点,则称该路径为回路 (环) ;
回路1, 1 → 2 → 5 → 1 1\rightarrow 2 \rightarrow 5 \rightarrow 1 1→2→5→1 ;
回路2, 1 → 2 → 3 → 4 → 5 → 1 1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow 1 1→2→3→4→5→1 ;
回路3, 2 → 5 → 1 → 2 2 \rightarrow 5 \rightarrow 1 \rightarrow 2 2→5→1→2 ;
回路4, 2 → 3 → 4 → 5 → 2 2 \rightarrow 3 \rightarrow 4 \rightarrow 5 \rightarrow 2 2→3→4→5→2 ;
回路5, 3 → 4 → 5 → 2 → 3 3 \rightarrow 4 \rightarrow 5 \rightarrow 2 \rightarrow 3 3→4→5→2→3 ;
11. 稀疏图
对于图,其边的条数 ∣ E ∣ |E| ∣E∣ 远小于 ∣ V ∣ 2 |V|^2 ∣V∣2 的图称为稀疏图;
12、稠密图
对于图,其边的条数 ∣ E ∣ |E| ∣E∣ 接近 ∣ V ∣ 2 |V|^2 ∣V∣2 的图称为稀疏图;
边越多,图越稠密,反之越稀疏;
三、图的存储
1. 邻接矩阵
思路
对于具有 n n n 个顶点的有向无权图,可以使用一个 n ∗ n n * n n∗n 的矩阵来表示图中的边;
令矩阵为 g g g ,若顶点 i i i 到顶点 j j j 之间有一条边,则置 g [ i ] [ j ] = 1 g[i][j] = 1 g[i][j]=1,否则置 g [ i ] [ j ] = 0 g[i][j]=0 g[i][j]=0 ;
对于有向有权图,若顶点 i i i 到顶点 j j j 之间有一条边权值为 a i , j a_{i,j} ai,j ,则置 g [ i ] [ j ] = a i , j g[i][j]=a_{i,j} g[i][j]=ai,j,否则置 g [ i ] [ j ] = ∞ g[i][j]=\infty g[i][j]=∞ ;
对于其优缺点,
-
优点,可以快速查询某条边是否存在;
-
缺点,若要查询某个顶点相连的顶点,需要对矩阵进行遍历,不是很方便;其次当顶点数量较多但边数较少 (稀疏图) ,邻接矩阵表示法效率不高,矩阵中会有大量的元素为 0 ( ∞ ) (\infty) (∞) ;
无向图的邻接矩阵关于左上角到右下角对称;
实现
定义 g [ i ] [ j ] g[i][j] g[i][j] 表示从点 i i i 到点 j j j 的边的权值,定义如下,
g [ i ] [ j ] = { 1 或权值 ( 当 v i 与 v j 之间有边或弧时,取值为 1 或权值 ) 0 或极大值 ( 当 v i 与 v j 之间无边或弧时,取值为 0 或极大值 ) g[i][j] = \begin{cases} 1 或 权值 & (当v_i 与 v_j 之间有边或弧时,取值为1或权值) \\ 0 或 极大值 & (当v_i 与 v_j 之间无边或弧时,取值为0或极大值) \end{cases} g[i][j]={1或权值0或极大值(当vi与vj之间有边或弧时,取值为1或权值)(当vi与vj之间无边或弧时,取值为0或极大值)
邻接矩阵一般适用于稠密图;
例子
用邻接矩阵表示:
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
1 | 0 | ∞ \infty ∞ | 1 | ∞ \infty ∞ | 4 | 3 |
2 | ∞ \infty ∞ | 0 | ∞ \infty ∞ | ∞ \infty ∞ | 2 | 10 |
3 | ∞ \infty ∞ | ∞ \infty ∞ | 0 | 5 | 6 | ∞ \infty ∞ |
4 | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ | 0 | ∞ \infty ∞ | ∞ \infty ∞ |
5 | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ | 0 | 7 |
6 | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ | ∞ \infty ∞ | 0 |
代码
有向有权图
int g[MAXN][MAXN]; // 定义g数组,表示从点i到点j的边的权值
void input(int n) { //n为边数;
memset(g, 127, sizeof(g)); // 初始化为极大值;
for (int i = 1; i <= n; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
g[x][y] = z;
}
}
有向无权图
int g[MAXN][MAXN]; // 定义g数组,表示从点i到点j的边的权值
void input(int n) { //n为边数;
for (int i = 1; i <= n; i++) {
int x, y;
scanf("%d %d", &x, &y);
g[x][y] = 1;
}
}
无向有权图
int g[MAXN][MAXN]; // 定义g数组,表示从点i到点j的边的权值
void input(int n) { //n为边数;
memset(g, 127, sizeof(g)); // 初始化为极大值;
for (int i = 1; i <= n; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
g[x][y] = z;
g[y][x] = z; // 无向图对称性
}
}
无向无权图
int g[MAXN][MAXN]; // 定义g数组,表示从点i到点j的边的权值
void input(int n) { //n为边数;
for (int i = 1; i <= n; i++) {
int x, y;
scanf("%d %d", &x, &y);
g[x][y] = 1;
g[y][x] = 1; // 无向图对称性
}
}
2. 邻接表
思路
邻接表是一种顺序分配和链式分配相结合的存储结构;
邻接表由多个单链表组成,每个单链表的表头结点所对应的顶点存在相邻顶点,把相邻顶点依次存放于表头结点所指向的单向链表中;
实现
使用 vector[i]
模拟链表,存储以
i
i
i 为起点的边的终点;
若为有权图,则将 vector
内存储结构体表示边权与所达点即可;
例子
用邻接表存储
代码
有向有权图
struct edge {
int to, tot; // to 为终点,tot为边权
};
vector < edge > g[MAXN];
void input(int n) {
for (int i = 1; i <= n; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
g[x].push_back(edge({y, z}));
}
}
有向无权图
vector < int > g[MAXN]; // 直接存储所达点
void input(int n) {
for (int i = 1; i <= n; i++) {
int x, y;
scanf("%d %d", &x, &y);
g[x].push_back(y);
}
}
无向有权图
struct edge {
int to, tot; // to 为终点,tot为边权
};
vector < edge > g[MAXN];
void input(int n) {
for (int i = 1; i <= n; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
g[x].push_back(edge({y, z}));
g[y].push_back(edge({x, z})); // 无向图对称性
}
}
无向无权图
vector < int > g[MAXN]; // 直接存储所达点
void input(int n) {
for (int i = 1; i <= n; i++) {
int x, y;
scanf("%d %d", &x, &y);
g[x].push_back(y);
g[y].push_back(x); // 无向图对称性
}
}
3. 链式前向星
思路
链式前向星是邻接表的静态建表方式,采用数组模拟链表的方式实现邻接表的功能;
实现
定义 4 个数组,含义分别如下:
- v e r ver ver ,存储每条边的终点,例如 v e r [ i ] ver[i] ver[i] 存储的是编号为 i i i 的边到达的终点;
- e d g e edge edge ,存储每条边的权值,例如 e d g e [ i ] edge[i] edge[i] 存储的是编号为 i i i 的边的权值;
- h e a d head head ,存储每个单链表的头节点直接相连的边的编号,例如 h e a d [ x ] head[x] head[x] 存储的是以 x x x 为头节点直接相连的有向边 ( x , y ) (x, y) (x,y) 编号;
- n x t nxt nxt ,存储每个边下一条边的编号,假设编号为 i i i 的边为 ( x , y ) (x, y) (x,y) ,则 n x t [ i ] nxt[i] nxt[i] 表示在邻接表中与 y y y 直接相连的边的编号;
例子
如上有向图,按照(1,2),(2,3),(2,5),(5,4),(5,1)存放在邻接表中,下图展示了链式前向星的存储过程。
关键代码
//添加(x,y)有向边,权值为z
void add (int x, int y, int z) {
ver[++tot] = y;
edge[tot] = z;
nxt[tot] = head[x];
head[x] = tot;
//在头节点x后插入(x,y)边
}
//访问以x为头节点的单链表所有边
for(int i = head[x]; i; i = nxt[i]) {
int y = ver[i];
z = edge[i];
}
//找到了(x,y)边,权值为z
链式前向星的空间复杂度为 O ( n + m ) O(n+m) O(n+m) ,其中 n n n 为节点数量, m m m 为边数量;
四、图的遍历
1. 深度优先搜索遍历
思路
图的深度优先搜索 (Depth-First-Search) ,由树的先根遍历的推广;
当图中所有顶点均未被访问时,从顶点 v v v 出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和 v v v 有路径相通的顶点都被访问到;
若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止;
特点
选定一个出发点后进行遍历,对于一个节点,一直遍历到与其有公共祖先的出度为 0 的节点,再继续遍历其他节点;
依此重复,直到所有与选定点相通的所有顶点都被遍历;
过程
-
从图中某个顶点 v i v_i vi 出发,首先访问 v i v_i vi ;
-
访问结点 v i v_i vi 的第一个邻接点,以这个节点的邻接点 v t v_t vt 作为一个新结点,访问 v t v_t vt 的所有邻接点,直到以 v t v_t vt 出发的所有结点都被访问;
-
回溯到 v i v_i vi 的下一个未被访问过的邻接点,以这个邻结点为新节点,重复步骤 2 ,直到图中所有与 v i v_i vi 相通的所有节点都被访问;
-
若此时图中仍有未被访问的结点,则另选图中的一个未被访问的顶点作为起始点,重复步骤 1 ,直到图中的所有节点均被访问;
例子
图的 DFS 序列为, v 0 → v 1 → v 3 → v 7 → v 4 → v 2 → v 5 → v 6 v_0 \rightarrow v_1 \rightarrow v_3 \rightarrow v_7 \rightarrow v_4 \rightarrow v_2 \rightarrow v_5 \rightarrow v_6 v0→v1→v3→v7→v4→v2→v5→v6 ;
图的 DFS 序列为, v 0 → v 1 → v 3 → v 2 → v 4 → v 5 → v 6 → v 7 v_0 \rightarrow v_1 \rightarrow v_3 \rightarrow v_2 \rightarrow v_4 \rightarrow v_5 \rightarrow v_6 \rightarrow v_7 v0→v1→v3→v2→v4→v5→v6→v7;
代码
以邻接表存图为例;
int vis[MAXN];
void dfs(int i) {
vis[i] = true;
for (int j = 0; j < g[i].size(); j++) {
if (!vis[g[i][j]]) {
vis[g[i][j]] = true;
dfs(g[i][j]);
}
}
}
for (int i = 1; i <= n; i++) {
if (!vis[i]) dfs(i);
}
2. 广度优先搜索遍历
思路
在广度优先遍历过程中,如果与当前顶点相连接的其他顶点尚未处理完毕,则不会处理下一个未访问顶点;
广度优先遍历则首先会发现与起始顶点 s s s 距离为 k k k 条边的所有顶点,然后才会发现与 s s s 距离为 k + 1 k+1 k+1 条边的顶点;
例子
图的 BFS 序列为, v 0 → v 1 → v 2 → v 3 → v 4 → v 5 → v 6 → v 8 v_0 \rightarrow v_1 \rightarrow v_2 \rightarrow v_3 \rightarrow v_4 \rightarrow v_5 \rightarrow v_6 \rightarrow v_8 v0→v1→v2→v3→v4→v5→v6→v8
图的 BFS 序列为, v 0 → v 1 → v 2 → v 3 → v 4 → v 5 → v 6 → v 7 v_0 \rightarrow v_1 \rightarrow v_2 \rightarrow v_3 \rightarrow v_4 \rightarrow v_5 \rightarrow v_6 \rightarrow v_7 v0→v1→v2→v3→v4→v5→v6→v7
代码
以邻接表存图为例;
bool vis[MAXN];
void bfs(int s) {
queue < int > q;
q.push(s);
vis[s] = true;
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = 0; i < g[x].size(); i++) {
if (!vis[g[x][i]]) {
q.push(g[x][i]);
vis[g[x][i]] = true;
}
}
}
}
for (int i = 1; i <= n; i++) {
if (!vis[i]) bfs(i);
}