文章目录
1、图的定义和基本术语
\qquad
图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E),
V
V
V顶点(数据元素)的有穷非空集合;
E
E
E边的有穷集合。
\qquad
无向图:每条边都是无方向的;有向图:每条边都是有方向的。
\qquad
完全图:图中任意两个顶点都有一条边相连。
\qquad
对于有n个顶点的无向完全图,要有
n
(
n
−
1
)
/
2
n(n-1)/2
n(n−1)/2条边;对于有n个顶点的有向完全图,要有
n
(
n
−
1
)
n(n-1)
n(n−1)条边。
\qquad
稀疏图:有很少边或者弧的图
e
<
n
l
o
g
n
e<nlogn
e<nlogn。
\qquad
稠密图:有较多边或者弧的图。
\qquad
网:边/弧带权重的图。
\qquad
邻接:有边/弧相连的两个顶点之间的关系,存在
(
v
i
,
v
j
)
(v_i, v_j)
(vi,vj),则称
v
i
v_i
vi和
v
j
v_j
vj互为邻接点;存在
<
v
i
,
v
j
>
<v_i,v_j>
<vi,vj>,则称
v
i
v_i
vi邻接到
v
j
v_j
vj,
v
j
v_j
vj邻接于
v
i
v_i
vi。
\qquad
关联(依附):边/弧与顶点之间的关系,存在
(
v
i
,
v
j
)
,
<
v
i
,
v
j
>
(v_i, v_j), <v_i,v_j>
(vi,vj),<vi,vj>,则称该边/弧关联于
v
i
v_i
vi和
v
j
v_j
vj。
\qquad
顶点的度:与该顶点相关联的边的数量,即为
T
D
(
v
)
TD(v)
TD(v),在有向图中,顶点的度等于该顶点的入度和出度之和。顶点的入度是以v为终点的有向边的条数,记作
I
D
(
v
)
ID(v)
ID(v);顶点的出度为以v为出发点的有向边的条数,记作
O
D
(
v
)
OD(v)
OD(v)。
\qquad
路径:接续的便构成的顶点序列;
\qquad
路径长度:路径上边或者弧的数目/权值之和;
\qquad
回路(环):第一个顶点和最后一个顶点相同的路径;
\qquad
简单路径:除路径的起点和终点可以相同外,其余顶点均不相同的路径;
\qquad
简单环:除路径的起点和终点相同外,其余顶点均不相同的路径;
\qquad
连通图(强连通图):在无(有)向图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)中,若对于任意两个顶点
v
,
u
v,u
v,u,都存在从
v
v
v到
u
u
u的路径,则称图
G
G
G是连通图(强连通图)。
\qquad
权和网:图中边或者弧具有相关数称为权。表明从一个顶点到另一个顶点的距离或者耗费。带权的图称为网。
\qquad
子图:设有两个图
G
=
(
V
,
E
)
,
G
1
=
(
V
1
,
E
1
)
G=(V,E), G_1=(V_1,E_1)
G=(V,E),G1=(V1,E1),若满足
V
1
∈
V
,
E
1
∈
E
V_1 \in V, E_1 \in E
V1∈V,E1∈E,则称
G
1
G_1
G1是
G
G
G的子图。
\qquad
连通分量(强连通分量):无向图
G
G
G的极大连通子图称为
G
G
G的连通分量,极大连通的意思是:该子图是
G
G
G的连通子图,将
G
G
G中任意一个不再该子图中的顶点加入之后,该子图不再连通。
\qquad
有向图
G
G
G的极大强连通子图称为
G
G
G的强连通分量。极大强连通子图的意思是:该子图是
G
G
G的强连通子图,将
D
D
D的任何不在该子图中的顶点加入,子图不再是强连通的。
\qquad
极小连通子图:该子图是
G
G
G的连通子图,在该子图中删除任意一条边,子图不再连通。
\qquad
生成树:包含无向图
G
G
G的所有顶点的极小连通子图;
\qquad
生成森林:对于非连通图,由各个连通分量的生成树的集合。
1.1 图的数据类型定义
\qquad
图的抽象类型定义如下所示:
\qquad
图的基本操作包括以下几种:
2、图的存储结构
\qquad 图是多对多的逻辑结构,图没有顺序存储结构,但可以借助二维数组来表示元素之间的关系,如邻接矩阵,邻接表,邻接多重表,十字链表等。
2.1 邻接矩阵
\qquad
首先建立一个顶点表(记录各个顶点的信息),和一个邻接矩阵(表示各个顶点之间的关系)。设图
A
=
(
V
,
E
)
A=(V,E)
A=(V,E)有n个顶点,则顶点表的表示如下所示:
\qquad
图的邻接矩阵是一个二维数组
A
.
a
r
c
s
[
n
]
[
n
]
A.arcs[n][n]
A.arcs[n][n],定义为:
\qquad
下图是一个无向图邻接矩阵的示意图:
\qquad
可以发现无向图的邻接矩阵是一个对称矩阵,同时对角线的元素均为0;顶点
i
i
i的度等于第
i
i
i行(列)中1的个数;完全图的邻接矩阵中,对角线元素为0,其余元素均为1。
\qquad
下图是一个有向图邻接矩阵的示意图:
\qquad
在有向图邻接矩阵中,第
i
i
i行的含义是:以结点
v
i
v_i
vi为尾的弧,即出度边;第
i
i
i列的含义是:以结点
v
i
v_i
vi为头的弧,即入度边。有向图的邻接矩阵可能是不对称的;顶点的出度=第
i
i
i行的元素之和;顶点的入度=第
i
i
i列的元素之和;有向图顶点的度=第
i
i
i行的元素之和+第
i
i
i列的元素之和。
\qquad
网-即有权图的邻接矩阵表示法,定义如下所示:
\qquad
下图是一个网的邻接矩阵的示意图:
\qquad
邻接矩阵的存储表示:用两个数组分别存储顶点表
和邻接矩阵
。
#define MaxInt 50000 //定义一个极大的数
#define MVNum 100 //定义最大的顶点数
typedef char VerTexType; //设顶点的数据类型为字符型
typedef int ArcType; //设边的权值类型为整数型
typedef struct
{
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexNum, arcNum; //图当前的节点数量和边数量
}AMGraph;
\qquad 采用邻接矩阵构建无向网:
//创建图
void CreatUDN(AMGraph &G)
{
cin >> G.vexNum >> G.arcNum;
for(int i = 0; i < G.verNum; ++i)
cin>> G.vex[i]; //依次输入点的信息
for(int i = 0; i < G.vexNum; ++i) //初始化邻接矩阵
for(int j = 0; j < G.vexNum; ++j)
G.arcs[i][j] = MaxInt;
//给边赋值
for(int k = 0; k < G.arcNum; ++k)
{
int v1 = 0, v2 = 0;
double w = 0;
cin >> v1 >> v2 >> w; //输入当前边的顶点和边的权重
int i = LocateVex(G, v1); //确定出点v1和v2在图中的位置
int j = LocateVex(G, v2);
G.arcs[j][i] = G.arcs[i][j] = w;//无向图是对称的
}
}
//在图中查找顶点
int LocateVex(AMGraph &G, VertexType u)
{
for(int i = 0; i < G.verNum; ++i)
if(u==G.vers[i]) return i;
return -1;
}
2.2 邻接矩阵的优缺点
\qquad
优点: 直观,简单,好理解;方便检查任意一对顶点间是否存在边;方便找任意一个顶点的所有“邻接点”,有边直接相连的顶点;方便计算任意一个顶点的“度”,从该点发出的边的数量为“出度”;指向该点的边的数量为“入度”。
\qquad
缺点: 不便于增加和删除顶点;浪费空间,
O
(
n
2
)
,
n
O(n^2),n
O(n2),n是顶点的数量,如果存稀疏图(点很多,边很少),则会有大量无效元素;浪费时间,统计稀疏图中有多少条边。
2.3 邻接表
\qquad
邻接表表示法使用链式存储的结构,顶点按照编号顺序将顶点数据存储在一维数组之中;关联同一顶点的边,用线性链表进行存储。邻接表的示意图如下所示:
\qquad
邻接表是不唯一的,因为链表中的边的顺序可以互换;若无向图中有n个顶点,e条边,则其邻接表需要n个头结点和2e个表结点。适合存储稀疏图。所以邻接表的空间复杂度为:
O
(
n
+
2
e
)
O(n+2e)
O(n+2e);无向图中顶点
v
i
v_i
vi的度为第
i
i
i个单链表中的结点个数。
\qquad
有向图的邻接表如下图所示:
\qquad
有向图邻接表的空间复杂度为
O
(
n
+
e
)
O(n+e)
O(n+e);容易计算顶点
v
i
v_i
vi的出度即为第
i
i
i个单链表中结点的个数;顶点
v
i
v_i
vi的入度即为整个单链表中邻接点域值为
i
−
1
i-1
i−1的结点的个数,计算比较复杂。
\qquad
上图是以顶点的出度建立的出度边邻接表,还可以使用顶点的入度边建立入度边邻接表,如下图所示:
\qquad
容易计算顶点
v
i
v_i
vi的入度即为第
i
i
i个单链表中结点的个数;顶点
v
i
v_i
vi的出即为整个单链表中邻接点域值为
i
−
1
i-1
i−1的结点的个数,计算比较复杂。
\qquad
当已知一个邻接矩阵或者邻接表,可以唯一确定一个无向图/有向图。
\qquad
邻接表的存储表示:
//顶点的结构
#define MVnum 100 //最大顶点数量
typedef struct VNode
{
VerTexType data; //顶点信息
ArcNode *firstArc; //指向第一条依附于该顶点的边的指针
}AdjList[MVnum]; //AdjList表示邻接表类型
//边的节点结构
typedef struct ArcNode
{
int adjVex; //该边指向的顶点的位置
ArcNode *nextArc; //指向下一条边的指针
double info; //边的权值
};
//图的结构定义
typedef struct ALGraph
{
AdjList vertices; //邻接表中的顶点表
int vexNum, arcNum;//图中的顶点数和边数
};
//邻接表的构建
void CreateUDG(ALGraph &G)
{
cin >> G.vexNum >> G.arcNum; //输入总的顶点数和总的边数
for(int i = 0; i < G.vexNum; ++i)
{//构建顶点表
cin >> G.vertices[i].data;
G.vertices[i].firstArc = nullptr; //初始化头结点的指针域为空
}
for(k= 0; k <G.arcNum; ++k)
{//构建边链表
cin >> v1 >> v2; //输入一条边依附的两个顶点
int i = LocateVex(G,v1);
int j = LocateVex(G,v2);
ArcNode p1 = new ArcNode;//生成一个新的边结点
p1->adjVex = j; //邻接点的序号为j
p1->nextArc = G.vertices[i].firstArc;//头插法
G.vertices[i].firstArc = p1; //将新结点插入到顶点vi的头部
//构建无向网
ArcNode p2 = new ArcNode;//生成一个新的边结点
p2->adjVex = i;
p2->nextArc = G.vertices[j].firstArc;//头插法
G.vertices[j].firstArc = p2;
}
}
2.4 邻接表的优缺点
\qquad
方便找到任一顶点的所有“邻接点”;
\qquad
节约稀疏图的空间,需要N个头指针+2E个结点;
\qquad
不方便计算某个顶点的度:对于无向图可以直接通过和头结点相连的边结点的个数获得结点的度;
\qquad
但是对于有向图,只能计算节点的出度,需要构造逆邻接表来方便计算结点的入度;
\qquad
不方便检查任意一对顶点之间是否存在边;
\qquad
邻接矩阵常用于稠密图,邻接表常用于稀疏图
2.5 十字链表和邻接多重表
\qquad
使用十字链表来存储有向图可以克服有向图求结点的度困难的问题;使用邻接多重表存储无向图可以解决无向图每条边都要存储两边的问题。
\qquad
十字链表可以看做是有向图的邻接表和逆邻接表结合起来形成的一种链表。在原有向图邻接表的基础之上,给头结点增加一个指针域指向节点的第一条出度边;给边结点的增加一个指针域指向弧头相同的节点,之前边结点的指针域指向的是弧尾相同的节点;增加一个节点值域,记录边的尾节点的值。
\qquad
邻接多重表中每一条边只申请一个边结点,增加几个指针域将边结点和顶点之间的关系表示出来即可。
3、图的遍历
\qquad
遍历的定义: 从已给的连通图中某个顶点出发,沿着一些边访问遍图中所有的顶点,且使得每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算。图遍历的实质是:找每个顶点的邻接点的过程。
\qquad
图的特点: 图中可能存在回路,且图的任一顶点都可能与其他顶点相连通,在访问完某个顶点之后,可能会沿着某些边右回到曾经访问过的顶点。可以设置一个辅助数组visited[n],记录已经被访问过的顶点,初始状态均置为0,若顶点i被访问过了,则visited[i]置为1,从而放置顶点的多次访问。
3.1 深度优先搜索-DFS
\qquad
在访问图中的某个起始顶点v之后,由v出发,访问它的任意一个邻接顶点
w
1
w_1
w1;
\qquad
再从
w
1
w_1
w1出发,访问与
w
1
w_1
w1邻接但是还没有被访问过的顶点
w
2
w_2
w2;
\qquad
然后再从
w
2
w_2
w2出发,进行类似的访问… ;
\qquad
如此进行下去,直到到达所有的邻接顶点都被访问过的u为止;
\qquad
接着退回一步,退到前一次刚访问过的顶点,看是否还有其他没有被访问的邻接顶点;
\qquad
如果有,则访问此顶点,之后再从此顶点出发,进行与之前类似的访问;
\qquad
如果没有,则再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
\qquad
连通图的深度优先遍历类似于树的先根遍历。
\qquad
采用邻接矩阵表示图的深度优先搜索遍历的实现如下所示:
void DFS(AMGraph &G, int v)
{
cout << v << endl;
visited[v] = true; //visited为辅助记录节点是否访问的向量
for(int i = 0; i < G.vexNum; ++i)
{
if(G.arcs[v][i]!=0 && !visited[i])
DFS(G, i);
}
}
\qquad
用邻接矩阵来表示图,遍历图中每个点点都要从头扫描该顶点所在的行,时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)
\qquad
用邻接表来表示图,虽然有2e个表结点,但只需要扫描e个结点即可以完成遍历,加上访问n个头结点的时间,时间复杂度为
O
(
n
+
e
)
O(n+e)
O(n+e)
\qquad
所以稠密图适合在邻接矩阵上进行深度遍历;稀疏图适合在邻接表上进行深度遍历。
\qquad
非连通图的遍历:首先从图中任意选择一个节点进行DFS,当DFS结束之后,若visited还有没有访问的节点,则再从图中任意选择一个节点进行DFS;重复上述过程,直到visited中所有的节点均被访问过。
3.2 广度优先搜索-BFS
\qquad
从图的某个结点出发,首先依次访问该结点的所有邻接顶点
v
1
,
v
2
,
.
.
.
,
v
m
v_1,v_2,...,v_m
v1,v2,...,vm;
\qquad
再按照这些顶点被访问的先后顺序依次访问与它们相邻接的所有未被访问的顶点;
\qquad
重复上述过程,直到所有顶点均被访问了
\qquad
借助队列实现图的广度优先遍历:
void BFS(Graph &G, int v)
{
cout << v;
visited[v]=true;
queue<int> Q;
Q.push(v);
while(!Q.empty())
{
int top = Q.top();
Q.pop();
for(int w = FistAdjVex(G,top); w>=0; w=NextAdjVex(G,top,w))
{
if(!visited[w])
{
cout << w<<endl;
visited[w] = true;
Q.push(w);
}
}
}
}
\qquad
如果使用邻接矩阵,则对于BFS每一个被访问到的顶点,都要循环检测矩阵中的一行n个元素,总的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。和深度优先遍历相同,使用邻接表的时间复杂度为
O
(
n
+
e
)
O(n+e)
O(n+e)
\qquad
DFS和BFS的空间复杂度相同,都是
O
(
n
)
O(n)
O(n),DFS借助了栈(递归借助系统中的栈),BFS借助了队列。
4、图的应用
4.1 生成树
\qquad
生成树: 所有顶点均由边连接在一起,但不存在回路的图。
\qquad
一个图中可以有许多棵不同的生成树;
\qquad
所有生成树具有以下共同特点:
\qquad\qquad
· 生成树的顶点个数与图的顶点个数相同;
\qquad\qquad
· 生成树是图的极小连通子图,去掉一条边则非连通;
\qquad\qquad
· 一个有n个顶点的连通图的生成树有n-1条边;
\qquad\qquad
· 在生成树中再加一条边必然形成回路。
\qquad\qquad
· 生成树中任意两个顶点之间的路径唯一
\qquad
含有n个顶点n-1条边的图不一定是生成树
\qquad
设图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)是个连通图,当从图中任意一个顶点出发遍历图G时,将边集合
E
(
G
)
E(G)
E(G)分成两个集合
T
(
G
)
T(G)
T(G)和
B
(
G
)
B(G)
B(G)。其中,
T
(
G
)
T(G)
T(G)是遍历图时经过的边的集合,
B
(
G
)
B(G)
B(G)是遍历图时未经过的边的集合。显然,
G
1
(
V
,
T
)
G1(V,T)
G1(V,T)是图G的极小连通子图,即子图
G
1
G1
G1是图G的生成树。
\qquad
最小生成树:给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树。
\qquad
MST性质:设
N
=
(
V
,
E
)
N=(V,E)
N=(V,E)是一个连通网,
U
U
U是顶点集
V
V
V的一个非空子集。若边
(
U
,
V
)
(U,V)
(U,V)是一条具有最小权值的边,其中
u
∈
U
,
v
∈
V
−
U
u\in U, v \in V-U
u∈U,v∈V−U,则必然存在一棵包含边
(
u
,
v
)
(u,v)
(u,v)的最小生成树。
\qquad
Prim算法: 算法思想:设
N
=
(
V
,
E
)
N=(V,E)
N=(V,E)是连通网,
T
E
TE
TE是
N
N
N上最小生成树中的边的集合;初始令
U
=
u
0
,
T
E
=
{
}
U={u_0},TE=\{\}
U=u0,TE={};在所有
u
∈
U
,
v
∈
V
−
U
u \in U,v \in V-U
u∈U,v∈V−U的边
(
u
,
v
)
∈
E
(u,v) \in E
(u,v)∈E中,找一条代价最小的边
(
u
0
,
v
0
)
(u_0,v_0)
(u0,v0);将
(
u
0
,
v
0
)
(u_0,v_0)
(u0,v0)并入边集合
T
E
TE
TE,同时将
v
0
v_0
v0并入
U
U
U;重复上述操作,直至
U
=
V
U=V
U=V为止,则
T
=
(
V
,
T
E
)
T=(V,TE)
T=(V,TE)为
N
N
N的最小生成树。
\qquad
Kruskal算法: 算法思想:设连通网
N
=
(
V
,
E
)
N=(V,E)
N=(V,E),令最小生成树初始状态是只有n个顶点但是没有边的非连通图
T
=
(
V
,
)
T=(V,{})
T=(V,),每个顶点自成一个连通分量。在E中选取代价最小的边,若改变依附的顶点落在
T
T
T中不同的连通分量上(即不能形成环),则将此边加入到
T
T
T中;否则去掉此边,选取下一条代价最小的边;依次类推,直至T中所有顶点都在同一个连通分量上为止。
\qquad
Prim算法 的时间复杂度为:
O
(
n
2
)
O(n^2)
O(n2),其中n为顶点数量,所以适用于稠密图;Kruskal算法的时间复杂度为:
O
(
e
l
o
g
e
)
O(eloge)
O(eloge),其中e为边的数量,所以适用于稀疏图。
4.2 最短路径
\qquad
交通网络使用有向网来表示:顶点表示地点;弧表示两个地点之间有路连通;弧上的权值表示两个地点之间的距离,交通费或者图中所花费的时间等。如何使得从一个地点到另外一个地点的运输时间最短或者运费最省,这就是一个求两个地点之间的最短路径问题。
\qquad
问题抽象:在有向网中从源点到终点的多条路径中,寻找一条各边的权值之和最小的路径,即最短路径,最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n-1条边。
4.2.1 单源最短路径问题-Dijkstra算法
\qquad
求图中某个顶点到另外其余顶点的最短路径,叫做单源最短路径问题,常用的算法时Dijkstra算法。Dijkstra算法的流程如下所示:
\qquad
1)初始化:先找出从源点
v
0
v_0
v0到各终点
v
k
v_k
vk的直达路径
(
v
0
,
v
k
)
(v_0,v_k)
(v0,vk),即通过一条弧到达的路径;
\qquad
2)选择:从这些路径中找出一条长度最短的路径
(
v
0
,
v
u
)
(v_0,v_u)
(v0,vu);
\qquad
3)更新:然后对其余各条路径进行适当地调整:若在图中存在弧
(
u
,
v
k
)
(u,v_k)
(u,vk),且
(
v
0
,
u
)
+
(
u
,
v
k
)
<
(
v
0
,
v
k
)
(v_0,u)+(u,v_k)<(v_0,v_k)
(v0,u)+(u,vk)<(v0,vk),则以路径
(
v
0
,
u
,
v
k
)
(v_0,u,v_k)
(v0,u,vk)代替
(
v
0
,
v
k
)
(v_0,v_k)
(v0,vk);
\qquad
4)在调整之后的各条路径中,再找长度最短的路径,以此类推。
\qquad
Dijkstra算法的示意图如下图所示:
4.2.2 多源最短路径问题-Floyd算法
\qquad
上述介绍的Dijkstra算法可以计算某个点到其他点的最短路,但是如果想要计算每个点到其他的最短路需要将Dijkstra算法循环顶点个数次。
\qquad
另外一种求多源最短路径问题的算法时Floyd算法,Floyd算法的思想如下所示:首先进行逐个顶点的额试探;之后从
v
i
v_i
vi到
v
j
v_j
vj的所有可能存在的路径中,选出一条长度最短的路径。Floyd算法的思路演示如下图所示:
4.3 拓扑排序
\qquad
拓扑排序和关键路径这两种应用针对的是一种特殊的图-有向无环图,简称DAG图(Directed Acycline Graph)。DAG的示意图如下图所示:
\qquad
AOV网,用一个有向图表示一个工程的各个子工程机器相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网-Activity On Vertex network。应用AOV网解决拓扑排序问题。
\qquad
AOE网,用一个有向图表示一个工程的各个子工程机器相互制约的关系,其中以弧表示活动,顶点表示活动的开始或者结束事件,称这种有向图为边表示活动的网,简称AOE网-Activity On Edge network。应用AOE网解决关键路径问题。
\qquad
AOV网的特点:若从i到j有一条有向路径,则i是j的前驱,j是i的后继;若<i,j>是网中的有向边,则i是j的直接前驱,j是i的直接后继。AOV网中不允许有回路存在,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。
\qquad
在AOV网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧<i,j>存在,则在这个序列中,i一定排在j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序。
\qquad
拓扑排序的方法:在有向图中选一个没有前驱的顶点并且进行输出;从图中删除该顶点和所有以它为尾的弧;重复上述两步,直至全部顶点均已输出或者当图中不存在无前驱的顶点为止。拓扑排序的示意如下图所示:
\qquad
检测AOV网中是否存在环的方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。
4.4 关键路径
\qquad
把工程计划表示为边,以此来表示活动的网络,即AOV网。用顶点表示时间,弧表示活动,弧的权值表示活动的持续时间。事件表示在它之前的活动已经完成,在它之后的活动可以开始。
\qquad
关键路径的求解方法可以参考运筹学课程。