这部分为了快速入坑参考了浙大的课程,邓俊辉那个由于自己 C++ 基础太拉胯了就换了个简单的 www。后面还有一些细节的地方懒得搞了,反正写了这文章也图一乐 2333333,以后再润色了(

「re: 从零开始的带学数据结构㈡」图的一点基础理解

图的相关定义

概论

线性表和树本身是图的特殊情形。线性表和树表示的是一对一和一对多的关系,而图表示的是多对多的关系。包含:

  1. 一组顶点(Vertex)

  2. 一组边(Edge)

    其中:

    边是顶点对, (v, w) ∈ E,其中 v, w ∈ V;

    有向边 <v, w> 表示从 v 指向 w 的边(单行线);

    不考虑自回路

简单路径:不经过相同的顶点。

邻接:顶点和顶点

关联:顶点和边

ADT

image-20211216111102025

表示方法

邻接矩阵

G [N] [N],N 个顶点从 0 到 N-1 编号。

  • 注意:二维数组的顺序是 A [X] [Y]。分别代表 X 位置和 Y 位置。

其中 G [I] [J] = 1,若 <vi, vj>是 G 中的边;否则是 0.

直观。方便找邻接点、边、计算度(出度、入度)。但面对稀疏图的时候会浪费空间。

邻接表

G [N] 是指针数组,对应矩阵每行一个链表,只存非 0 元素。

节约稀疏图的空间、便于计算出度。但不便检查顶点间的边、也不便计算入度。

图的遍历

广度优先搜索(BFS)

一套矩阵图的 BFS 实现。本质上是个队列的操作,按顺序压进去也就相当于按照广度优先的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//广度优先搜索的简化重载函数。
void BFS(Vertex S)
{
//存放顶点(实际上是位置的 int 形式)
queue<Vertex> Q;
Vertex Temp;

//访问函数
cout << MGraph->Elements[S] << endl;
//改变 flag,标记已经访问。
MGraph->Visited[S] = true;
//压入 S。
Q.push(S);

while(!Q.empty())
{
Temp = Q.front();
Q.pop();

for(int i = 0; i < MGraph->Vertex_Amount; i++) //对于这个顶点的每个位置都要检查。
{
//1. 这个位置上邻接(权 < 无限) 2. 没有被遍历过
if(MGraph->WeightContainer[Temp][i] != INFINITY && !MGraph->Visited[i])
{
//访问函数
cout << MGraph->Elements[i] << endl;
//改变 flag,标记已经访问。
MGraph->Visited[i] = true;
//压入 S。
Q.push(i);
}
}
}
}

深度优先搜索(DFS)

伪代码实现:

1
2
3
4
5
6
7
8
void DFS(Vertex V)
{
V.visited = true;
for(iterator in adjvexs of V)
{
DFS(V.adjvex);
}
}

最短路径问题

在网络中,求两个不同顶点之间的所有路径中,边的权值之和最小的一条路径。这条路径(源点 - 终点)的路径就是最短路径。

分为:

  1. 单源:对从某固定源点出发,到其他顶点
  2. 多源:任意两顶点。

单源带权图最短路径问题——Dijkstra 算法(视频理解

前提

给定两个数组 DistancePrevious 来表示目标点到不同点的距离,以及达到这个路径需要走的前一个节点。

过程

  1. 初始化:先利用 flag 标记源点(下例表示为 V0),代表收入到最短路线集合里面;并且计算 V0 到邻接点的距离,存入 Distance 数组内,上一个元素显然是 V0,故存入 Previous 中。

  2. 循环开始:每次找未标记的节点里面距离出发点最近(Distance 最小)的节点。标记、收录到最短路线集合里面。

    计算它到邻接点的距离,如果端点 P 和邻接点 A 之间满足: \[ Dist[A] < Dist[P] + Edge<P, A> \] 那么就把邻接点的 Distance[A] 和 Previous[A] 更新。(松弛)

    每步取最优的操作是贪心算法思想的体现。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//Dijkstra 算法。有权图的情况。
//1. 寻找最小 DIST 值.
Vertex FindMinDistance(GNode* G, int distance[], bool collected[])
{
Vertex MinPosition;
int MinDist = INFINITY;

//迭代。i 表示从每一个端点开始统计。从未收录的端点里面选。
for(int i = 0; i < G->Vertex_Amount; i++)
{
if(collected[i] == false && distance[i] < MinDist)
{
//每次找到一个(第一次会从最小号开始找,一定有几个符合条件的)
MinDist = distance[i]; //更新最小距离
MinPosition = i; //更新顶点位置。
}
}

if(MinDist < INFINITY)
return MinPosition;
else
return -1; //找不到。返回 Error。
}

void Dijkstra(GNode* G, int distance[], int previous[], Vertex S)
{
bool collected[MaxVertexNum] = {false};
previous = {-1};
distance = {INFINITY};
//注意:默认邻接矩阵不存在的边权值是 INFINITY!从上向下才能正常使用。

distance[S] = 0;
collected[S] = true;

while(1)
{
//找最小的 dist。例如第一回找到 S。
Vertex V = FindMinDistance(G, distance, collected);
if(V==-1)
break;

//收录 V。
collected[V] = true;
//对于图中的每个顶点,找 V 未被收录的邻接点。
for(Vertex W = 0; W < G->Vertex_Amount; W++)
{ //1. 未收录 2. 权值存在(临接)
if(collected[W] == false && G->WeightContainer[V][W] < INFINITY)
{
if(distance[V] + G->WeightContainer[V][W] < distance[W])
{
distance[W] = distance[V] + G->WeightContainer[V][W];
previous[W] = V;
}
}
}
}
}

多源最短路算法——Floyd 算法

前提

调用 V 次 Dijkstra 之类的算法也可以暴力解决多源最短路算法。它的时间复杂度是 O(V^3 + V*E),但只在稀疏图可以实现 Floyd 算法的程度,达到 O(V^3) 的水平。

过程

代码实现

最小生成树

前提

树:无回路,边比顶点少一个。

生成:包含所有顶点,边都在图内。

权重和最小。

最小生成树存在,图一定联通。

算法分析

贪心算法可以在这个问题应用:

  1. 每一步取最优结果——代表权重最小的边
  2. 约束情况:只能用图中的边、正好用掉 V-1 条、不能有回路

方法 1:Prim 算法

Prim 算法的基本思路是:从一个根节点开始,慢慢地建立起一棵树。每次选权重最小的来生成。如果最后节点数量不够,就说明图不连通,生成失败。

伪代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Prim()
{
MST = {s}; //最小生成树
while(1)
{
V = 最小 dist;
if 不存在,break;

将 V 收录到 MST:dist[V] = 0

for(V 的每个邻接点 W)
if(dist[W]!=0)
{
if E(V,W) < dist[W]
dist[W] = E(V,W)
parent[W] = V;
}
}
if MST 顶点不够 V 个
error(生成树不存在)
}