一、基本概念
- 图:G(V,E),V(G)是顶点集合,一定非空;E(G)是边集,可以为空。图没有空图。
- 有向图:边又叫弧,是顶点的有序对<v,w>,v头w尾(尾巴带箭头)
- 无向图:(v,w)=(w,v)
- 简单图:①不存在重复边②不存在顶点到自身的边
- 多重图:图中某两个顶点之间的边数大于1条,又允许顶点通过一条边和自身关联。仅讨论简单图。
- 度:图中顶点v关联的边的数目定义为v的度,记为TD(v)。无向图的全部顶点的度之和等于边数的2倍;有向图入度ID(v)——被指向,出度OD(v)——指出去,度(TD)=出度+入度。有向图 全部顶点的入度之=与出度之和=边数。
- 路径长度:路径上边的数目
- 路径:两个顶点之间的顶点序列
- 回路(环):序列中第一个顶点和最后一个顶点相同的路径。若一个图有n个顶点,且有大于n-1条边,则此图一定有环
- 简单路径:序列中顶点不重复出现的路径
- 简单回路:除了第一个和最后一个顶点之外,其余顶点不重复出现的回路
- 距离:从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v不存在路径,则记该距离为无穷∞
- 子图:设图G=(V,E) 和图 G′=(V′,E′),且V′⊆V, E′⊆E,则称 G′ 为 G 的子图
- 生成子图:若有满足V(G')=V(G)的子图G',则G'为G的生成子图
- 并非V和E的任意子集都能构成G的子图,因为这样的子集可能不是图
- 连通:在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。
- 连通图:若图G中任意两个顶点都是连通的,则称图 G为连通图,否则称为非连通图。
- 连通分量:无向图中的极大连通子图称为连通分量
- 设一个图有n个顶点,若边数小于n-1,则此图必是非连通图
- 连通图最少有n-1非连通图最多可以有C(n-1,2)条边
- 强连通:在有向图中,若有一对顶点v和w,从v到w和从w到v之间都有路径,则称这两个顶点强连通
- 强连通图:有向图中,若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。否则,其各个极大强连通子图称作它的强连通分量(从任一点出发均能访问所有点)
- 强连通图最少需要有n条边
- 生成树:假设一个连通图有 n 个顶点和 e 条边,其中 n-1条边和 n 个顶点构成一个极小连通子图,称该极小连通子图为此连通图的生成树(生成树不唯一)。若砍去生成树的一条边,就会变成非连通图,若加上一条边就形成一个回路
- 生成森林:对非连通图,由各个连通分量的生成树的集合为此非连通图的生成森林
- 极大连通子图要求子图必须连通,且包含尽可能多的顶点和边;极小连通子图既要保持子图连通又要使得边数最少
- 网:边带权的无向图——无向网;弧带权的有向图——有向网
- 带权路径长度:路径上所有边的权值之和
- 完全图:n 个顶点的含有 n(n-1)/2 条边的无向图称作完全图,在完全图中任意两个顶点之间都存在边; n 个顶点的含有 e=n(n-1) 条弧的有向图称作有向完全图,任意两个顶点之间都存在方向相反的两条弧
- 边很少的图,称作稀疏图,否则称作稠密图,相对而言
- 有向树:如果一个有向图恰有1个顶点的入度为0,其余顶点入度均为1,则称该图为一棵有向树
二、图的存储
(一)邻接矩阵法
1.无向图
- A[i][j]表示顶点vi 和vj 之间是否存在连边,存在为1,不存在为0
- 无向图邻接矩阵是对称阵(唯一),可以利用对称阵的压缩存储方法存储
- 一维数组存放顶点信息(数据元素的值)
- 顶点vi的度:第i行或第i列的非零(或非∞)元素个数
2.有向图
- A[i][j]表示是否存在顶点vi 流向顶点vj 的弧(行->列),有为1,没有为0,不一定为对称阵
- 顶点vi的入度:第i列的非零个数
- 顶点vi的出度:第i行的非零个数
- 顶点vi的度=出+入
3.无向网
邻接矩阵A[i][j],存在连边为wij(在顶点vi 和vj 的连边上的权值),不存在为0或无穷
4.有向网
wij表示在顶点vi 流向顶点vj 的弧上的权值,不存在边为0或无穷
- 邻接矩阵法求顶点的度/出度/入度,时间复杂度O(|V|)
- 稠密图(边多)适合用邻接矩阵。稀疏图会浪费空间
- 设图 G 的邻接矩阵为 A,A^n的元素 A^n[i][j]等于由顶点i到顶点j的长度为n的路径的数目
- 邻接矩阵是唯一的
//空间复杂度O(n^2),n为顶点数
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct {
VertexType Vex[MaxVertexNum]; //顶点表(数据元素值)
EdgeType Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表。1表示存在边,0不是
int vexnum, arcnum; //图的当前顶点数和弧数
} MGraph;
(二)邻接表法
- 顺序存储+链式存储
- 对图 G中的每个顶点 vi建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边(对于有向图则是以顶点vi为尾的弧),这个单链表就称为顶点v的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储,称为顶点表,所以在邻接表中存在两种结点:顶点表结点和边表结点
- 邻接表的表示方法不唯一,边的顺序任意 - 无向图求某个顶点的度:计算其邻接表中的边表结点个数
- 有向图求某个顶点的出度:计算其邻接表中的边表结点个数
- 有向图求某个顶点x的入度:需遍历全部的邻接表,统计邻接点(adjvex)域为x的边表结点个数。
- 图的邻接表表示并不唯一,因为在每个顶点对应的边表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序
#define MaxVertexNum 100 //图中顶点数目的最大值
typedef struct ArcNode { //边表结点
int adjvex; //该弧所指向的顶点的位置
struct ArcNode *next; //指向下一条弧的指针
//InfoType info; //网的边权值
} ArcNode;
typedef struct VNode { //顶点表结点
VertexType data; //顶点信息
ArcNode *first; //指向第一条依附该结点的弧的指针
} VNode, AdjList[MaxVertexNum];
typedef struct { //图的邻接表存储结构定义
AdjList vertices; //邻接表
int vexnum, arcnum; //图的顶点数和弧数
} ALGraph;
在邻接表中,给定一个顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为 0(n)。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低。
(三)十字链表法
- 只能存储有向图,链式
- 有向图的每条弧用一个结点(弧结点)来表示,每个顶点也用一个结点(顶点结点)来表示
- 弧头是弧的终点,有箭头
- 在十字链表中,既容易找到vi为尾的弧,也容易找到vi为头的弧,因而容易求得顶点的出度和入度。图的十字链表表示是不唯一的,但一个十字链表表示唯一确定一个图。
- 空间复杂度O(|V|+|E|)
- 顺着绿色可以找到该顶点的所有入边,顺着绿色可以找到该结点的所有出边
#define MaxVertexNum 100 //图中顶点数目的最大值
typedef struct ArcNode{ //边表结点
int tailvex, headvex; //该弧的头尾结点
struct ArcNode2 *hlink, *tlink; //分别指向弧头相同和 弧尾相同的结点
//InfoType info; //相关信息指针
} ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data; //顶点信息
ArcNode *firstin, *firstout; //指向第一条入弧和出弧
} VNode;
typedef struct {
VNode xlist[MaxVertexNum]; //邻接表
int vexnum, arcnum; //图的顶点数和弧数
} GLGraph;
(四)邻接多重表存储表
- 无向图,链式存储
- 在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,因为每条边依附于两个顶点,所以每个边结点同时链接在两个链表中。对无向图而言,其邻接多重表和邻接表的差别仅在于,同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。
#define MaxVertexNum 100 //图中顶点数目的最大值
typedef struct ArcNode { //边表结点
bool mark; //访问标记
int ivex, jvex; //分别指向该弧的两个结点
struct ArcNode3 *ilink, *jlink; //分别指向两个顶点的 下一条边
//InfoType info; //相关信息指针
} ArcNode;
typedef struct VNode { //顶点表结点
VertexType data; //顶点信息
ArcNode *firstedge; //指向第一条依附该顶点的边
}VNode;
typedef struct {
VNode adjmulist[MaxVertexNum]; //邻接表
int vexnum, arcnum; //图的顶点数和弧数
} AMLGraph;
三、图的基本操作
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中添加该边。
Removegdge(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.概念
- 图中的某个未访问顶点v0出发,并在访问此顶点之后依次访问v0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的未访问过的邻接点,直至图中所有和v0有路径相通的顶点都被访问到。(按照与出发点v0路径长度递增的顺序访问顶点,先访问长度为1的,再2,3……)
- 分层,无后退,非递归
- 邻接矩阵:遍历序列唯一;邻接表:遍历序列不唯一
- 空间复杂度:最坏O(|V|)
- 时间复杂度:邻接矩阵O(|V|^2),邻接表O(|V|+|E|)
bool visited[MAX_VERTEX_NUM]; //访问标记数组
void BFSTraverse(Graph G){
for(i=0;i<G.vexnum,++i)
visited[i]=false; //访问标记数组初始化微博false表示未被访问/’
InitQueue(Q); //初始化辅助队列Q
for (int i = 0; i < G.vexnum; ++i)//从0号顶点遍历
if(!visited[i]) //对每个连通分量调用一次BFS
BFS(G,i); //Vi未访问过,从Vi开始BFS
//调用BFS的次数=无向图里连通分量的数量
}
void BFS(Graph G,int v){
visit(v); //访问初始顶点v
visited[v]=true; //对v做已访问标记
EnQueue(Q,v); //顶点v入队列
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
}//for
}//while
}
//如果用邻接表,则改成:
for(p=G.vertices[V].firstarc;p;p->nextarc)//检测v的所有邻接点
//如果用邻接矩阵,改成
for(w=0;w<G.vexnum;w++) {
if(visited[w]==FALSE && G.edge[v][w]==1){...}
}
2.广度优先生成树
- 广度遍历过程中,访问时经过的顶点和边构成的子图
- 邻接矩阵:唯一;邻接表:不唯一
- 非连通图进行广度遍历,得到广度优先生成森林
(二)深度优先搜索BFS
1.概念
- 类似于树的先序遍历
- 递归思想
- 首先访问图中某一起始顶点v,由v出发,访问与v邻接且未被访问的任意一个顶点 w,再访问与 w邻接且未被访问的任意一个顶点w1...重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
- 空间复杂度最坏O(|V|),最好O(1)
- 时间复杂度O(|V|^2)
- 邻接矩阵:唯一;邻接表:不唯一
bool visited[MAX_VERTEX_NUM]; //访问标记数组
void DFSTraverse(Graph 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);
//调用BFS的次数=无向图里连通分量的数量
}
void DFS(Graph G,int 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
}
}
//如果用邻接表,则改成:
for(p=G.vertices[V].firstarc;p;p->nextarc)//检测v的所有邻接点
//如果用邻接矩阵,改成
for(w=0;w<G.vexnum;w++) {
if(visited[w]==FALSE && G.edge[v][w]==1){DFS(G,w);}
}
2.深度优先生成树
- 访问时经过的顶点和边构成的子图
- 非连通图进行深度遍历,得到深度优先生成森林
- 邻接矩阵:唯一;邻接表:不唯一
图的遍历算法可以判断图的连通性:
- 无向图:若无向图是连通的,则从任意一个结点出发,仅需一次遍历就能够访问图中的所有顶点;若无向图是非连通的,则从某一个顶点出发,一次遍历只能访问到该顶点所在连通分量的所有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问。
- 有向图:若从初始顶点到图中的每个顶点都有路径,则能够访问到图中的所有顶点,只需调用1次BFS/DFS函数。否则不能访问到所有顶点。强连通图从任一结点出发都只需调用一次函数。
五、图的应用
(一)最小生成树
- 生成树:一个连通图的生成树包含图的所有顶点,并且只含尽可能少的边。对于生成树来说,若砍去一条边,则会使生成树变成非连通图;若增加一条边,则会在图中形成一条回路。
- 对于一个带权连通无向图 G,生成树不同,每棵树的权(树中所有边的权值之和)也可能不同。权值之和最小的那棵生成树称为G的最小生成树
- 1)若图 G中存在权值相同的边,则G的最小生成树可能不唯一,即最小生成树的树形不唯一。当图 G中的各边权值互不相等时,G的最小生成树是唯一的;若无向连通图G的边数比顶点数少 1,即 G本身是一棵树时,则G的最小生成树就是它本身。
- 2)虽然最小生成树不唯一,但其对应的边的权值之和总是唯一的,而且是最小的。
- 3)最小生成树的边数为顶点数减1。
- 最小生成树中所有边的权值之和最小,但不能保证任意两个顶点之间的路径是最短路径。
- 每次加入一条边,且不会产生回路
GENERIC_MST(G){
T=NULL;
while T 未形成一颗生成树;
do 找到一条最小代价边(u,v)并且加入T后不会产生回 路;
T=T (u,v); }
1.Prim算法
- 初始时从图中任取一顶点加入树 T,此时树中只含有一个顶点,之后选择一个与当前T中顶点集合距离最近的顶点,并将该顶点和相应的边加入 T,每次操作后 T中的顶点数和边数都增 1。以此类推,直至图中所有的顶点都并入 T,得到的T就是最小生成树。此时T中必然有n-1条边。
- 时间复杂度O(|V|^2)
- 适合边稠密的图
void Prim(G,T){
T= ; //初始化空树
U={w}; //添加任一顶点w
while((V-U)!= ){ //若树中不含全部顶点
设(u,v)是使u U与v (V-U),且权值最小的边;
T=T {(u,v)}; //边归入树
U=U {v}; //顶点归入树
}
}
2.Kruskal算法
- 假设 G=(V,E)是连通图,其最小生成树 T=(U,E)。初始化:U=V,Et=空集。即每个顶点构成一棵独立的树,T此时是一个仅含|V|个顶点的森林,循环(重复直至T是一棵树):按 G的边的权值递增顺序依次从 E-Et中选择一条边,若这条边加入T后不构成回路,则将其加入Et,否则舍弃,直到Et中含有n-1条边。
- 适合边稀疏、顶点多的图
- 时间复杂度O(|E|log(2,|E|))
void Kruskal(V,T){
T=V; //初始化树T,仅含顶点
numS=n; //连通分量数
while(numS>1){ //如果连通分量数大于1 从E中取出权值最小的边(v,u);
if(v和u属于T中不同的连通分量){
T=T {(v,u)}; //将此边加入生成数中
numS--; //连通分量数减1
}
}
}
(二)单源最短路径
1.BFS算法(无权图)
求某个顶点到其余所有顶点的每个最短路径
void BFS_MIN_Distance(Graph G,int u){
//d[i]表示从u到i结点的最短路径
for (int i = 0; i < G.vexnum; ++i) {
d[i]=∞ ; //初始化路径长度 ,代表u到i没有通路
path[i]=-1;//最短路径从哪个顶点过来
}
visited[u]=true;
d[u]=0;
EnQueue(Q,u);
while(!IsEmpty(Q)){ //BFS算法主过程
DeQueue(Q,u); //队头元素u出队
for (w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w)) {
if(!visited[w]){ //w为u的尚未访问的邻接顶点
visited[w]=true; //设已访问标记
d[w]=d[u]+1; //路径长度加1
path[w]=u;//最短路径应从u到w
EnQueue(Q,w); //顶点w入队
}//if
}
}//while
}
2.Dijksstra算法(带权图、无权图)
- 有向图、无向图均可
- 只考手算!不考代码
- 时间复杂度O(|V|^2)
- 不适用于有负权值边的带权图
(二)每对顶点间的最短路径(带权图、无权图)
1.Floyed算法
- 允许图中有带负权值的边,但不允许包含总权值为负的回路
- 适用于带权无向图
- 时间复杂度O(|V|^3)
总结
六、有向无环图(DAG图)
- 有向无环图:一个有向图中不存在环
- 在表达式的有向无环图表示中,不可能出现重复的操作数顶点
- 解题方法,需要看视频:
(一)AOV网
- AOV网:DAG图表示工程,每个活动用一个顶点表示,活动之间的先后制约关系用弧表示
- 任何活动不能以自己作为自己的前驱和后继
- 拓扑排序:①每个顶点出现且只出现一次。②若顶点4在序列中排在顶点B的前面,则在图中不存在从B到A的路径。
- 每个 AOV网都有一个或多个拓扑排序序列。
(二)拓扑排序
- 排序方法:1找AOV网中入度为0的顶点放到拓扑排序序列中,2删除该顶点以及以它为起点的有向边,3重复步骤1、2, 直到AOV网为空或不再存在无前驱的顶点(第二种情况说明有回路)
若图中还有剩余顶点未被删除,说明图中有回路,不是一个AOV网
- 拓扑排序结果可能不唯一。若每次输出顶点时,检测入度为0的顶点每次都唯一,则排序唯一。
- 若图的邻接矩阵是三角形,则存在拓扑结构,反之不一定成立
bool TopologicalSort(Graph G){
//如果G存在拓扑序列,返回true;否则返回false,这时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->nextarc){
//将所有i指向的顶点的入度减1,并且将入度减为0的顶 点压入栈S
v=p->adjvex;
if(!(--indegree[v]))
Push(S,v); //入度为0,则入栈
}//for
}//while
if(count < G.vexnum)
return false; //排序失败,有向图中有回路
else
return true; //拓扑排序成功
时间复杂度:以上代码用的是邻接表-O(|V|+|E|);若用邻接矩阵-O(|V|^2)
(三)逆拓扑排序
每次删除出度为0的结点
1.代码待写见视频
2.DFS
void DFSTraverse(Graph G){
for(v=0;v<G.vexnum;++V)//对图G进行深度优先遍历
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出发,深度优先遍历图
Gvisited[v]=TRUE;//设已访问标记
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighor(G,v,w))
if(!visited[w]){//w为u的尚未访问的邻接顶点
DFS(G,w);
}//if
print(v);//输出顶点
}
(四)AOE网
- 顶点表示事件,弧表示活动,弧上的权值表示完成一项活动需要的时间
- 有向无环图,存在唯一开始顶点(源点,入度为0)和唯一完成顶点(汇点,出度为0)
- 只有在某顶点代表的事件发生后,从该顶点发出去的弧所代表的各项活动才能开始;只有进入某顶点的各条弧所代表的活动都已经结束,该顶点所代表的事件才能发生。
- AOE网中的某些活动可以并行进行
- 关键路径:从开始顶点到完成顶点的路径中,具有最大路径长度的路径。关键路径上所有活动都叫做关键活动
- 关键路径的长度是完成工程的最短时间
- 若关键活动不能按时完成,则工程被延长
- 活动的最早开始时间Ve(k):v1到vk的最长路径长度
- 活动的最迟开始时间()--它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。
活动“;的时间余量d(i)=/(0)-e(,表示在不增加完成整个工程所需总时间的情况下,活动a可以拖延的时间若一个活动的时间余量为零,则说明该活动必须要如期完成,d(0)=0即I(0)=e(0)的活动a,是关键活动
计算关键路径:
- 令Ve(源点)=0,计算事件Vk的最早发生时间Ve(k)
- 令Vl(汇点)=Ve(源点),按逆拓扑有序求其余顶点的最迟发生时间Vl()。
- 根据各顶点的Ve()值求所有弧的最早开始时间 e()。
- 根据各顶点的Vl()值求所有弧的最迟开始时间 l()。
- 求 AOE 网中所有活动的差额d(),找出所有d()=0的活动构成关键路径。
**具体算法待写 **
- 可以通过加快关键活动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定的程度,该关键活动就可能会变成非关键活动。
- 关键路径不唯一,且对于有几条关键路径的网,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,多条路径上都加快,或者加快那些包括在所有关键路径上的关键活动才能缩短工期。