一. 图的基本概念
1.图
图G由顶点集V和边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合。
若V={ v 1 , v 2 , . . . v n v_1,v_2,...v_n v1,v2,...vn},则用|V|表示图G中顶点的个数,也称图G的阶,E={(u,v)|u∈V,v∈V},用|E|表示图G中边的条数。
***** : 线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集。
2.无向图,有向图
-
若E是无向边(简称边)的有限集合时,则图是无向图。
边是顶点的无序对,记为(v,w)或(w,v),==(v,w)=(w,v)==其中v,w为顶点。v和w互为邻接点。
-
若E是有向边(也称弧)的有限集合时,则图G为有向图。
弧是顶点的有序对,记为<v,w>,其中v、w是顶点,v称为弧尾,w称为弧头,<v,w>称为从顶点v到顶点w的弧,也称v邻接到w,或w邻接自v。<v,w>≠<w,v>
3. 简单图,多重图
简单图——①.不存在重复边②.不存在顶点到自身的边;
多重图——图G中某两个节点之间的边数多于1条,又允许顶点通过同一条边与自己关联。
4. 顶点的度,入度,出度
-
对于无向图:顶点v的度是指依附于该顶点的边的条数,记为TD()。
在具有n个顶点、e条边的无向图中, ∑ i = 1 n T D ( v i ) = 2 e ∑_{i=1}^nTD(v_i)=2e ∑i=1nTD(vi)=2e
即无向图的全部顶点的度的和等于边数的2倍 -
对于有向图:入度是以顶点v为终点的有向边的数目,记为ID(v):
出度是以顶点v为起点的有向边的数目,记为OD(v)。
顶点v的度等于其入度和出度之和,即TD(v)=ID(v)+OD(v)。
在具有n个顶点、e条边的有向图中, ∑ i = 1 n I D ( v i ) = ∑ i = 1 n O D ( v i ) = e ∑_{i=1}^nID(v_i)=∑_{i=1}^nOD(v_i)=e ∑i=1nID(vi)=∑i=1nOD(vi)=e。(入度=出度=边数)
5. 顶点-顶点关系的描述
-
路径——顶点 v p v_p vp到顶点 v q v_q vq之间的一条路径是指顶点序列 v p , v i 1 , v i 2 , . . . , v i m , v q v_p,v_{i_1},v_{i_2},...,v_{i_m},v_q vp,vi1,vi2,...,vim,vq
-
回路一一第一个顶点和最后一个顶点相同的路径称为回路或环
-
简单路径一一在路径序列中,顶点不重复出现的路径称为简单路径。
-
简单回路一一除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
-
路径长度一一路径上的边的数目
-
点到点的距离一一从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离,若从u到v根本不存在路径,则记该距离为无穷(∞)
-
无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的
-
有问图中,若从顶点到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的
-
若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。
常见考点:
对于n个顶点的无向图G,
若G是连通图,则最少有n-1条边
若G是非连通图,则最多可能有 C n − 1 2 C_{n-1}^2 Cn−12条边 -
若图中任何一对顶点都是强连通的,则称此图为强连通图。
常见考点:
对于n个顶点的有向图G,
若G是强连通图,则最少有n条边(形成回路)。
5.子图,生成子图
无向图中的极大连通子图称为连通分量
有向图中的极大强连通子图称为有向图的强连通分量
连通图的生成树是包含图中全部顶点的一个极小连通子图。
若图中顶点数为n,则它的生成树含有n-1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路
在非连通图中,连通分量的生成树构成了非连通图的生成森林。
6. 边的权,带权图/网
边的权一一在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。
带权图/网一一边上带有权值的图称为带权图,也称网。
带权路径长度一一当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度。
7. 几种特殊形态的图
-
无向完全图——无向图中任意两点都存在边
-
有向完全图——有向图中任意两个顶点之间都存在方向相反的两条弧
-
边很少的图成为稀疏图,反之为稠密图
-
树——不存在回路,且连通的无向图。
-
有向树——一个顶点入度为0,其余顶点的入度均为1的有向图,成为有向树。
二. 图的存储结构
1. 邻接矩阵法
#define MaxVertexNum 100 //顶点数目最大值
typedef struct{
char Vex[MaxVertexNum]; //顶点表
int Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵边表
int vexnum.arcnum; //图的当前顶点数和边数/弧数
}MGraph;
-
无向图: 第i个结点的度=第i行(或第i列)的非零元素个数
-
有向图:第i个结点的出度=第i行的非零元素个数
第i个结点的入度=第i列的非零元素个数
第i个结点的度=第i行、第i列的非零元素个数之和邻接矩阵法存储带权图(网)
#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;
2. 邻接表(顺序+链式存储)
//边/弧
typedef struct ArcNode{
int adjvex; //边/弧指向哪个节点
struct ArcNode *next; //指向下一条弧的指针
//InfnType info; //边权值
}ArcNode;
//顶点
typedef struct VNode{
VertexType data; //顶点信息
ArcNode *first; //第一条边/弧
}VNode,AdjList[MaxVertexNum];
//用邻接表存储的图
typedef struct{
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
邻接表 | 邻接矩阵 | |
---|---|---|
空间复杂度 | 无向图O(|V|+2|E|;有向图O(|V|+|E|) | O($ |
适合用于 | 存储稀疏图 | 存储稠密图 |
表示方式 | 不唯一 | 唯一 |
计算度/出度/入度 | 计算有向图的度、入度不方便,其余很方便 | 必须遍历对应行或列 |
找相邻的边 | 找有向图的入边不方便,其余很方便 | 必须遍历对应行或列 |
3. 十字链表(存储有向图)
十字链表法只用于存储有向图
空间复杂度:O(|V|+|E|)
4. 邻接多重表(存储无向图)
空间复杂度:O(|V|+(E))
删除边、删除结点等操作比较方便。
邻接多重表只适用于存储无向图
邻接矩阵 | 邻接表 | 十字链表 | 邻接多重表 | |
---|---|---|---|---|
空间复杂度 | 0(${ | V | }^2$) | 无向图O(|V|+2|E|) 有向图O(|V|+|E|) |
找相邻边 | 遍历对应行或列时间复杂度为O(|V|) | 找有向图的入边必须遍历整个邻接表 | 很方便 | 很方便 |
删除边或顶点 | 删除边很方便,删除顶点需要大量移动数据 | 无向图中删除边或顶点都不方便 | 很方便 | 很方便 |
适用于 | 稠密图 | 稀疏图和其他 | 只能存有向图 | 只能存无向图 |
表示方式 | 唯一 | 不唯一 | 不唯一 | 不唯一 |
三. 图的基本操作
- Adjacent(G,x,y): 判断图G是否存在边<x,y>或(x,y)。 //邻接矩阵更好
- Neighbors(G,x):列出图G中与结点x邻接的边。 //邻接表更优秀
- InsertVertex(G,x): 在图G中插入顶点x。 //时间复杂度O(1)
- DeleteVertex(G,x):从图G中删除顶点x。 //邻接矩阵时间复杂度O(|V|),邻接表时间复杂度删出边O(1)~O(|V|)删入边O(|E|)
- AddEdge(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。
四. 图的广度优先遍历(BFS)
1.广度优先遍历(辅助队列)
参考树的层序遍历
bool visited [MAX_VERTEX_NUM]; //访问标记数组
void BFSTraverse(Graph 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
}
//广度优先遍历
void BFS(Graph G,int v){ //从顶点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(G,v,W))//检测V所有邻接点
if(!visited [w]){ //w为v的尚未访问的邻接顶点
visit(w); //访问顶点w
visited[w]=TRUE; //对w做已访问标记
EnQueue(Q,w); //顶点w入队列
}//if
}//while
}
2. 时间复杂度分析
邻接矩阵存储的图:
访问|V|个顶点需要O(|V|)的时间
查找每个顶点的邻接点都需要O(|V|)的时间,而总共有|V|个顶点
时间复杂度=O(|V|)
邻接表存储的图:
访问V个顶点需要O|V|的时间
查找各个顶点的邻接点共需要O(|E|)的时间,
时间复杂度=O(|V|+|E|)
3.广度优先生成树和广度优先生成森林
遍历非连通图可以得到广度优先生成森林。
五. 图的深度优先遍历(DFS)
1.深度优先遍历(辅助栈)
参考树的先根遍历
以上代码如果是非连通图,无法遍历完所有结点。所以修改:
bool visited[MAX VERTEX NUM]; //访问标记数组
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,v);W>=0;w=NextNeighor(G,V,W))
if(!visited [w]){ //w为u的尚未访问的邻接顶点
DFS(G,w);
}//if
}
2. 复杂度分析
空间复杂度:来自函数调用栈,最坏情况,递归深度为O(|V|)。最好情况:O(1)。
时间复杂度=访问各结点所需时间+探索各条边所需时间。
邻接矩阵存储的图:
访问|V|个顶点需要O(|V|)的时间
查找每个顶点的邻接点都需要O(|V|)的时间,而总共有|V|个顶点
时间复杂度=O(
∣
V
∣
2
|V|^2
∣V∣2)
邻接表存的图:
访问|V|个顶点需要O(|V|)的时间
查找各个顶点的邻接点共需要O(|E|)的时间,
时间复杂度=O(|V|+|E|)
3. 深度优先生成树和深度优先生成森林
无向图:DFS/BFS函数调用次数=连通分量数
有向图:若从其十点到其他顶点都有路径,则只需调用1次DFS/BFS函数;对于强连通图,从任一顶点出发都只需要调用1次DFS/BFS函数。
六. 最小生成树
对于一个带权连通无向图G=仍,E),生成树不同,每棵树的权(即树中所有边上的权值
之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spanning-Tree,MST)
1.Prim算法(普里姆)
从某一个顶点开始构建生成树;
每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
时间复杂度:O( ∣ V ∣ 2 |V|^2 ∣V∣2)适合用于边稠密图
2. Krusakl算法(克鲁斯卡尔)
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有结点都连通。
时间复杂度:O( ∣ E ∣ l o g 2 ∣ E ∣ |E|log_2|E| ∣E∣log2∣E∣)适合用于边稀疏图。
七.最短路径问题
1.BFS算法(无权图)
//求顶点u到其他顶点的最短路径
void BFS_MIN_Distance(){
//d[i]表示从u到i结点的最短路径
int i;
for(i=0;i<G.vexnum;++i){
d[i]=INFINITY;//初始化路径长度
path[i]=-1; //最短路径从哪个顶点过来
}
d[u]=0;
visit[u]=TRUE;
EnQueue(Q,u);
while(!isEmpty(Q)){ //BFC算法主过程
DeQueue(Q,u); //队头元素出队
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w))
if(!visited(w)){
d[w]=d[u]+1; //w为u尚未访问的邻接顶点
path[w]=u; //最短路径应从u到w
visited[w]=TRUE; //设已访问标记
EnQueue(Q,w); //顶点w入队
}
}
}
2.Dijkstra算法(带权图,无权图)
不合适用于有负权值的带权图。
3. Floyd算法(带权图,无权图)
可以解决负权值图
Floyd算法:求出每一对顶点之间的最短路径
使用动态规划思想,将问题的求解分为多个阶段
对于n个顶点的图G,求任意一对顶点Vi一>Vj之间的最短路径可分为如下几个阶段:
#初始:不允许在其他顶点中转,最短路径是?
0:若允许在V0中转,最短路径是?
#1:若允许在V0、V1中转,最短路径是?
#2:若允许在V0、V1、V2中转,最短路径是?
#n-1:若允许在V0、V1、V2…Vn-1中转,最短路径是?
//Floyd核心
void Floyd(){
//.....
for(int k=0;k<n;k++){
for(int i=0;i<n;i++){
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; //中转点
}
}
}
}
BFS算法 | Dijkstra算法 | Floyd算法 | |
---|---|---|---|
无权图 | √ | √ | √ |
带权图 | × | √ | √ |
带负权值的图 | × | × | √ |
带负权回路的图 | × | × | × |
时间复杂度 | $O( | V | ^2)或O( |
通常用于 | 求无权图的单源最短路径 | 求带权图的单源最短路径 | 求带权图中各顶点的最短路径 |
八.有向无环图(DAG图)描述表达式
九.拓扑排序
AOV网
AOV网(Activity On Vertex NetWork,用顶点表示活动的网):
用DAG图(有向无环图)表示一个工程。J顶点表示活动,有向边<
V
i
,
V
j
V_i,V_j
Vi,Vj>表示活动
V
i
V_i
Vi必须先于活动
V
j
V_j
Vj进行
1.拓扑排序
拓扑排序的实现:
①从AOV网中选择一个没有前驱(入度为0)的顶点并输出。
②从网中删除该顶点和所有以它为起点的有向边。
③重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
//邻接表图拓扑排序
bool TopologicalSort(Graph G){
InitStack(S); //初始化栈,存储入度为0的顶点
for(inr 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,则入栈
}
}
if(count<G.vexnum)
return false; //排序失败,有向图有回路
else
return true; //拓扑排序成功
}
时间复杂度:O(|V|+|E|)
若采用邻接矩阵,则需O(| V 2 V^2 V2|)
2.逆拓扑排序
对一个AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序:
①从AOV网中选择一个没有后继(出度为0)的顶点并输出。
②从网中删除该顶点和所有以它为终点的有向边。
③重复①和②直到当前的AOV网为空。
考量出度
//逆拓扑排序的实现(DFS算法)
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
visited[v]=TRUE; //设已访问标记
for(w=FirstNeighbor(G,v);W>=0;w=NextNeighor(G,V,W))
if(!visited[w]){ //w为u的尚未访问的邻接顶点
DFS(G,w);
}//if
else{ //w的邻接顶点已访问,但仍存在。存在回路
return;
}
print(v); //输出顶点
}
回路判断:w的邻接顶点已访问,但仍存在。即有环
十. 关键路径
AOE网
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)
AOE网具有以下两个性质:
①只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
②只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
另外,有些活动是可以并行进行的。
关键路径
从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为
关键路径,而把关键路径上的活动称为关键活动
完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个
工程的完成时间就会延长
①求所有事件的最早发生时间ve()
按拓扑排序序列,依次求各个顶点的ve(k):
ve(源点)=0
ve(k)=Max{ve(j)+Weight(
v
j
,
v
k
v_j,v_k
vj,vk)},
v
j
v_j
vj为
v
k
v_k
vk的任意前驱
②求所有事件的最迟发生时间()
按逆拓扑排序序列,依次求各个顶点的vl(k):
vl(汇点)=ve(汇点)
vl(k)=Min{vl(j)-Weight(
v
k
,
v
j
v_k,v_j
vk,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
j
v_k,v_j
vk,vj)
⑤求所有活动的时间余量d()
d(i)=l(i)-e(i)
若关键活动耗时增加,则整个工程的工期将增长
缩短关键活动的时间,可以缩短整个工程的工期
当缩短到一定程度时,关键活动可能会变成非关键活动
可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工
期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。