图G有定点集V和边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合。用 ∣ V ∣ |V| ∣V∣表示图G中顶点个数,用 ∣ E ∣ |E| ∣E∣表示图G中边的条数。图不能是空图,最少要有一个顶点。
对于无向图,|E|的取值范围为0到
n
(
n
−
1
)
/
2
n(n-1)/2
n(n−1)/2,有
n
(
n
−
1
)
/
2
n(n-1)/2
n(n−1)/2条边的无向图称为完全图。
对于有向图,|E|的取值范围为0到
n
(
n
−
1
)
n(n-1)
n(n−1),有
n
(
n
−
1
)
n(n-1)
n(n−1)条弧的有向图称为完全图。
若图G和图G’顶点相同,E(G’)是E(G)的子集,则成G’是G的生成子图。
无向图中,任意两个顶点都是连通的。称为连通图。无向图中的极大连通子图称为连通分量。假设一个图有n个顶点,如果边数小于n-1,其必定是非连通图。
有向图中,如果有一对顶点v、w,从v到w和从w到v之间都有路径,称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,称此图为前连通图。有向图的极大强连通子图称为强连通分量。假设一个图有n个顶点,至少需要n-1条边,构成强连通分量,此时为一个环路。
连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为n,则它的生成树有n-1条边。
无向图的全部顶点度之和等于边数的两倍,;
有向图全部顶点的入度之和与出度之和相等,并且等于边数。有向图顶点的度为入度和出度之和。
一个顶点的入度为0、其余顶点入度均为1的有向图,称为有向树。
用一个一维数组储存图中顶点的信息,有一个二维数组(邻接矩阵)储存图中边的信息。
无向图的邻接矩阵是对称矩阵,实际储存时只需储存三角矩阵元素。
邻接矩阵的空间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
可以很方便地确定两个顶点之间是否有边相连,但要确定图中有多少条边,必须遍历每个元素。
适合表示稠密图。
邻接矩阵储存结构定义如下
typedef struct{
vertexType vex[MaxVertexNum]; // 顶点表
edgeType edge[MaxVertexNum][MaxVertexNum]; // 邻接矩阵(边表)
int vexNum, edgeNum; // 当前顶点数和边数
}MGraph;
对图中每个顶点建立一个单链表(边表、出边表),每个单链表中的结点表示依附与该顶点的边(对于有向图,表示以该顶点为尾的弧)。边表的头指针和顶点的数据采用顺序存储(顶点表)。
对于无向图,所需储存空间为
O
(
2
∣
E
∣
+
∣
V
∣
)
O(2|E|+|V|)
O(2∣E∣+∣V∣);
对于有向图,所需存储空间为
O
(
∣
E
∣
+
∣
V
∣
)
O(|E|+|V|)
O(∣E∣+∣V∣)。
可以很方便找到一个顶点的所有临边。
在有向图中,求顶点的出度只需计算邻接表的结点个数,但求入度需要遍历所有邻接表。
适合表示稀疏图,能极大节省存储空间。
邻接表储存结构定义如下
typedef struct{ // 顶点结点
vertexType data;
ArcNode *first; // 指向第一个依附该顶点的弧
}VerNode, VerList[MaxVertexNum];
struct ArcNode{ // 边结点
int vertex; // 该边指向的顶点序号
edgeType data;
struct ArcNode *next; // 指向下一个边
}
typedef struct{
VerList VerList; // 邻接表
int vexNum, edgeNum;
}AlGraph;
是有向图的一种链式储存结构。每条弧和每个顶点都有一个结点表示。顶点结点顺序存储。
弧结点结构 | tailvex | headves | hlink | tlink | info |
---|---|---|---|---|---|
作用 | 尾域,标识弧尾顶点 | 头域,标识弧头顶点 | 指向弧头相同的下一条弧 | 指向弧尾相同的下一条弧 | 携带弧的相关信息 |
顶点结点结构 | data | firstin | firstout |
---|---|---|---|
作用 | 存放顶底数据信息 | 指向以该顶点为弧头的第一个弧结点 | 指向以该顶点为弧尾的第一个弧结点 |
是无向图的一种链式存储结构。每条边和每个顶点也各用一个结点表示。
顶点结点结构 | mark | ivex | ilink | jvex | jlink | info |
---|---|---|---|---|---|---|
作用 | 标志域,记录该边是否被搜索过 | 标识边依附的第一个顶点 | 指向下一条依附ivex的边 | 标识边依附的另一个顶点 | 指向下一条依附jvex的边 | 携带相关信息 |
在遍历图的过程中,必须记下每个已访问过的顶点。
类似树的层序遍历,广度优先搜索是一个分层的查找过程,没有回退的过程,必须借助一个辅助队列,记忆正在访问的顶点的下一层顶点。
int visited[MAX_VERTEX_NUM];
void BFSTraverse(Graph G){
for(int i=0; i<G.vexNum; i++)
visited[i] = 0;
for(int i=0; i<G.vexNum; i++)
if( !visit[i] )
BFS(G, i);
}
void BFS(Graph G, int v){
initQueue(Q);
visit(v);
visited[v] = 1;
enQueue(Q, v);
while( !isEmpty(Q) ){
deQueue(Q, v);
for(w=firstNeighbor(G, v); w>=0; w=nextNeighbor(G, v, w)){
if(!visited[w]){
visit(w);
visited[w] = 1;
enQueue(Q, w);
}
}
}
}
无论采用邻接表还是邻接矩阵,BFS都需要借助一个辅助队列,所有顶点均需入队一次。最坏情况下,空间复杂度为
O
(
∣
V
∣
)
O(|V|)
O(∣V∣)。
采用邻接表存储时,每个顶点均需被搜索依次,时间复杂度为
O
(
∣
V
∣
)
O(|V|)
O(∣V∣);在搜索任一顶点的边时,每一条边至少访问依次,总时间复杂度为
O
(
∣
E
∣
)
O(|E|)
O(∣E∣)。算法总的时间复杂度为
O
(
∣
V
∣
+
∣
E
∣
)
O(|V|+|E|)
O(∣V∣+∣E∣)
采用邻接矩阵存储时,查找每个顶点的所有邻接点的时间为
O
(
V
)
O(V)
O(V),算法总的时间复杂度为
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)。
借助BFS,可以求解非带权图的单源最短路径问题。
在广度遍历中,可以得到一棵遍历树,称为广度优先生成树。邻接矩阵产生的生成树是唯一的,而邻接表产生的生成树不唯一。
类似树的先序遍历,先尽可能“深”的搜索一个图,当不能继续向下访问时,依次退回最近被访问的顶点,若其还有未被访问的邻接顶点,则从这个邻接顶点继续该搜索过程。
DFS是一个递归的过程,需要借助一个递归工作栈,空间复杂度为
O
(
∣
V
∣
)
O(|V|)
O(∣V∣)。
采用邻接表存储时,访问所有顶点的时间复杂度为
O
(
∣
V
∣
)
O(|V|)
O(∣V∣),查找所有顶点的邻接点的总时间复杂度为
O
(
∣
E
∣
)
O(|E|)
O(∣E∣),故算法总的时间复杂度为
O
(
∣
V
∣
+
∣
E
∣
)
O(|V|+|E|)
O(∣V∣+∣E∣)。
采用邻接表存储是,查找一个顶点的所有邻接点所需时间为O(|V|),故总的时间复杂度为
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)。
借助DFS,可以求解有向无环图的拓扑排序问题。
在深度遍历中,可以得到一棵遍历树,称为深度优先生成树。邻接矩阵产生的生成树是唯一的,而邻接表产生的生成树不唯一。
对于无向图,若图是连通的,则从任一顶点出发,仅需一次遍历就可以访问图中的所有顶点。
对于有向图,若从初始顶点到图中每个顶点都有路径,则能访问到图中的所有顶点。一个有向图的连通子图分为强连通分量和非强连通分量,非强连通分量调用一次搜索过程无法访问到所有顶点。
最小生成树具有如下特征:
初始时从图中任取一顶点加入树T,此时树中只含有一个顶点。之后选择一个与T中顶点距离最近的顶点,将顶点和相应的边加入T中。每次操作,T中的顶点数和边数都增加1,直到所有顶点都加入T。
prim算法基于贪心策略,其简单实现如下
def prim(G):
T = set() # 初始化空边集
U = {w} # 初始化顶点集,添加任一顶点w
# 若树中未包含全部顶点,选择一个加入
while not (V - U):
# (u,v)是u∈U,v∈(V-U)且权值最小的边
# 此处最坏情况需要遍历所有n个顶点,时间复杂度为O(n)
find a edge (u, v)
U.add(v) # 将选定的点加入顶点集
T.add((u, v)) # 将选定的边加入边集
算法的时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2),适合求解稠密图的最小生成树。
初始时只有n个顶点而无边的非连通图T={ V,{ } },每个顶点自成一个连通分量。然后按边权值从小到大的顺序,查看当前为被选取且权值最小的边,若该边依附的两个顶点落在T中不同的连通分量中,则将此边加入T。
kruskal算法基于贪心策略,其简单实现如下
def kruskal(G):
T = V # 初始化树,仅含所有顶点
numS = n # 连通分量数
while numS > 1:
# 从边集中找到权值最小的边
pop a edge with shortest length (u, v)
if (u ,v) belong to different connected components:
T.add((u, v)) # 将此边加入生成树中
numS = numS - 1
通常在kruskal算法中,采用堆来存放边的集合,每次选择边只需 O ( log ∣ E ∣ ) O(\log |E|) O(log∣E∣)的时间。此外,由于生成树T中的所有边可视为一个等价类,每次添加新边的过程类似求解等价类的过程,可以采用并查集的数据结构来描述T,构造T总的时间复杂度为 O ( ∣ E ∣ log ∣ E ∣ ) O(|E|\log |E|) O(∣E∣log∣E∣)。适合求解稀疏图的最小生成树。
把带权路径长度最短的那条路径称为最短路径。
重要性质:两点之间的最短路径也包含了路径上其他点间的最短路径。
设置一个集合S,记录已求得的最短路径的顶点。初始时把源点
v
0
v_0
v0放入
S
S
S。集合S每并入一个新顶点
v
i
v_i
vi,都要修改
v
0
v_0
v0到集合
V
−
S
V-S
V−S中顶点的当前最短路径长度。
设置两个辅助数组:
假设从顶点0出发,即 v 0 = 0 v_0=0 v0=0,集合 S S S最初只包含顶点0,邻接矩阵arcs表示带权有向图,arcs[i][j]表示有向边<i, j>的权值。若不存在有向边,arcs[i][j]置为 ∞ \infty ∞。
Dijkstra算法基于贪心策略,步骤如下:
Djkstra算法并不适用于带负权值的有向图。
算法时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
递推产生一个n阶方阵序列
A
(
−
1
)
,
A
(
0
)
,
.
.
.
,
A
(
k
)
,
.
.
.
,
A
(
n
−
1
)
A^{(-1)}, A^{(0)},...,A^{(k)},...,A^{(n-1)}
A(−1),A(0),...,A(k),...,A(n−1),其中
A
(
k
)
[
i
]
[
j
]
A^{(k)}[i][j]
A(k)[i][j]表示从顶点
v
i
v_i
vi到
v
j
v_j
vj的路径长度,k表示绕行第k个顶点的运算步骤。
初始时,对于任意两个顶点,若它们之间存在弧,则以此边上的权值作为它们之间的最短路径长度;若它们不存在有向边,则置为
∞
\infty
∞。
以后逐步尝试在原路径中加入顶点k作为中间顶点,若增加中间顶点后,它们之间的路径长度比原来的路径减少了,则以此新路径代替原路径。
算法描述如下:
定义一个n阶反正序列
A
(
−
1
)
,
A
(
0
)
,
.
.
.
,
A
(
n
−
1
)
A^{(-1)}, A^{(0)},...,A^{(n-1)}
A(−1),A(0),...,A(n−1),其中
Floyd算法是一个迭代的过程,每迭代一次,在从 v i v_i vi到 v j v_j vj的最短路径上就多考虑一个顶点。经过n次迭代后,所得到的 A ( n − 1 ) [ i ] [ k ] A^{(n-1)}[i][k] A(n−1)[i][k]就是 v i v_i vi到 v j v_j vj的最短路径,方阵 A ( n − 1 ) A^{(n-1)} A(n−1)保存了任意一对顶点之间的最短路径长度。
Floyd算法允许图中有带负权值的边,但不允许有包含带负权值边的回路。
Floyd算法同样适合带权无向图,因为无向图可视为权值相同往返二重边的有向图。
算法的时间复杂度为 O ( ∣ V ∣ 3 O(|V|^3 O(∣V∣3),但对于中等规模的输入,仍然有效。
DAG图是描述含有公共子式的表达式的有效工具。
用二叉树描述表达式时,相同的子式会重复出现,使用DAG图可以对相同子式的共享,从而节省存储空间。
AOV网:若用DAG图表示一个工程,其顶点表示活动,用有向边
<
v
i
,
v
j
>
<v_i,v_j>
<vi,vj>表示活动
V
i
V_i
Vi必须先于
V
i
V_i
Vi进行,则间这种有向图称为顶点表示活动的网络,记为AOV网。
拓扑排序:由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时:
称这个序列为该图的一个拓扑排序。每个AOV网都有一个或多个拓扑排序序列。
拓扑排序常用算法如下
由于输出每个顶点的同时还有删除以它为起点的边,故拓扑排序的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)。
由于AOC网中各顶点地位平等,每个顶点的编号都是人为的,因此可以按拓扑排序的结果重新编号,生成AOV网的新的邻接存储矩阵,这种邻接矩阵可以时三角矩阵。对于一个图,若其邻接矩阵是三角矩阵,其必存在拓扑排序。
AOE网:在带权有向无环图中,若以顶点表示事件,以有向边表示活动,以边上的权值代表活动的开销,则称其为用边表示活动的图,记为AOE网。
在AOE网中仅有一个入度为0的点,称为开始顶点(源点),表示整个工程的开始;仅有一个出度为0的点,称为结束顶点(汇点),表示整个工程的结束。
从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,它决定了完成整个工程的最短时间。关键路径上的活动称为关键活动。
求解关键路径的算法步骤如下:
关键路径上的所有活动都是关键活动,可以通过加快关键活动来缩短整个工程的工期。但不能任意缩短关键活动,因为一旦缩短到一定程度,关键活动就可能会变成非关键活动。
AOE网中的关键路径不是唯一的,对于有多条关键路径的AOE网,只提高一条关键路径上的关键活动速度并不能缩短整个工期。
可以判断一个有向图是否有环的算法: