1.1 图的基本概念
1.1.1基本概念:
图G(Graph)由顶点集V(Vertex)和边集E(Edge)组成,记成G=(V,E),
其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间关系(边)集合。
|V|表示图G中顶点的个数,也称图G的阶;|E|表示图G中边的条数。
注意:线性表可以为空表,树可以为空树,但图不可以为空,即:V一定是非空集。
注:
有限非空集说明图不可以为空,线性表、树都可以为空。
1.1.2无向图&有向图:
====》无向图:就好像微信中的好友,相互之间肯定是好友,没有方向。
而有向图:就好像是微博中的好友,你可能是他的粉丝,但是他不一定是你的分数,他发的东西你能接收到,但是你发的东西他是接收不到的,这就是有方向的。
无向图:
若E是无向边(简称边)的有限集合时,则图G为无向图。边是顶点的无序对,记为
(
v
,
w
)
(v,w)
(v,w)或者
(
w
,
v
)
(w,v)
(w,v),因为
(
v
,
w
)
=
(
w
,
v
)
(v,w)=(w,v)
(v,w)=(w,v),其中
v
、
w
v、w
v、w是顶点。可以说顶点w和顶点v互为邻接点。边
(
v
,
w
)
(v,w)
(v,w)依附于顶点w和v,或者说边
(
v
,
w
)
(v,w)
(v,w)和顶点
v
、
w
v、w
v、w相关联。
G
2
=
(
V
2
,
E
2
)
G_2=(V_2,E_2)
G2=(V2,E2)
V
2
V_2
V2= { A,B,C,D,E }
E_2={(A,B),(B,D),(B,E),(C,D),(C,E),(D,E)}
有向图:
若E是有向边(简称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为
<
v
,
w
>
<v,w>
<v,w>,其中v,w是顶点,v称为弧尾,w称为弧头,<v,w>称为从顶点v到顶点w的弧,也称v邻接到w,或w邻接自v。<v,w>≠<v,w>
G
1
=
(
V
1
,
E
1
)
G_1=(V_1,E_1)
G1=(V1,E1)
V
1
V_1
V1= { A,B,C,D,E }
E_2={<A,B>,<A,C>,<A,D>,<A,E>,<B,A>,<B,C>,<B,E>,<C,D>,}
1.1.3简单图&多重图:
====》数据结构中我们只讨论简单图。
1.1.4顶点的度、入度、出度:
对于无向图:
顶点v的度是指:依附于该顶点的边的条数,记为TD(v)。
在具有n个顶点,e条边的无向图中,
∑
i
=
0
n
T
D
(
V
i
)
\displaystyle\sum_{i=0}^nTD(V_i)
i=0∑nTD(Vi)=2e,即:无向图的全部顶点的度的和等于边数的2倍。
====》因为无向图的每条边都对应了2个顶点。故e条边,总共就是2e。
对于有向图:
入度是以顶点v为终点的有向边的数目,记为ID(v);
出度是以顶点v为起点的有向边的数目,记为OD(v)。
顶点v的度是指:入度和出度之和,记为TD(v)=ID(v)+OD(v).
在具有n个顶点,e条边的有向图中,
∑
i
=
0
n
I
D
(
V
i
)
\displaystyle\sum_{i=0}^nID(V_i)
i=0∑nID(Vi)=
∑
i
=
0
n
O
D
(
V
i
)
\displaystyle\sum_{i=0}^nOD(V_i)
i=0∑nOD(Vi)=e,
====》因为有向图的每条边1个顶点贡献一个入度,给另一个顶点贡献一个出度,故相等且等于e。
1.1.5 顶点-顶点的关系描述:
路径——顶点
v
p
v_p
vp到顶点
v
q
v_q
vq之间的一条路径是指顶点序列,
v
p
v_p
vp,
v
1
v_1
v1,
v
2
v_2
v2,…,
v
m
v_m
vm,
v
p
v_p
vp
====》无向图路径的方向是无向的,有向图路径的方向要与弧方向一致。
====》 顶点之间也可能不存在路径。
回路——第一个顶点和最后一个顶点相同的路径称为回路或环。
简单路径——在路径序列中,顶点不重复出现的路径称为简单路径。
简单回路——除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
路径长度——路径上边的数目。
点到点的距离——从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。
若从u到v根本不存在路径,则记距离为无穷(∞).
连通与强连通
无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。
有向图中若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这个顶点是强连通的。
小考点:
①n个顶点的连通图(无向图)和强连通图(有向图)最少有多少条边?
连通图 :n-1 强连通图:n
②n个顶点的非连通图最多有多少条边?
非连通图 :
∁
n
−
1
2
\complement^2_{n-1}
∁n−12
连通图及连通分量(极大连通子图)仅针对无向图,
强连通图及强连通分量(极大强连通子图)仅针对有向图。
连通分量:无向图中的极大连通子图(子图必须连通,且包含尽可能多的顶点和边)
强连通分量:有向图中的极大强连通子图(子图必须强连通,同时保留尽可能多的边)
种类 | 无向图 | 有向图 |
概念一 | 连通图 | 强连通图 |
概念二 | 连通分量 | 强连通分量 |
无向图中的极大连通子图(子图必须连通,且包含尽可能多的顶点和边) | 有向图中的极大强连通子图(子图必须强连通,同时保留尽可能多的边) | |
概念三 | 极小连通子图 | 极小强连通子图 |
连通子图且包含的边最少 | ||
概念四 | 生成树 | |
连通图包含全部顶点的一个极小连通子图 | ||
n个顶点图的生成树有n-1条边 | ||
概念五 | 生成森林 | |
非连通图所有连通分量的生成树组成生成森林 | ||
1.1.6完全图:
1.1.7 子图(有向图和无向图都适用):
====》并非任意挑几个点,几条边都能构成子图(前提是图,若不是图自然不是子图)
生成子图:若有满足 V ( G ‘ ) = V ( G ) V(G‘)=V(G) V(G‘)=V(G)的子图,则称其为生成子图。(包含所有顶点,去掉某些边)
1.1.8 生成树和生成森林
连通图的生成树是包含图中全部顶点的一个极小连通子图(极小:边尽可能的少,但要保持连通)
====》生成树有可能会有多个
====》若图中顶点数为n,则它的生成树含有n-1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
在非连通图中,连通分量的生成树构成了非连通的生成森林。
====》生成树的应用:
修路,若经费有限,且要所有结点都相连,故就用到生成树的概念(极小连通子图,保证边数少,且所有都有连通),那可能会生成多个生成树,要想从多个生成树中找到最优的生成树,就要知道对应边的长短(或修路的成本)。这就引出了权。
1.1.9 边的权、带权图/网
边的权——在一个图中,每条边都可以标上具有某些含义的数值,该数值称为该边的权值。
带权图/网——边上带有权值的图称为带权图,也称网。
带权路径长度——当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度。
1.2.0 几种特殊形态的图
无向完全图——无向图中任意两个顶点之间存在边
====》若无向图的顶点数|V|=n,则|E|
∈
\in
∈[0,
∁
n
2
\complement^2_{n}
∁n2]=[0,n(n-1)/2]
有向完全图——有向图中任意两个顶点之间都存在方向相反的两条弧。
====》若无向图的顶点数|V|=n,则|E|
∈
\in
∈[0,2
∁
n
2
\complement^2_{n}
∁n2]=[0,n(n-1)]
稀疏图——边数很少的图
稠密图——与稀疏图相反(边数很多的图)
====》没有绝对的界限,一般来说|E| < |V| log |V| (非绝对),可将g视为稀疏图
树——结点之间不存在回路,且连通的无向图
====》n个顶点的树,必有n-1条边。
====》常见考点,n个顶点的图,若|E| > n-1,则一定有回路。
森林——各个子图都是极小的,且是连通的。
有向树——一个顶点的入度为0,其余顶点的入度均为1的有向图,称为有向树。
====》并不是一个强连通图。
1.2 图的存储——邻接矩阵(一维数组+二维数组)
1.2.1邻接矩阵
无向图
====》0代表两个结点没有连接,1代表两个节点有连接。
====》无向图中,邻接矩阵的关于y=-x对称的。(因为边对两个结点都是连接的)
====》求无向图中,顶点的度(空间复杂度为O(|v|),|v|为顶点的个数)
第i个结点的度=第i行(或第i列)的非零元素个数。
====》无向图的邻接矩阵是对称矩阵,可以压缩存储(只存储上三角区/下三角区)、
压缩对称矩阵
有向图
====》有向图中,弧只对一个结点有效,故没有无向图中的特点
====》求有向图中,顶点的入度和出度及其度(空间复杂度为O(|v|),|v|为顶点的个数)
第i个结点的出度=第i行的非零元素个数。
第i个结点的入度=第i列的非零元素个数。
第i个结点的度=第i行、第i列的非零元素个数之和。
#define MaxVertexNum 100 //顶点数目的最大值
typedef struct{
char Vex[MaxVertexNum]; //顶点表(顶点的信息,可以存放更复杂的信息)
int Edge[MaxVertexNum] [MaxVertexNum]; //邻接矩阵,边表(这边只用0和1可使用bool)
int vexnum,arcnum; //图的当前顶点数和边数/弧数
}MGraph;
结点数为n的图G=(V,E)的邻接矩阵A是nxn的。将G的顶点编号为
v
1
v_1
v1,
v
2
v_2
v2,…,
v
n
v_n
vn,则:
A
[
i
]
[
j
]
=
{
1
若
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
是
E
(
G
)
中
的
边
0
若
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
不
是
E
(
G
)
中
的
边
A[i][j]=\begin{cases} 1 &\text若(v_i,v_j)或<v_i,v_j>是E(G)中的边\\ 0 &\text若(v_i,v_j)或<v_i,v_j>不是E(G)中的边\end{cases}
A[i][j]={10若(vi,vj)或<vi,vj>是E(G)中的边若(vi,vj)或<vi,vj>不是E(G)中的边
1.2.2邻接矩阵法存储带权图(网)
====》就是之前邻接矩阵存放0和1,改为
∞
∞
∞和对应的权值。
====》也有把自己指向自己的权值设为0.
====》空间复杂度为
O
(
∣
v
∣
2
)
O(|v|^2)
O(∣v∣2),|v|为顶点的个数——只和顶点数相关,和实际的边数无关。
====》适合用于存储稠密图(因为稀疏图的话就有点浪费空间了)
#define MaxVertexNum 100 //顶点数目的最大值
#define INFINITY 最大的int值 //宏定义常量“无穷”
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum]; //顶点
EdgeType Edge[MaxVertexNum] [MaxVertexNum]; //边的权
int vexnum,arcnum; //图的当前顶点数和边数/弧数
}MGraph;
1.2.3邻接矩阵法的性质(不带权值的图)
设图G的邻接矩阵A(矩阵元素为0/1),则
A
n
A^n
An的元素
A
n
[
i
]
[
j
]
A^n[i][j]
An[i][j]等于由顶点i到顶点j的长度为n的路径的数目。
====》举例说明。若
A
2
[
1
]
[
4
]
A^2[1][4]
A2[1][4]即:
A
[
1
]
[
4
]
×
A
[
1
]
[
4
]
=
a
1
,
1
a
1
,
4
+
a
1
,
2
a
2
,
4
+
a
1
,
3
a
3
,
4
+
a
1
,
4
a
4
,
4
A[1][4]×A[1][4]=a_{1,1}a_{1,4}+a_{1,2}a_{2,4}+a_{1,3}a_{3,4}+a_{1,4}a_{4,4}
A[1][4]×A[1][4]=a1,1a1,4+a1,2a2,4+a1,3a3,4+a1,4a4,4(矩阵的第一行分别乘以矩阵的第四列),它的实际意义是:表示矩阵A从结点1(i)到结点4(j)中路径长度为2(n)的数目,其中
a
1
,
1
a
1
,
4
a_{1,1}a_{1,4}
a1,1a1,4表示:从结点1到结点1在从结点1到结点2(由于我们是简单图,没有结点到自身,故没有该路径).
====》这样最终得到的新的邻接矩阵,就是表示刚刚叙述的那个意义。
====》邻接矩阵的缺点:空间复杂度高 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
1.3 图的存储——邻接表(顺序+链式)
//“顶点”
typedef struct VNode{
VertexType data; //顶点信息
ArcNode *first //第一条边/弧
}VNode,AdjList[MaxVertexNum];
//用邻接表存储的图
typedef struct{
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
//“边/弧”
typedef struct ArcNode{
int adjvex; //边/弧指向哪个结点
struct ArcNode *next; //指向下一条弧的指针
//InfoType info; //边权值
}ArcNode;
====》对比之前讲的树的孩子表示法相同
====》无向图:边结点的数量是2|E|,整体空间复杂度为O(|V|+2|E|)
有向图:边结点的数量是|E|,整体空间复杂度为O(|V|+|E|)
====》图的邻接表表示方式并不唯一,但只要确定了顶点编号,图的邻接矩阵表示方式唯一。
====》邻接表缺点:找有向图的入边不方便的。
====》邻接表(存储无向图)的缺点:每条边对应两份冗余信息,删除顶点、删除边等操作时间复杂度高。
====》解决办法:使用邻接多重表存储无向图
1.4 图的存储——十字链表(存储有向图)
====》空间复杂度:O(|V|+|E|)
====》解决邻接矩阵复杂度太高问题。
====》也可解决邻接表中,找入边不方便的问题。
1.5 图的存储——邻接多重表(存储无向图)
====》空间复杂度:O(|V|+|E|)
====》解决邻接矩阵复杂度太高问题。
====》也可解决邻接表中,删除边、删除节点等操作很方便。
1.6 图的基本操作
Adjacent(G,x,y):判断图G是否存在边<x,y>或者(x,y)。
Neighbors(G,x):列出图G中与结点x邻接的边。
InsertVertex(G,x):在图G中插入顶点x。
DeleteVertex(G,x):从图G中删除顶点x。
AddEdge(G,x,y):若无向边(x,y)或有向边<x,y>不存在,则向图G中添加该边。
RemoveEdge(G,x,y):若无向图(x,y)或有向边<x,y>存在,则向图G中删除该边。
FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。
NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。
Get_edge_value(G,x,y):获取图G中边(x,y)或<x,y>对应的权值。
Set_edge_value(G,x,y,v):设置图G中边(x,y)或<x,y>对应的权值为v。
====》基本操作肯定适合存储结构密不可分的,故这边主要讨论的是邻接矩阵和邻接表的基本操作。
时间复杂度 | |||
基本操作 | 有/无向图 | 邻接矩阵 | 邻接表 |
Adjacent(G,x,y): | 无向图 | O(|1|) | O(1)~O(|V|) |
有向图 | O(1) | O(1)~O(|V|) | |
Neighbors(G,x): | 无向图 | O(|V|):遍历矩阵某行(列) | O(1)~O(|V|):遍历根节点链表 |
有向图 | O(|V|):遍历矩阵某行(列) | 出边:O(1)~O(|V|),遍历根节点链表 入边:O(|E|):遍历所有根节点 | |
InsertVertex(G,x): | 无向图 | O(1) | O(1) |
有向图 | O(1) | O(1) | |
DeleteVertex(G,x): | 无向图 | O(|V|) | O(1)~O(|E|) |
有向图 | O(|V|) | 删出边:O(1)~O(|V|) 删入边:O(|E|) | |
AddEdge(G,x,y): | 无向图 | O(1) | O(1) |
有向图 | O(1) | O(1) | |
RemoveEdge(G,x,y): | 无向图 | O(1) | O(1)~O(| v |) |
有向图 | O(1) | O(1)~O(| v |) | |
FirstNeighbor(G,x): | 无向图 | O(1)~O(|V|) | O(1) |
有向图 | O(1)~O(|V|) | 找出边邻接点:O(1) ~~找入边邻接点~~:O(1)~O(|E|) | |
NextNeighbor(G,x,y): | 无向图 | O(1)~O(|V|) | O(1) |
有向图 | O(1) | O(1)~O(|V|) | |
Get_edge_value(G,x,y): | 无向图 | O(1) | O(1)~O(|V|) |
有向图 | O(1) | O(1)~O(|V|) | |
Set_edge_value(G,x,y,v): | 无向图 | O(1) | O(1)~O(|V|) |
有向图 | O(1) | O(1)~O(|V|) |
1.7 图的遍历
1.7.1广度优先遍历(BFS)
树的广度优先遍历(层序遍历)
①若树非空,则根结点入队。
②若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队。
③重复②直到队列为空。
====》不存在“回路”,搜索相邻的结点时,不可能搜到已经访问过的结点
图的广度优先遍历(Breadth-First-Search,BFS)
要点:
①找到与一个顶点相邻的所有顶点。
②标记哪些顶点被访问过。
③需要一个辅助队列。
====》问题①:可用FirstNeighor(G,x)
找到第一个邻接点,NextNeighbor(G,x,y)
找到后一个与之相连接的邻接点。
====》问题②:使用 bool visited[MAX_VERTEX_NUM]; //访问标记数组
bool visited[MAX_VERTEX_NUM]; //访问标记数组,初始值都为false
//广度优先遍历
void BFS(Graph G,int v){ //从顶点出发,广度优先遍历图G
visit(v); //访问初始顶点v
visited[v]=TRUE; //对v做已访问标记
Enqueue(Q,v); //顶点v入队列Q
while(!isEmpty(Q)){
DeQueue(Q,v); //顶点v出队列
for(w=FirstNeighor(G,x);w>=0;NextNeighbor(G,x,y)){
//检测v所有邻接点
if(!visited[w]) //w为v的尚未访问的邻接顶点
visit(w); //访问顶点w
visited[w]=TRUE; //对w做已访问标记
EnQueue(Q,w) //顶点w入队列
}//if
}//while
若是非连通图,则无法遍历完所有结点,故需要加上下面这段程序:
void BFSTraverse(Gragh G){ //对图G进行广度优先遍历
for(i=0;i<G.vexnum;++i)
visited[i]=FALSE; //访问标记数组初始化
InitQueue(Q); //初始化辅助队列Q
for(i=0;i<G.vexnum;i++) //从0号顶点开始遍历
if(!visited[i]) //对每个连通分量调用一次BFS
BFS(G,i); //vi未访问过,从vi开始BFS
}
}
====》搜索相邻的顶点时,有可能搜到已经访问过的顶点
====》采用邻接矩阵遍历序列:顶点后多个邻接点是按顺序存储的,故是一致的。
采用邻接表遍历序列是可变的
====》也就是说:
同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一。
同一个图的邻接表表示方式唯一,因此广度优先遍历序列不唯一。
====》结论:对于无向图,调用BFS函数的次数=连通分量数
====》空间复杂度:来源于辅助队列。最坏情况,辅助队列大小为O(|V|)
时间复杂度:
邻接矩阵存储的图:
访问|V|个顶点需要O(|V|)的时间,查找每个顶点的邻接点都需要O(|V|)的时间,而总共有|V|个顶点,
时间复杂度=
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)
邻接表存储的图:
访问|V|个顶点需要O(|V|)的时间,查找每个顶点的邻接点都需要O(|E)的时间,
时间复杂度=
O
(
∣
V
+
∣
E
∣
)
O(|V+|E|)
O(∣V+∣E∣)
广度优先生成树
广度优先生成树由广度优先遍历过程确定。由于邻接表的表示方式不唯一因此基于邻接表的广度优先生成树也不唯一。
广度优先生成森林
对非连通的广度优先遍历,可得到广度优先生成森林
1.7.2深度优先遍历(DFS)
树的深度优先遍历(树本身就是一种特殊的图)分为:树的先根遍历和后根遍历。
图的深度优先遍历:类似树的先根遍历。
//树的先根遍历
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R); //访问根节点
while(R还有下一个子树T)
PreOrder(T); //先根遍历下一棵子树
}
}
树:新找到的相邻结点一定是没有访问过的
图:有可能是已经已经被访问过了
//图的深度优先遍历
bool visited[MAX_VERTEX_NUM] //访问标记数组
void DFS(Graph G,int v) { //从顶点v出发,深度优先遍历图G
visit(v); //访问顶点v
visited[v]=TRUE; //设已访问标记
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
if(!visited[w]){ //w为u的尚未访问的邻接顶点
DFS(G,w);
}//if
}
若是费连通图,则无法遍历完所有结点
Void DFSTraverse(Graph G){ //对图G进行深度优先遍历
for(v=0;v<G.vexnum;++v)
visited[v]=FALSE; //初始化已访问标记数据
for(v=0;v<G.vexnum;++v) //本代码中是从v=0开始遍历
if(!visited[v])
DFS(G,v);
}
====》空间复杂度:来自函数调用栈,最坏情况,递归深度为O(|V|),最好情况,O(1)
====》时间复杂度=访问各结点所需时间+探索各条边所需时间
邻接矩阵存储的图:
访问|V|个顶点需要O(|V|)的时间
查找每个顶点的邻接点都需要O(|V|)的时间,而总共有|V|个顶点
时间复杂度=
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)
邻接表存储的图:
访问|V|个顶点需要O(|V|)的时间
查找每个顶点的邻接点共需要O(|E|)的时间,
时间复杂度=
O
(
∣
V
∣
+
∣
E
∣
)
O(|V|+|E|)
O(∣V∣+∣E∣)
深度优先遍历序列
====》邻接表不同则遍历序列不同。
====》找序列就是一个结点一个结点往下遍历,遍历到没有就跳回上级继续遍历。
深度优先生成树
其实整个深度优先遍历就是在探索边所连接的顶点这样的过程,若通过边找到了此时还没有被访问过的结点,则将边标红。
同一个图的邻接矩阵表示方式唯一,因此深度优先遍历序列唯一,深度优先生成树也唯一。
同一个图的邻接表表示方式不唯一,因此深度优先遍历序列不唯一,深度优先生成树也不唯一。
深度优先生成森林
调用一次DFS就会生成一棵深度优先生成树,两次则两棵,就可形成森林。
总结
对无向图进行BFS/DFS遍历,调用BFS/DFS函数的次数=连通分量数
对于连通图,只需调用1次BFS/DFS
对有向图进行BFS/DFS遍历,调用BFS/DFS函数的次数要具体问题具体分析
若起始顶点到其他各顶点都有路径,则只需调用1次BFS/DFS函数。
强连通图,从任一结点出发都只需调用1次BFS/DFS
1.8最小生成树(最小代价树)
生成树:对于连通的无向图,包含各个顶点,且每个顶点都能连通,边的数量为n-1条。
====》详细的定义可看:1.1.8
对于一个带权连通无向图G=(V,E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spanning-Tree,MST)
最小生成树可能有多个,但是边的权值之和总是唯一且最小的
最小生成树的边数=顶点数-1。砍掉一条则不连通,增加一条边则会出现回路。
若一个连通图本身就是一棵树,则其最小生成树就是它本身
只有连通图才有生成树,非连通图只有生成森林
求最小生成树:①Prim算法②Kruskal算法
Prim算法(普里姆):
从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
====》从点入手,找最小边
====》时间复杂度:
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)适合用于边稠密图
实现思想:
数组1[isJoin]:标记各结点是否已加入树
数组2[lowCost]:各结点加入树的最低代价
从
V
0
V_0
V0开始,总共需要n-1轮处理。(n个顶点)
每一轮处理:循环遍历所有个结点,找到lowCost最低的,且没加入树的顶点。再次循环遍历,更新还没加入的各个顶点的lowCost值。
====》每一轮时间复杂度
O
(
2
n
)
O(2n)
O(2n),要执行n-1轮。故:
====》总时间复杂度
O
(
n
2
)
O(n^2)
O(n2),即
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)
Kruskal算法(克鲁斯卡尔):
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点都连通。
====》从边入手,找最小边
====》时间复杂度:
O
(
∣
E
∣
l
o
g
2
∣
E
∣
)
O(|E|log_2|E|)
O(∣E∣log2∣E∣)适合用于边稀疏图
实现思想:
初始:将各条边按权值排序,且还要保存对应连接的两个结点。
检查第一条边的两个顶点是否连通(是否属于同一个集合)【用的是并查集的内容,不连通,连起来,已连通,跳过。本来不是一个集合,连起来就是一个集合,需要从头遍历】。
共执行e轮(e为边数),每轮判断两个顶点是否属于同一集合,需要
O
(
l
o
g
2
e
)
O(log_2e)
O(log2e)
====》总时间复杂度就为
O
(
e
l
o
g
2
e
)
O(elog_2e)
O(elog2e)
1.9最短路径 BFS算法
单源最短路径问题
——“G港”是个物流集散中心,经常需要往各个城市运东西,怎么运送距离最近?
BFS算法(无权图)
注:无权图可以视为一种特殊的带权图,只是每条边的权值都为1
bool visited[MAX_VERTEX_NUM]; //访问标记数组
//广度优先遍历
void BFS(Graph G,int v){ //从顶点出发,广度优先遍历图G
visit(v); //访问初始顶点v
visited[v]=TRUE; //对v做已访问标记
Enqueue(Q,v); //顶点v入队列Q
While(!isEmpty(Q)){
DeQueue(Q,v); //顶点v出队列
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(W,v,w))
//检测v所有邻接点
if(!visited[w]){ //w对v的尚未访问的邻接顶点
visit(w); //访问顶点w
visited[w]=TRUE;//对w做已访问标记
Enqueue(Q,w); //顶点w入队列
}//if
}//while
}
====》根据上面进行修改:
对BFS的小修改,在visit一个顶点时,修改其最短路径d[]并在path[]记录前驱结点
//求顶点u到其他顶点的最短路径
void BFS_MIN_Distance(Graph G,int u){
//d[i]表示从u到i结点的最短路径
for(i=0;i<G.vexnum;++i){
d[i]= ∞ ; //初始化路径长度
path[i]= -1; //最短路径从哪个顶点过来
}
d[u]=0;
visited[u]=TRUE; //对u做已访问标记
Enqueue(Q,u); //顶点u入队列Q
While(!isEmpty(Q)){ //BFS算法主过程
DeQueue(Q,v); //队头元素u出队
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(W,u,w))
if(!visited[w]){ //w对v的尚未访问的邻接顶点
d[w]=d[u]+1; //路径长度加1
path(w)=u; //最短路径应从u到w
visited[w]=TRUE;//对w做已访问标记
Enqueue(Q,w); //顶点w入队列
}//if
}//while
}
====》d[ ]记录的是最短路径长度
====》path[ ]:可查找最短路径所有前驱(完整信息)。
Dijkstra(迪杰斯特拉)算法(带权图、无权图)
BFS算法的局限性:带权值的图不适用,求单源最短路径只适用于无权图,或所有边和权值都相同的图.
带权路径长度——当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度。
实现思想:
初始化:从
v
0
v_0
v0开始,初始化三个数组信息如下:
数组1:final[ ]:标记各顶点是否已找到最短路径
数组2:dist[ ]:最短路径长度
数组3:path[ ]路径上的前驱
每一轮:循环遍历所有结点,找到还没确定最短路径,且dist最小的顶点
V
1
V_1
V1,令final[i]=true。
检查所有邻接自
V
1
V_1
V1的顶点,若其final值为false,则更新dist和path信息。
====》dist[ ]记录的是最短路径长度
====》path[ ]:可查找最短路径所有前驱(完整信息)。
代码实现:
初始化:若从
v
0
v_0
v0开始,令final[0]=true;dist[0]=0;path[0]=-1.
其余顶点final[k]=false; dist[k]=arcs[0][k]; path[k]=(arcs[0][k]=∞)?-1:0
n-1轮处理:循环遍历所有顶点,找到还没确定最短路径,且dist最小的顶点V,令final[i]=true.并检查所有邻接自
V
1
V_1
V1的顶点,对于邻接字
V
1
V_1
V1的顶点
V
1
V_1
V1,若final[j]=false且dist[i]+arcs[i][j]<dist[j]
,则令dist[j]=dist[i]+arcs[i][j];path[j]=i.
(注:arcs[i][j]表示
V
i
V_i
Vi到
V
j
V_j
Vj的弧的权值)
====》时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)即
O
(
∣
V
2
∣
)
O(|V^2|)
O(∣V2∣)
用于负权值带权图:可能失效(不适合)。
每对顶点间的最短路径
——各个城市之间也需要互相往来,互相之间怎么走距离最近?
Floyd(弗洛伊德)算法(带权图、无权图)
使用动态规划思想,将问题的求解分为多个阶段。
对于n个顶点的图G,求任意一堆顶点
V
i
−
>
V
j
V_i -> V_j
Vi−>Vj之间的最短路径可分为如下几个阶段:
#初始:不允许在其他顶点中转,最短路径是?
#0:若允许在
V
0
V_0
V0中转,最短路径是?
#1:若允许在
V
0
V_0
V0、
V
1
V_1
V1中转,最短路径是?
#2:若允许在
V
0
V_0
V0、
V
1
V_1
V1、
V
2
V_2
V2中转,最短路径是?
…
#n-1:若允许在
V
0
V_0
V0、
V
1
V_1
V1、
V
2
V_2
V2…
V
n
−
1
V_{n-1}
Vn−1中转,最短路径是?
具体过程:
初始矩阵:
A
(
−
1
)
A^{(-1)}
A(−1):目前来看、各顶点间的最短路径长度。
p
a
t
h
(
−
1
)
path^{(-1)}
path(−1):两个顶点之间的中转点。
#初始:不允许在其他顶点中转,最短路径是?
====》矩阵A其实就是邻接矩阵。
#0:若允许在
V
0
V_0
V0中转,最短路径是?求
A
(
0
)
A^{(0)}
A(0)
p
a
t
h
(
0
)
path^{(0)}
path(0)
#1:若允许在
V
0
V_0
V0、
V
1
V_1
V1中转,最短路径是?——求
A
(
1
)
A^{(1)}
A(1)
p
a
t
h
(
1
)
path^{(1)}
path(1)
#2:若允许在
V
0
V_0
V0、
V
1
V_1
V1、
V
2
V_2
V2中转,最短路径是??——求
A
(
2
)
A^{(2)}
A(2)
p
a
t
h
(
2
)
path^{(2)}
path(2)
====》n指的是结点的个数。
//……准备工作,根据图的信息初始化矩阵A和path(如上图)
for(int k=0;k<n;k++){ //考虑以Vk作为中转点
for(int i =0;i<n;i++){ //遍历整个矩阵,i为行号,j为列号
for(int j=0;j<n;j++){
if(A[i][j] > A[i][k]+A[k][j]){ //以Vk为中转点的路径更短
A[i][j]=A[i][k]+A[k][j]; //更新最短路径长度
path[i][j]=k; //中转点
}
}
}
}
====》时间复杂度: O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3),空间复杂度, O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
1.9有向无环图(DAG:Directed Acyclic Graph)
若一个有向图中不存在环,则称为有向无环图,简称DAG图。
1.9.1DAG描述表达式
====》顶点中不可能出现重复的操作数
方法:
Step1:把各个操作数不重复地排成一排
Step2:标出各个运算符的生效顺序(先后顺序有点出入无所谓)
Step3:按顺序加入运算符,注意“分层”
Step4:从底向上逐层检查同层的运算符是否可以合体
1.9.2AOV网
AOV网(Activity On Vertex NetWork,用顶点表示活动的网):
用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边
<
V
i
,
V
j
>
<V_i,V_j>
<Vi,Vj>表示活动
V
i
V_i
Vi必须先于活动
V
j
V_j
Vj进行
1.9.3拓扑排序
在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:①每个顶点出现且只出现一次。
②若顶点A在序列中排在顶点B的前面,则在途中不存在从顶点B到顶点A的路径。
或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面。每个AOV网都有一个或多个拓扑排序序列。
拓扑排序:放到AOV网中其实就是找到做事的先后排序。
拓扑排序的实现:
①从AOV网中选择一个没有前驱(入度为0)的顶点并输出。
②从网中删除该顶点和所有以它为起点的有向边。
③重复①和②直到当前AOV网为空或当前网中不存在无前驱的顶点为止。
====》并不是所有的图都可以拓扑排序的,若是有回路则不行。
(因为当为步骤①时,会有所有顶点入度 > 0)
#define MaxVertexNum 100 //图中顶点数目的最大值
typedef struct ArcNode{ //边表结点
int adjvex; //该弧所指向的顶点的位置
struct ArcNode *nextarc;//指向下一条弧的指针
//InfoType info; //网的边权值
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的弧欸点指针
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices; //邻接表
int vexnum,arcnum; //图的顶点数和弧数
}Graph; //Graph是以邻接表存储的图类型
bool TopologicalSort(Graph G){
InitStack(S); //初始化栈,存储入度为0的顶点
for(int i=0;i<G.vexnum;i++)
if(indegree[i] == 0)
Push(S,i); //将所有入度为0的顶点进栈
int count =0; //计数,记录当前已经输出的顶点数
while(!IsEmpty(S)){ //栈不空,则存在入度为0的顶点
Pop(S,i); //栈顶元素出栈
print[count++]=i; //输出顶点i
for(p=G.vertices[i].firstarc;p;p=p->nextarc){
//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈S
v=p->adjvex;
if(!--indegree[v])
Push(S,v); //入度为0,则入栈
}
}//while
if(count < G.vexnum)
return false; //排序失败,有向图中有回路
else
return true; //拓扑排序成功
}
indegreep[] 当前顶点入度
print[] 记录拓扑序列
S 保存度为0的顶点(也可用队列)
====》时间复杂度:O(|V|+|E|),若采用邻接矩阵,则需要 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
对一个AOV网,若采用下列步骤进行排序,则称之为逆拓扑排序:
①从AOV网中选择一个没有后继(出度为0)的顶点并输出。
②从网中删除该顶点和所有以它为起点的有向边。
③重复①和②直到当前AOV网为空。
逆拓扑排序的实现:
void DFSTraverse(Graph G){ //对图G进行深度优先遍历
for(v=0;v<G.vexnum;++v)
visited[v]=FALSE; //初始化已访问标记数据
for(v=0;v<G.vexnum;++v) //本代码中是从V=0开始遍历
if(!visited[v])
DFS(G,v);
}
void DFS(Graph G,int v) //从顶点v出发,深度优先遍历图G
visit(v); //访问顶点v
visited[v]=TRUE; //设已访问标记
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(W,u,w))
if(!visited[w]){ //w对v的尚未访问的邻接顶点
DFS(G,w);
}//if
print(v); //输出顶点
}
2.0关键路径
2.0.1AOE网
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edhe NetWork)
AOE网具有以下两个性质:
①只有在某顶点所代表的时间发生后,从该顶点出发的各有向边所代表的活动才能开始;
②只有在进行某顶点的各有向边所代表的活动都已结束时,该顶点所代表的时间才能发生。
另外,有些活动是可以并行进行的。
在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;
也仅有一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。
从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长。
事件
v
k
v_k
vk的最早发生时间
v
e
(
k
)
ve(k)
ve(k)——决定了所有从
v
k
v_k
vk开始的活动能够开工的最早时间。
活动
a
i
a_i
ai的最早发生时间
e
(
i
)
e(i)
e(i)——指该活动弧的起点所表示的时间的最早发生时间。
事件
v
k
v_k
vk的最迟发生时间
v
l
(
k
)
vl(k)
vl(k)——它是指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间。
活动
a
i
a_i
ai的最迟开始时间
l
(
i
)
l(i)
l(i)——它是指该活动弧的终点所表示时间的最迟发生时间与该活动所需时间之差。
活动 a i a_i ai的时间余量 d ( i ) = l ( i ) − e ( i ) d(i)=l(i)-e(i) d(i)=l(i)−e(i)——表示在不增加完成整个工程所需总时间的情况下,活动 a i a_i ai可以拖延的时间,若一个活动的时间余量为零,则说明该活动必须要如期完成, d ( i ) = 0 d(i)=0 d(i)=0即: l ( i ) = e ( i ) l(i)=e(i) l(i)=e(i)的活动 a i a_i ai是关键活动,由关键活动组成的路径就是关键路径。
2.0.2求关键路径的步骤
①求所有事件的最早发生时间 ve()
②求所有事件的最迟发生时间 vl()
③求所有活动的最早发生时间 e()
④求所有活动的最迟发生时间 l()
⑤求所有活动的时间余量d()
d(i)=0的活动就是关键活动,由关键活动可得到关键路径。
①求所有事件的最早发生时间 ve()
按拓扑排序序列,依次求各个顶点ve(k):
v
e
ve
ve(源点)=0
v
e
(
k
)
=
M
a
x
ve(k)=Max
ve(k)=Max {
v
e
(
j
)
ve(j)
ve(j)+Weight(
v
j
v_j
vj,
v
k
v_k
vk) },
v
j
v_j
vj为
v
k
v_k
vk的任意前驱。
②求所有事件的最迟发生时间 vl()
按逆拓扑排序序列,依次求各个顶点vl(k):
v
l
vl
vl(汇点)=ve(汇点)
v
l
(
k
)
=
M
i
n
vl(k)=Min
vl(k)=Min {
v
l
(
j
)
vl(j)
vl(j)+Weight(
v
k
v_k
vk,
v
j
v_j
vj) },
v
j
v_j
vj为
v
k
v_k
vk的任意前驱。
③求所有活动的最早发生时间 e()
若边
<
v
k
,
v
j
>
<v_k,v_j>
<vk,vj>表示活动
a
i
a_i
ai,则有e(i)=ve(k).
④求所有活动的最迟发生时间 l()
若边
<
v
k
,
v
j
>
<v_k,v_j>
<vk,vj>表示活动
a
i
a_i
ai,则有l(i)=vl(j)-Weight(
v
k
v_k
vk,
v
j
v_j
vj)
⑤求所有活动的时间余量d()
d(i)=l(i).-e(i)
若关键活动耗时增加,则整个工程的工期将增长
缩短关键活动的时间,可以缩短整个工程的工期
当缩短一定程度时,关键活动可能会变成非关键活动
可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。