图论
见名知意,图论就是研究图的数学理论和方法。图是一种抽象的数据结构,由节点和连接这些节点的边组成。图论在计算机科学、网络分析、物流、社会网络分析等领域有广泛的应用。
如下,这就是一个图,可以看到这个图有555个顶点,分别编号为 {0,1,2,3,4}\{ 0 , 1 , 2 , 3 , 4 \}{0,1,2,3,4} 。同时这个图有555条边,例如,在顶点222和顶点444之间存在着一条边。
图的基本概念
在详细讲解图论和有关图论算法之前,先来了解一下在图论中的一些基本表述和规范。
- 图:图是一种由一组顶点和一组边组成的数据结构,记做G=(V,E)G = ( V , E )G=(V,E),其中VVV代表顶点集合EEE 代表边集合。
- 顶点 :顶点是图的基本单位,也称为节点。
- 边 :一条边是连接两个顶点的线段或弧。可以是无向的,也可以是有向的。一条边可以记做为(u,v)(u,v)(u,v)。在无向图中,若存在一条(u,v)(u,v)(u,v),表示可以从uuu点直接走到vvv 点,反之同理。但若在有向图中,存在一条边(u,v)(u,v)(u,v),表示可以从uuu节点直接走向vvv节点。
- 无向图:图中的边没有方向,即(u,v)(u,v)(u,v)和(v,u)(v,u)(v,u)是同一条边。
- 有向图:图中的边有方向,即(u,v)(u,v)(u,v)和(v,u)(v,u)(v,u) 不是同一条边。
- 简单图:表示不含有重边(两个顶点之间的多条边)和自环(顶点到自身的边)的图。
- 多重图:允许有重边和自环的图。
- 边权 :一般表示经过这一条边的代价(代价一般是由命题人定义的)。
如下图,就是一个有向的简单图(通常来说,在有向图中边的方向用箭头来表示):
如下图,就是一个无向的多重图,其中存在两条边可以从顶点555到顶点222:
与此同时,为了方便起见,对于无向图的处理,我们只需要在两个顶点之间建立两个方向相反的无向边就可以表示一个无向图,具体如下:
图的表示方法
在计算机中,图可以通过许多方式来构建和表示。总的可以分成图的邻接矩阵和邻接表两种方法(关于链式前向星本文不过多展开叙述,有兴趣的可以自行查阅相关文档)。
图的邻接矩阵
若一个图中有NNN个顶点,那么我们就可以用一个N×NN\times NN×N的矩阵来表示这个图。我们一般定义,若矩阵的元素Ai,j≠0A_{i, j} ≠ 0Ai,j=0 表示从节点iii到 jjj有一条有向边,其中边的权值为Ai,jA_{i,j}Ai,j 。
假设存在一个有333个顶点的图,并且有三条有向边E={(1,2),(2,3),(3,2)}E = \{ ( 1 , 2 ) , ( 2 , 3 ) , ( 3 , 2 )\}E={(1,2),(2,3),(3,2)}那么就可以用邻接矩阵表示
画成可视化的图就长这个样子:
在 C++ 中,我们可以简单地用一个二维数组来表示:
// 定义一个矩阵。
int map[50][50];
// 将所有的边初始化为0。
for(int i=1;i<=50;i++) for(int j=1;j<=50;j++) map[i][j]=0;
// 建边,若有边权,则值为边权,若无边权则用1表示直接两个点右边相连。
map[1][2]=map[2][3]=map[3][2]=1;
图的邻接表
邻接表本质上就是用链表表示图。数组的每个元素表示一个顶点,元素的值是一个链表,链表中存储该顶点的所有邻接顶点。假设存在一个有444个顶点的图,并且有四条有向边E={(1,2),(2,3),(3,2),(3,4)}E = \{ ( 1 , 2 ) , ( 2 , 3 ) , ( 3 , 2 ) , ( 3 , 4 ) \}E={(1,2),(2,3),(3,2),(3,4)},那么就可以用邻接表表示为:
画成可视化的图就长这个样子:
在 C++ 中,我们可以使用 STL模板库 中的 vector 来实现:
vector<int>G[50]; // 建图。
G[1].push_back(2);
G[2].push_back(3);
G[3].push_back(2);
G[3].push_back(4);
一般情况下,推荐使用邻接表的方式来存图,因为使用邻接矩阵比较浪费空间。在顶点数量非常多但边非常少的图中,N2N^2N2的时空复杂度会导致 MLE 或 TLE 等问题。
图的各种性质
- 度数:一个顶点的度是连接该顶点的边的数量。在有向图中,有入度和出度之分(具体例子见后文)。
- 路径:从一个顶点到另一个顶点的顶点序列,路径上的边没有重复。
- 回路:起点和终点相同的路径。
- 连通图:任意两个顶点之间都有路径相连的无向图。
- 强连通图:任意两个顶点之间都有路径相连的有向图。
对于下面这个无向图,顶点111的度数为111,顶点222的度数为222;顶点333的度数为111;顶点444 的度数为000。同时,由于444号顶点没有度数,所以该顶点没有办法到达任何一个其他的顶点,所以这个图是一个不连通图:
如下图,就是一个有向不强连通图。其中,顶点111的入度为000,出度为222;顶点222 的入度为111 ,出度也为111;顶点333的入度为222 ,但出度为000。由于顶点111和顶点222 可以走到顶点333,但顶点333 没有办法走到顶点111 或顶点222,因此下面的图不是一个强连通图:
对于下图来说,1→2→3→41\to 2\to 3\to 41→2→3→4是一条从顶点111到顶点444的路径。2→3→4→2→32\to 3\to 4 \to 2\to 32→3→4→2→3就不是一个路径,因为相同的边(2,3)(2,3)(2,3)被多次走到了。1→2→3→11\to 2\to 3\to 11→2→3→1就是一个回路,因为这个路径的起点和终点相同:
图的遍历
图通常采用 深度优先搜索(DFS)/ 广度优先搜索(BFS) 这两个算法来遍历。其中 深度优先算法(DFS) 是最常见的遍历算法。
对于一个用邻接矩阵保存的图,其深度优先搜索遍历的 C++ 代码如下:
int vis[105],map[105][105];
void dfs(int node)
{
vis[node]=1;
cout<<node<<endl;
for(int i=1;i<=n;i++) if(map[node][i]!=0&&!vis[i]) dfs(i);
return;
}
函数调用:
dfs(1);
表示从111号顶点开始遍历。
对于一个用邻接表保存的图,其深度优先搜索遍历的 C++ 代码如下:
vector<int> G[105];
int vis[105];
void dfs(int node)
{
vis[node]=1;
cout<<node<<endl;
for(int to:G[node]) if(!vis[to]) dfs(to);
return;
}
函数调用:
dfs(1);
表示从111号顶点开始遍历。
实现过程
如图:从AAA开始
- AAA进入堆栈
- AAA出堆栈时,AAA的邻接结点BBB、CCC、DDD进入堆栈
- DDD出堆栈时,DDD的邻接结点AAA、CCC、GGG中未进过堆栈的GGG进入堆栈
- GGG出堆栈时,GGG的邻接结点CCC、DDD已经全部进入过堆栈
- CCC出堆栈时,CCC的邻接结点AAA、DDD、FFF、GGG中未进过堆栈的FFF进入堆栈
- FFF出堆栈,邻接结点均已进入过堆栈
- BBB出堆栈,邻接结点EEE进入堆栈
- EEE出堆栈
结果 : ADGCFBEA D G C F B EADGCFBE
广度优先搜索的方式也类似:
#include <queue>
vector<int> G[105];
int vis[105];
void bfs(int node){
queue<int> que;
que.push(node);
while(!que.empty()){
int t = que.front();
cout << t << endl;
que.pop();
for (int to : G[node]){
if (!vis[to]) {
vis[to] = 1;
que.push(to);
}
}
}
return ;
}
函数调用:
bfs(1);
表示从111号顶点开始遍历。
实现过程
如图:从AAA开始
- AAA进入队列
- AAA出队列时,AAA的邻接结点BBB、CCC、DDD进入队列
- BBB出队列时,BBB的邻接结点AAA、EEE、FFF中未进过队列的EEE、FFF进入队列
- CCC出队列时,CCC的邻接结点AAA、DDD、FFF、GGG、中未进过队列的GGG进入队列
- DDD出队列时,DDD的邻接结点AAA、CCC、GGG已经全部进入过队列
- EEE出队列,邻接结点均已进入过队列
- FFF出队列,邻接结点均已进入过队列
- GGG出队列,邻接结点均已进入过队列
结果 :$ A B C D E F G$
对于判断无向图的连通性,我们只需要从任意一个点开始跑一遍深搜或者广搜就行了。如果所有顶点的 vis
都被标记了,则证明图是联通的,否则图就是不连通的。
番外 - 图的常见算法
- 深度优先搜索 (DFS):适用于遍历图和检测图中的回路。
- 广度优先搜索 (BFS):适用于寻找最短路径(无权图)。
- Dijkstra算法:适用于加权图中寻找单源最短路径。
- Spfa算法:适用于有负权边的图中寻找单源最短路径。
- Floyd算法:适用于寻找所有顶点对之间的最短路径。
- Kruskal算法:用于求解最小生成树 (MST - Minimum Spanning Tree)。
- Prim算法:另一种求解最小生成树的方法。
- 拓扑排序:适用于有向无环图 (DAG),用于任务调度等应用。
- Tarjan算法:用于求解图中的强连通分量、割点、桥。