图
一、认识图
- 一些例子
- 一个组织内部、不同部门员工之间的电子邮件网路
- 互联网,全球WWW网页构成的图
- 社会网络研究进展图
- 一个跆拳道俱乐部内部成员的社交关系
- facebook上宣称的好友关系和实质上长维护着的关系
- 网页间相互连接的关系图
- 图的基本术语
- 图(又称网络):由节点和节点间的连边构成的对象称为“图”
- 节点/结点(node),又称顶点(vetex)
- 边(edge),又称链接(link),联系(tie),弧(arc)
- 本介绍中网络指边带权的图,弧指有向边
- 图的ADT
ADT Graph{ 数据对象:具有相同特性的数据元素的集合,称为顶点集$V$ 数据关系:$R=\{<v,w>|v,w\in V&&P(v,w)\}$ 基本操作: Create_Graph(); GetVex(G,v); ... DFStraverse(G,v); BFStraverse(G,v); }
- 图的类型
- 无向图
- 有向图
- 完全图
- 边相关的概念
- 邻接点:用边链接的两个顶点互为邻接点,常指无向图
- 边权值:描述边的属性或特定,若边无权可假设为固定常数
- 顶点的度
- 度:顶点链接的边的条数
- 有向图的度为出度和入度二者之和
- 路径相关
- 在图G ( V , E ) (V,E) (V,E)中, 若从顶点 v i v_i vi出发, 沿E中的一些边经过一些 V V V中的顶点 v 1 , v 2 , ⋯ , v m v_1,v_2,\cdots,v_m v1,v2,⋯,vm,到达顶点 v j v_j vj, 则称顶点序列 ( v i , v 1 , v 2 , ⋯ , v m , v j ) (v_i,v_1,v_2,\cdots,v_m,v_j) (vi,v1,v2,⋯,vm,vj)为从顶点 v i v_i vi到顶点 v j v_j vj的路径
- 路径长度:非带权图为边的条数,带权图为权之和
- 顶点互不重复的路径称为简单路径
- 第一个顶点与最后一顶点重合的简单路径为简单回路
- 子图相关
- 在无向图中, 若从顶点 v 1 v_1 v1到顶点 v 2 v_2 v2有路径, 则称顶点 v 1 v_1 v1与 v 2 v_2 v2是连通的。如果图中任意一对顶点都是连通的, 则称此图是连通图。非连通图的极大连通子图叫做连通分量
- 在有向图中, 若对于每一对顶点 v i v_i vi和 v j v_j vj, 都存在一条从 v i v_i vi到 v j v_j vj和从 v j v_j vj到 v i v_i vi的路径, 则称此图是强连通图。非强连通图的极大强连通子图叫做强连通分量
- 树相关的概念
- 生成树:有 n n n个顶点和 e e e条边的连通图,其 n − 1 n − 1 n−1条边和 n n n个顶点构成一个极小连通子图为此连通图的生成树
- 一棵生成树,任意添加一条边,都会形成环
- 删除生成树的任何一条边,则变成非连通图
- 有向树:恰有一个顶点入度为0,其余顶点入度均为1的有向图
- 生成森林:一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,且仅包含足以构成若干棵不相交的有向树的弧
二、图的存储结构
- 邻接矩阵/数组表示法
- 一个记录各个顶点信息的定点表+一个表示各顶点之间关系的邻接矩阵。更准确地说是“数组表示法”,一个一维顶点数组+一个二维边数组
- 无向图
- 邻接矩阵是对称的
- 统计第 i i i行(列)1的个数可得顶点 i i i的度
- 有向图
- 邻接矩阵可能是不对称的
- 统计第 i i i行1的个数可得顶点 i i i的出度,统计第 i i i列1的个数可得顶点 i i i的入度
- 带权图:邻接矩阵上无边写 ∞ \infty ∞,有边写权值,对角线为0
#define INFINITY INT_MAX //最大值 #define MAX_VERTEX_NUM 20 //最大顶点个数 typedef enum{DG,DN,UDG,UDN} GraphKind; //图的类型 typedef struct ArcCell{ WType w; // WType为边权值类型 InfoType *info; //该弧相关信息指针 }ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; typedef struct { VexType vexs[MAX_VERTEX_NUM]; //顶点向量 AdjMatrix adj; //邻接矩阵 int vexnum,arcnum; //顶点数和弧数 GraphKind kind; //图的种类标志 }MGraph;
- 邻接表(广义表的链式存储结构)
- 顶点结构:结构体数组,结构体包括数据域data和指向第一个依附该顶点的弧节点的指针firstarc
- 弧节点结构:每个顶点依附的弧构成一个单链表;节点结构包括:弧的另一个顶点adjvex,指向下一个弧节点的指针nextarc和弧的信息info
- 有向图
- 头节点=顶点节点的数目,弧节点数目=弧节点数目
- 邻接表:头节点代表的顶点是箭头的起点,弧节点中的顶点是箭头的指向
- 邻接表求顶点的出度,就是求顶点后接的链表的长度
- 逆邻接表求顶点的入度,就是求顶点后接的链表的长度
- 无向图
- 头节点=顶点节点的数目,弧节点数目=2倍边数目
- 同一个顶点发出的边链接在同一个边链表中
- 每条边被存在其两个顶点为头节点的边链表中
- 邻接表求顶点度,就是求顶点后接的链表的长度
- 在邻接表上易找出任一顶点的第一个邻接点和下一个邻接点
#define MAX_VERTEX_NUM 20 typedef struct ArcNode { int adjvex; // 该弧所指向的顶点的位置 struct ArcNode *nextarc;// 指向下一条弧指针 InfoType *info; // 该弧相关信息的指针 } ArcNode; typedef struct VNode { VexType data; // 顶点信息 ArcNode *firstarc; // 指向第一条依附该顶点的弧 } VNode, AdjList[MAX_VERTEX_NUM]; typedef struct { AdjList vertices; int vexnum,arcnum;//图的当前顶点数和弧数 int kind;//图的种类标志 }ALGraph;
- 有向图的十字链表:融合邻接表和逆邻接表
- data域:存储和顶点相关信息
- 指针域firstin:指向以该节点为弧头的第一条弧所对应的弧节点
- 指针域firstout:指向以该节点为弧尾的第一条弧所对应的弧节点
- 尾域tailvex:表示弧尾顶点在图中的位置
- 头域headvex:表示弧头顶点在图中的位置
- 指针域hlink:指向弧头相同的下一条弧
- 指针域tlink:指向弧尾相同的下一条弧
- Info域:指向该弧的相关信息
- 顶点节点(data,firstin,firstout)
- 弧节点(tailvex,headvex,hlink,tlink,info)
#define INFINITY MAX_VAL /*最大值∞*/ #define MAX_VEX 30 //最大顶点数 typedef struct ArcNode{ int tailvex, headvex; //尾结点和头结点在图中的位置 InfoType *info; //与弧相关的信息, 如权值 struct ArcNode *hlink, *tlink; }ArcNode; //弧结点类型定义 typedef struct VexNode{ VexType data; // 顶点信息 ArcNode *firstin, *firstout; }VexNode; //顶点结点类型定义 typedef struct{ int vexnum; VexNode xlist[MAX_VEX]; }OLGraph; //图的类型定义
- 无向图的邻接多重链表
- 边节点(mark,ivex,ilink,jvex,jlink,info)
- 标志域mark:用以表示该边是否被访问过
- ivex和jvex域:分别保存该边所依附的两个顶点在图中的位置
- info域:保存该边的相关信息
- 指针域ilink:指向下一条依附于顶点ivex的边
- 指针域jlink:指向下一条依附于顶点jvex的边
- 顶点节点(data,firstedge)
- data域:存储和顶点相关的信息
- 指针域firstedge:指向依附于该顶点的第一条边所对应的表节点
#define INFINITY MAX_VAL //最大值∞ #define MAX_VEX 30 //最大顶点数 typedef emnu {unvisited, visited} Visitting; typedef struct EdgeNode{ Visitting mark; //访问标记 int ivex, jvex; //该边依附的两个结点在图中的位置 InfoType *info; //与边相关的信息, 如权值 struct EdgeNode *ilink, *jlink; // 分别指向依附于这两个顶点的下一条边 }EdgeNode; //弧边结点类型定义 typedef struct VexNode{ VexType data; //顶点信息 ArcNode *firstedge; //指向依附于该顶点的第一条边 }VexNode; //顶点结点类型定义 typedef struct{ int vexnum; VexNode mullist[MAX_VEX]; }AMGraph;
- 边节点(mark,ivex,ilink,jvex,jlink,info)
三、图上的搜索
- 输入:图 G = ( V , E ) G=(V,E) G=(V,E),初始顶点 v 0 v_0 v0,目标测试函数 G o a l ( s ) Goal(s) Goal(s)判断顶点 s s s是否满足给定条件,满足给定条件返回 t r u e true true否则返回 f a l s e false false
- 输出:从给定初始顶点
v
0
v_0
v0出发沿着边到达目标顶点的路径
- 一条路径或多条路径:最优(最短)路径
- 退化时,不需要路径,只要目标状态
- 典型的不同目标测试函数
G
o
a
l
(
)
Goal()
Goal()
- 找到指定编号的顶点(返回路径,应用案例:迷宫寻路)
- 找到一个或所有红色(指某个特定属性)的顶点(返回目标顶点或路径,应用案例:n皇后或华容道)
- 输出连通图每个顶点的着色信息恰好一次(应用案例:遍历图)
- 图的搜索算法框架
- 搜索过程中图 G G G的节点分为三类:访问过的节点,访问过节点的直接邻接点(搜索的边界),其它未被访问过的节点
Input:图G;初始节点s0; Output: path:代表解的路径 path<--(s0),FRINGE<--φ //初始化,其中FRINGE是保存搜索边界的数组 if(GOAL(s0)==T) return path=(s0); INSERT(s0,FRINGE); while T do if(isEmpty(FRINGE==T)) return failure; //表示无解 s<--REMOVE(FRINGE) if(s未访问过) { visit(s); update path; foreach s的邻接点s'' do { if(GOAL(s'')==T) return (path, s''); INSERT(s'',FRINGE); } end } end end
- 图的搜索的各种不同变型
- 各种搜索算法,不同之处在于对搜索边界中节点的次序确定方法
- 更具体地说:就是 I N S E R T ( s ′ , F R I N G E ) INSERT(s',FRINGE) INSERT(s′,FRINGE)函数地实现方法不同
- 广度优先搜索:每次插入 s ′ s' s′时,放在 F R I N G E FRINGE FRINGE地末尾
- 深度优先搜索:每次插入 s ′ s' s′时,放在 F R I N G E FRINGE FRINGE地开头
- 启发式搜索算法:依据问题地特定,设计一个节点排序地策略或方法,将
s
′
s'
s′插入在
F
R
I
N
G
E
FRINGE
FRINGE地某个特殊位置
- 贪婪/贪心算法:只考虑从当前节点到目标节点地最短路径
- A*算法:考虑从出发点开始,到目标点地最短路径
- 搜素策略:本质上就是决定哪一个节点放在 F R I N G E FRINGE FRINGE开头,不排序,只选择最有“希望地”节点
- 图搜索中的技术问题
- 如果一个节点被多次访问会出现什么问题
- 算法不会停止,构成三角循环
- 防止节点被多次访问
- 节点数目少时:用一个标志数组 b o o l v i s i t e d [ ] bool visited[] boolvisited[]来记录图的每个节点是否被访问过
- 节点数目多时:内存中仅保存已经访问过的节点和FRINGE中的节点
- 如何保存和更新路径
- 设计数据结构保存信息:经过的边数,经过边上权值之和,节点是否已扩展过
- 输出一棵树时,每个子节点指向其双亲结点(扩展该子节点的节点);根的出度为0,树的双亲表示法
- 输出路径时,从找到的目标节点,沿双亲结点上行,直至根节点
- 如果一个节点被多次访问会出现什么问题
- 图搜索的时空复杂性
- 普通图(非指数图):图中所有顶点和边都可以在内存中同时保存
- 搜索目标是必须返回路径,空间需求 O ( n ) O(n) O(n),时间 O ( n + e ) O(n+e) O(n+e)
- 搜索目标仅需返回与路径无关的目标节点:平均查找时间为 ( n + 1 2 ) (\frac{n+1}{2}) (2n+1),空间需求 O ( 1 ) O(1) O(1)
- 指数图(图中顶点最大度为b,m为搜索树叶子的最大深度,d为搜索树种埋藏最浅的目标节点深度)
- 广度,均为 O ( b d ) O(b^d) O(bd)
- 深度,时间 O ( b m ) O(b^m) O(bm),空间 O ( b m ) O(bm) O(bm)
- 回溯算法,深度优先的改进,搜索树的每一层只保留一个邻居节点到 F R I N G E FRINGE FRINGE,空间开销是 O ( m ) O(m) O(m)
- 普通图(非指数图):图中所有顶点和边都可以在内存中同时保存
- 在连通图和非连通图上的图搜索算法
- 连通图
- 初始节点和目标节点位于一个连通片,有解
- 图为便无权或等权时,广度优先搜索一定能找到最短路径,深度优先搜索不一定能找到最短路径
- 非连通图
- 可能不存在从初始节点到目标节点的路径,无解
- 连通图
四、图的遍历:仅适用于非指数图
- 图的遍历:从图中某一顶点出发,沿着图的边,访遍图中所有的顶点,且使每个顶点仅被访问一次,这个过程叫做图的遍历
- 深度优先搜索遍历
- 一直找,找不到回退,回退后能找一直找,找不到回退,直至结束
- 采用邻接矩阵实现算法 O ( n 2 ) O(n^2) O(n2)
- 采用邻接表实现算法 O ( n + e ) O(n+e) O(n+e)
bool visited[MAX]; Status (* VisitFunc)(int v); void DFSTraverse (Graph G,Status (* Visit)(int v)) { VisitFunc =Visit; for ( v= 0; v < G.vexnum; v++ ) visited [v] = FALSE; //访问数组 visited 初始化 for ( int v = 0; v < G.vexnum; v++ ) (!visited[v]) DFS(G, v);//遍历每个连通片 } void DFS (Graph G, int v) { //递归函数 visited[v] = TRUE; //顶点 v 作访问标记 VisitFunc(v); //访问顶点 v for (w=FirstAdjVex(G,v); w>=0; w=NextAdjVex(G,v,w)) if ( !visited[w] ) DFS (G, w); //若顶点 w 未访问过, 递归访问顶点 w }
- 广度优先搜索遍历
- 依次访问起始顶点的每个邻接顶点,直至所有顶点被访问
- 时间复杂度同深度优先搜索遍历
void BFSTraverse(Graph G,Status (* Visit)(int v)) { for (v=0;v<=G.vexnum;++v) visited[v]=FALSE; //初始化数组 InitQueue(Q); //初始化队列 for (v=0;v<=G.vexnum;++v) if (!visited[v]) { visited[v]=TRUE; Visit(v); //标记顶点并访问 EnQueue(Q,v); //入队列 while (!QueueEmpty(Q)) { DeQueue(Q,u); //出队列 //遍访u的所有邻接点 for (w=FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w)) if (!Visited[w]) { Visited[w]=TRUE; Visit(w); EnQueue(Q,W); //入队列 } } } }
- 图遍历的输出
- 生成树和生成森林
- 图的搜索,输出是目标节点或到达目标节点的路径
- 图的遍历,我们定义其输出为图搜索时输出访问的节点以及访问该节点的原因:连通图(生成树),非连通图(生成森林)
- 深度优先搜索
- 不能确定输出的生成树有多少个孩子,因此采用孩子-兄弟链表来存储生成树
- 算法思想:递归
- 从某个顶点 v v v出发,建立一个树节点 T T T
- 以 v v v的一个邻接点为起始点,建立子生成树
- 将子生成树作为 T T T的节点的子树,连接为 T T T节点的左孩子
- 第2,3…个未被访问邻接点为起始点,建立子生成树,并连接为 T T T或前一棵子生成树的右孩子
typedef struct CSNode { ElemType data; struct CSNode *firstchild, *nextsibling; }CSNode; CSNode* DFSTree(ALGraph *G, int v) { CSNode* T,*ptr,*q; ArcNode *p; int w; visisted(v); Visited[v]=TRUE; T=(CSNode*)malloc(sizeof(CSNode)); T->data=G->vertices[v].data; T->firstchild=T->nextsibling=NULL; q=NULL; p=G->vertices[v].firstarc; while(p!=NULL) { w=p->adjvex; if(!Visited[G,w]) { ptr=DFSTree(G,w); if(q==NULL) T->firstchild=ptr; else q->nextsibling=ptr; q=ptr; } p=p->nextarc; } return T; }
- 广度优先搜索
- 不能确定输出的生成树有多少个孩子,因此采用孩子-兄弟链表来存储生成树
typedef struct Queue { int elem[MAX_VEX] int front,rear; }Queue; CSNode* BFSTree(ALGraph* G, int v) { CSNode* T, *ptr, *q, *now; CSNode* vertices[MAX_VERTEX_NUM]={NULL}; ArcNode *p; Queue Q; int w,k; for(int i=0; i<G->vexnum;i++) { vertices[i]=(CSNode*)malloc(sizeof(CSNode)); assert(vertices[i]); vertices[i]->data=G->vertices[i].data; vertices[i]->firstchild=NULL; vertices[i]->nextsibling=NULL; } bool visited[MAX_VERTEX_NUM]={false}; Q->front=Q->rear=0; Visited[v]=TRUE; T=(CSNode*)malloc(sizeof(CSNode)); assert(T); T->data=G->vertices[v].data; T->firstchild=T->nextsibling=NULL; Q->elem[Q.rear++]=v; while(Q.front != Q.rear) { w=Q.elem[Q.front++]; q=NULL; p=G->vertives[w].firstarc; now=vertives[p.adjvex]; while(p!=NULL) { k=p->adjvex; if(!visited[k]) { Visited[k]=TRUE; ptr=vertices[k]; if(q==NULL) now->firstchild=ptr; else q->nextsibling=ptr; q=ptr; Q->elem[Q.rear++]=k; } p=p->nextarc; } } return T; }
- 生成树和生成森林
- 将图的遍历应用于图的连通性
- 求无向图的各个连通分量
- 从任意结点开始,视为初始节点,执行图搜索算法,直至算法结束,得到 V 1 V_1 V1;选择任意未访问过的节点未初始节点,再次执行图搜索算法,得到 V 2 V_2 V2,…,不断重复,直到所有顶点都被访问过
- 有向图的强连通分量
- 对 G G G进行深度优先遍历,生成 G G G的深度优先生成森林 T T T
- 对森林 T T T的顶底按后根遍历顺序进行编号
- 改变 G G G中每一条弧的方向,构造新的有向图 G ′ G' G′
- 按所标的顶点编号,从编号最大的顶点开始对 G ′ G' G′进行深度优先搜索,得到一棵深度优先生成树,从未访问的顶点中选择编号最大的顶点,再进行深度优先搜索,直至所有顶点被访问
- 求无向图的各个连通分量
五、生成树与最小生成树
- 生成树:使用不同的遍历方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树
- 最小生成树:构造准则
- 必须使用且仅使用该网络中的 n − 1 n-1 n−1条边来连接网络中的 n n n个顶点
- 不能使用产生回路的边
- 各边上的权值总和达到最小
- 普里姆(Prim)算法
- 具体算法:见图论-02
- 邻接矩阵作为图/网络的存储表示
typedef struct{ //时间复杂度为 O(n^2), n 是节点数目, 它适用于稠密网 VexType adjvex; WType lowcost; //0:已经加入U;无穷大:无直接连边 } closedge[MAX_VERTEX_NUM]; void MiniSpanTree_PRIM(MGraph G, VexType u) { f=LocateVex(G,u); for (j=0;j<G.vexnum;++j) if (j!=f) closedge[j]={u,G.adj[f][j].w}; closedge[f].lowcost =0; //初始,U={u} for (i=0;i<G.vexnum&&i!=f;++i) { k=minimum(closedge); //非0最小权值的下标 printf(closedge[k].adjvex,G.vexs[k]); closedge[k].lowcost = 0; //k并入U集 for (j=0;j<G.vexnum;++j)//调整辅助数组 if(G.adj[k][j].w<closedge[j].lowcost) closedge[j]={G.vexs[k],G.adj[k][j].w}; }//各边有相同权值时,因选择的随意性,生成树可能不唯一 }//各边的权值不相同时,产生的生成树是唯一的
- 克鲁斯卡尔(Kruskal)算法
- 具体算法:见图论-02
- 时间复杂度 O ( e l o g e ) O(eloge) O(eloge)
六、AOV网络
- 用顶点表示活动的网络(AOV网络)
- 用有向图表示一个工程。在这种有向图中,用顶点表示活动,用有向边 < v i , v j > <v_i,v_j> <vi,vj>表示活动 v i v_i vi必须先于活动 v j v_j vj进行。这种有向图叫做顶点表示活动的AOV网络
- 在AOV网络中不能出现有向回路,即有向环,否则某活动以自己为先决条件
- 检测AOV网络中是否有环
- 拓扑排序:即将各个顶点(活动)排列成一个线性有序的序列,使得AOV网络中所有应存在的前驱和后继关系都能得到满足
- 算法思想
- 输入AOV网络,令 n n n为顶点个数
- 在AOV网络中选一个没有直接前驱的顶点,并输出之
- 从图中删去该顶点,同时删去所有它发出的有向边
- 重复2、3步,直到全部顶点均已输出
void count_indegree(ALGraph *G) { int k; ArcNode *p; for (k=0;k<G->vexnum;k++) G->vertices[k].indegree=0; //顶点入度初始化 for(k=0;k<G->vexnum;k++) { p=G->vertices[k].firstarc; while (p!=NULL) { //顶点入度统计 G->vertices[p->adjvex].indegree++; p=p->nextarc; } } } int Topologic_Sort(ALGraph *G, int topol[]) { //顶点的拓扑序列保存在一维数组topol中 int k, no, vex_no, top=0, count=0, boolean=1; int stack[MAX_VEX]; //用作堆栈 ArcNode *p; count_indegree(G); //统计各顶点的入度 for (k=0;k<G->vexnum;k++) if (G->vertices[k].indegree==0) stack[top++]=k; do { if (top==0) boolean=0; else { no=stack[top--]; //栈顶元素出栈 topol[count++]=no; //记录顶点序列 p=G->vertices[no].firstarc; while (p!=NULL) { //删除以顶点为尾的弧 vex_no=p->adjvex; G->vertices[vex_no].indegree--; if(G->vertices[vex_no].indegree==0) stack[top++]=vex_no; p=p->nextarc; } // end while } //end if }while(boolean==1); if (count<G->vexnum) return(-1); else return(1); }
- 时间复杂度 O ( n + e ) O(n+e) O(n+e)
七、AOE网络
- 与AOV网络相对应的AOE,是边表示活动的有向无环图
- 图中定点表示事件,每个事件表示在其前的所有活动已经完成,其后的活动可以开始,弧表示活动,弧上的权值表示相应活动所需的事件或费用
- 工程完成最短时间:从起点到终点的最长路径长度
- 长度最长的路径称为关键路径,关键路径上的活动称为关键活动。关键活动是影响整个工程的关键
- AOE网络的术语与符号
- 若活动 a i a_i ai是弧 < j , k > <j,k> <j,k>,持续时间是 d u t ( < j , k > ) dut(<j,k>) dut(<j,k>)
- e ( i ) e(i) e(i):表示活动 a i a_i ai的最早开始时间
- l ( i ) l(i) l(i):在不影响进度的前提下,表示活动 a i a_i ai的最晚开始时间;则 l ( i ) − e ( i ) l(i)-e(i) l(i)−e(i)表示活动 a i a_i ai的时间余量,若 l ( i ) − e ( i ) = 0 l(i)-e(i)=0 l(i)−e(i)=0,表示活动 a i a_i ai是关键活动
- v e ( i ) ve(i) ve(i):表示事件 v i v_i vi的最早发生时间,即从起点到顶点 v i v_i vi的最长路径长度
-
v
l
(
i
)
vl(i)
vl(i):表示事件
v
i
v_i
vi的最晚发生时间。则有关系
- e ( i ) = v e ( j ) e(i)=ve(j) e(i)=ve(j)
- l ( i ) = v l ( k ) − d u t ( < j , k > ) l(i)=vl(k)-dut(<j,k>) l(i)=vl(k)−dut(<j,k>)
- 理解
v
e
(
i
)
ve(i)
ve(i)
- v e ( j ) = { 0 j = 0 , 表 示 v j 是 起 点 M a x { v e ( i ) + d u t ( < i , j > ) ∣ < v i , v j > 是 网 中 的 弧 } 其 它 ve(j)=\begin{cases}0 & j=0,表示v_j是起点 \\ Max\{ve(i)+dut(<i,j>)|<v_i,v_j>是网中的弧\} & 其它\end{cases} ve(j)={0Max{ve(i)+dut(<i,j>)∣<vi,vj>是网中的弧}j=0,表示vj是起点其它
- 只有 v j v_j vj的所有前驱事件 v i v_i vi的 v e ( i ) ve(i) ve(i)算出后,才能计算 v e ( j ) ve(j) ve(j)
- 方法:对所有事件进行拓扑排序,再按拓扑排序顺序计算每个事件的最早发生事件
- 理解
v
l
(
j
)
vl(j)
vl(j)
- v l ( j ) = { v e ( n − 1 ) j = n − 1 , 表 示 v j 是 终 点 M i n { v l ( k ) − d u t ( < j , k > ) ∣ < v j , v k > 是 网 中 的 弧 } 其 它 vl(j)=\begin{cases}ve(n-1) & j=n-1,表示v_j是终点 \\ Min\{vl(k)-dut(<j,k>)|<v_j,v_k>是网中的弧\} & 其它\end{cases} vl(j)={ve(n−1)Min{vl(k)−dut(<j,k>)∣<vj,vk>是网中的弧}j=n−1,表示vj是终点其它
- 当 v j v_j vj的所有后继事件 v k v_k vk的最晚发生时间 v l ( k ) vl(k) vl(k)计算出来后,才能计算 v l ( j ) vl(j) vl(j)
- 按拓扑排序的逆顺序,依次计算每个事件的最晚发生时间
- 算法思想:见图论-08
void critical_path(ALGraph *G) { int j,k,m; ArcNode *p; int toppol[MAX_VER],ve[MAX_VER],vl[MAX_VER]; if(Topologic_Sort(G,topol)==-1) printf("\nAOE网络存在回路,错误!\n"); else { for(j=0;j<G->vexnum;j++) ve[j]=0; //时间最早发生时间初始化 for(m=0;m<G->vexnum;m++) { j=topol[m]; //存放了拓扑有序序列 p=G->vertices[j].firstarc; for(;p!=NULL;p=p->nextarc) { k=p->adjvex; if(ve[j]+p->weight>ve[k]) ve[k]=ve[j]+p->weight; }//遍历拓扑序列中第m各顶点的边链表 //更新被指向节点的ve(k),求最大值 }//从拓扑有序序列的第一个开始,依次计算每个事件最早发生时间 for(j=0;j<G->vexnunm;j++) { vl[j]=ve[j]; //事件最晚发生时间初始化 for(m=G->vexnum-1;m>=0;m--) { j=topol[m]; p=G->vertices[j].firstarc; for(;p!=NULL;p=p->nextarc) { k=p->adjvex; if(vl[k]-p->weight<vl[j]) vl[j]=vl[k]-p->weight; }//遍历拓扑序列中第m各顶点的边链表更新被指向节点的vl(k) }//从拓扑有序序列的最后一个开始,依次计算每个事件的vl值 } for(m=0;m<G->vexnum;m++) { p=G->vertices[m].firstarc; for(;p!=NULL;p=p->nextarc) { k=p->adjvex; if((ve[m]+p->weight)==vl[k]) printf("<%d, %d>", m, k); }//检查每个顶点的每条边,是否是关键活动 }//输出所有关键活动 } //end of else }
- 时间复杂度 O ( n + e ) O(n+e) O(n+e)
最短路径
- 从图中某一顶点到达另一顶点的路径可能不止一条,找到一条路径使得沿此路径上各边上的权值总和达到最小
- 边上权值非负情形的单源最短路径问题——Dijkstra算法
- 边上权值为任意值的单源最短路径问题——Bellman和Ford算法
- 所有顶点之间的路径——Floyd算法
- Dijkstra算法
- 具体算法:见图论-01
- 引入辅助变量D:存储已找到的各个最短路径长度
- 它的每一个分量 D [ i ] D[i] D[i]表示当前找到的源点 v v v到终点 v i v_i vi的最短路径长度,初始为边权或 ∞ \infty ∞
- 每次求得一条最短路径后,其重点 v j v_j vj加入集合 S S S,然后对所有的 v k ∈ V − S v_k\in V-S vk∈V−S,修改其 D [ i ] D[i] D[i]的值
- 算法流程
- 初始化
- S ← { v } S\leftarrow \{v\} S←{v}
- D [ i ] ← a d j [ L o c a t e V e x ( G , v ) ] [ i ] D[i]\leftarrow adj[LocateVex(G,v)][i] D[i]←adj[LocateVex(G,v)][i]
- 选择
v
j
v_j
vj使得
- D [ j ] ← M i n { D [ i ] } , v i ∈ V − S D[j]\leftarrow Min\{D[i]\},v_i\in V-S D[j]←Min{D[i]},vi∈V−S
- S ← S ∪ { j } S\leftarrow S\cup\{j\} S←S∪{j}
- 修改
- D [ k ] ← M i n { D [ k ] , D [ j ] + a d j [ j ] [ k ] , v k ∈ V − S } D[k]\leftarrow Min\{D[k],D[j]+adj[j][k],v_k\in V-S\} D[k]←Min{D[k],D[j]+adj[j][k],vk∈V−S}
- 判断
- 若 S = V S=V S=V,则算法结束,否则转步骤2
- 初始化
- 时间复杂度 O ( n 2 ) O(n^2) O(n2)
- Floyd算法
- 算法思想:设顶点集
S
S
S(初值为空),用数组
A
A
A的每个元素
A
[
i
]
[
j
]
A[i][j]
A[i][j]保存从
v
i
v_i
vi只经过
S
S
S中的顶点到达
v
j
v_j
vj的最短路径长度
- 初始时令 S = { } S=\{\} S={}, A [ i ] [ j ] = { 0 i = j W i j i ≠ j , < v i , v j > ∈ E , w i j : w e i g h t ∞ i ≠ j , < v i , v j > ∉ E A[i][j]=\begin{cases}0 & i=j \\ W_{ij} & i\neq j,<v_i,v_j>\in E,w_{ij}:weight \\ \infty & i\neq j, <v_i,v_j>\notin E\end{cases} A[i][j]=⎩⎪⎨⎪⎧0Wij∞i=ji=j,<vi,vj>∈E,wij:weighti=j,<vi,vj>∈/E
- 将图中一个顶点
v
k
v_k
vk加入到
S
S
S中,修改
A
[
i
]
[
j
]
A[i][j]
A[i][j]的值
- A [ i ] [ j ] = M i n { A [ i ] [ j ] , A [ i ] [ k ] + A [ k ] [ j ] } A[i][j]=Min\{A[i][j],A[i][k]+A[k][j]\} A[i][j]=Min{A[i][j],A[i][k]+A[k][j]}
- 重复前一步骤,直到所有顶点加入到 S S S
int A[MAX_VEX][MAX_VEX]; int Path[MAX_VEX][MAX_VEX]; void Floyd_path (MGraph *G) { int j, k, m; for(j=0;j<G->vexnum;j++) for(k=0;k<G->vexnum;k++) { A[j][k]=G->adj[j][k]; Path[j][k]=-1; }//各数组的初始化 for(m=0;m<G->vexnum;m++) for(j=0;j<G->vexnum;j++) for(k=0;k<G->vexnum;k++) if ((A[j][m]+A[m][k])<A[j][k]) { A[j][k]=A[j][m]+A[m][k]; Path[j][k]=m; }//修改数组A和Path的元素值 for(j=0;j<G->vexnum;j++) for(k=0;k<G->vexnum;k++) if (j!=k) { printf("%d到%d的最短路径为:\n", j, k); printf("%d",j) ; prn_pass(j, k); printf("%d", k); printf("最短路径长度为:%d\n",A[j][k]); } } // end of Floyd void prn_pass(int j,int k) { if (Path[j][k]!=-1) { prn_pass(j,Path[j][k]); printf(",%d",Path[j][k]); prn_pass(Path[j][k],k); } }
- 时间复杂度 O ( n 3 ) O(n^3) O(n3),空间复杂度 O ( n 2 ) O(n^2) O(n2)
- 算法思想:设顶点集
S
S
S(初值为空),用数组
A
A
A的每个元素
A
[
i
]
[
j
]
A[i][j]
A[i][j]保存从
v
i
v_i
vi只经过
S
S
S中的顶点到达
v
j
v_j
vj的最短路径长度