650 likes | 832 Views
本章概要. 第七章 图 本章主要介绍图的基本概念、图的存储、图的遍历以及图的应用;通过学习掌握: * 无向图、有向图、完全图的定义及表示; *路径、简单路径、无向图及有向图的连通; *图的数组存储、邻接表存储及十字链表存储; *最小代价生成树; *拓扑排序; *关键路径及最短路径;. 一、关于图的基本概念 1 、图的定义
E N D
本章概要 第七章 图 本章主要介绍图的基本概念、图的存储、图的遍历以及图的应用;通过学习掌握: * 无向图、有向图、完全图的定义及表示; *路径、简单路径、无向图及有向图的连通; *图的数组存储、邻接表存储及十字链表存储; *最小代价生成树; *拓扑排序; *关键路径及最短路径;
一、关于图的基本概念 1、图的定义 图( Graph)是顶点(Vertex)与边(Edge)的集合。一般表示为一个二元组,即,图G =(V,E )。V:是顶点的非空有限集合,顶点通常代表数据元素;E:是边的有限集合,边代表两个顶点间的关系。例如右图可以表示为: G =(V, E) V = {V1,V2,V3,V4} E ={e1,e2,e3,e4,e5} V1 e1 e2 e5 V2 V3 e3 e4 V4 图 7-1无向图
2、无向图(Undigraph) 如果,一个图中的边都没有方向,则称该图为无向图。图中的边称为无向边(undirected edge). 一般用一对圆括号括起来的顶点无序偶表示一条无向边。无向边代表对称,相等与兄弟关系。 例如上图中e1为一条无向边。e1 = ( v1,v2)=(v2,v1). E = {(v1,v2),(v1,v3),(v2,v4),(v3,v4),(v2,v3)} 对于含有n个顶点,e条边的无向图,设ei为一条无向边表示为 Ei = ( vi, vj ) = ( vj,vi ); vi,vj ∈ V; ei ∈ E; 称vi,vj 是边ei的两个端顶点。 称边ei是与顶点vi,vj 相关联的。 称顶点vi,vj 是相邻接的。如V1与 V2 是 相邻接的,而V1与V4就不邻接。 顶点vi的度 是与顶点vi相关联的边的条数。 例如图7.0中顶点V1、V4的度都为2; 顶点V2与 V3的度读都为3。 V1 e5 V2 V3 e3 e4 V4
2、有向图(Directed Graph) 如果,图中每条边都有固定的方向,则称该图为有向图。有向图 中的边称为有向边(Directed edge).一条有向边是用一对尖括号括 起来的顶点有序偶表示的。有向边代表不对称、不相等或父子关系。 如右图中e1 =< v1 v2 ≠〈 v2 v1 〉 E={<v1,v2>,<v1,v3>,<v3,v4>,<v4,v1>} v1 e1 v2 e2 e3 对于含有n个顶点,e条边的有向图,设ei为一条有向边表示为:ei=《vi, vj》≠ 《vj,vi》; vi,vj ∈ V; ei ∈ E; 称顶点vi为(有向边) 弧(Arc )ei 的尾顶点(tail node) ;称顶点vj为ei的头顶点(head node)或称为终端顶点(Terminal node)。 v3 e4 v4 图7-2有向图
称ei 是与顶点vi,vj相关联的边.称自顶点vi邻接至顶点vj。 顶点vi的度 是与顶点vi相关联的边的条数。有向边的度分为入度与 出度。 顶点vi的入度 是以顶点vi为头的弧的条数。 表示为id( vi) 顶点vi的出度 是以顶点vi为尾的弧的条数。 表示为od( vi) 所以,顶点的度:Td(vi) = id( vi)+ od( vi) 例如图7.1中: id( v1)= 1; od( v1)= 2; id( v3)= 1; od( v3)=1; Td(v1) = id( v1)+ od( v1)= 3; 对于一个具有n个顶点,e条边或弧的图,满足如下关系 e = ∑Td(vi) 3、完全图(Completed graph) 对于一个具有n个顶点,e条边的无向图,若边的总条数为 ½ *n*(n-1),则称该图为完全图(图7.3 a)。 n 1 2 i=1
对于一个具有n个顶点,e条边或弧的有向图,若边的总条数为对于一个具有n个顶点,e条边或弧的有向图,若边的总条数为 n*(n-1),则称该图为有向完全图(图7.3 b)。 有时图的边或弧具有与它相关的数,这种与图的边或弧相关的数称作权(weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网(Network).(有时称为赋权图) 4、子图(Subgraph) 假设有两个图 G =(V,{E})和G’= (V’,{E’})如果, V’⊆V;E’ ⊆E,则称G’是G 的子图。 右下图为图7.1的两子图。 即右两图为图7.1的部分顶点与 部分边组成。 v1 v2 v1 v2 v4 v3 (b) 有向完全图 (a)无向完全图 图7 -3完全图
二、路径、连通与连通图 1、路径 (Path)) 在一个图中若从任一顶点v出发,沿着边能达到另一顶点v`,则称这两顶点间存在路径。两顶点间不重复边的条数称为路径的长度。路径的表示:对于无向图 G = (V,{E} )从顶点v到v`的路径用顶点序列(v =vi,0,vi,1,…vim = v`),(vi,j-1,vi,j) ∈ E, 1≦ j≦ m. 如果,G为有向图,则路径也是有向的,顶点序列应满足《vi,j-1,vi,j》 ∈ E, 1≦ j ≦ m。第一个顶点和最后一个顶点相同的路径称为回路或环(Cycle) )。 2、简单路径 在用一个顶点序列表示一条路径时,若序列中没有相同的顶点重复出现,则称其为简单路径 。除了第一个顶点和最后一个顶点之外,其余顶点均不相同的回路称为简单回路。 3、连通与连通图 在无向图 G中,如果从顶点v到顶点v`有路径,则称v和v`是连通的。如果对于无向图 中任意两个顶点vi、vj ∈ V,vi和vj都是连通的,则称G 是连通图(Connected Graph).例如图7.4(1)所示
A B A B C A B C D E C D E F G H F F G H • 图7.4(2)为一个非连通图,但是它有三个连通分量(Connected Component)如图7.4(3)。 G H I I J K L J K L K 图7-4(1)连通图 图7-4(2)非连通图 D E 连通分量 指的是无向图的极大的连通子图。 注意:极大并不是单指顶点个数一定最多。 连通分量也可以用集合的方式表示 如图7.4(3)的三个连通分量可写为 {A,B,C,F,G,H,J,K,L}; {D,E}和{I}。(5.1 bb) I 图7-4(3)连通分量
v1 v2 在有向图G 中,如果对于每一对顶点vi,vj ∈ V,vi≠vj,从vi到vj和从vj到vi都存在路径,则称G是强连通图如图7.5(1)所示 。 有向图G 中的极大的强连通子图,称为有向图的强连通分量。图7.5(2)为非强连通图与其三个强连通分量。 v1 v2 v7 v1 v2 v5 v6 v3 v4 v3 v4 v5 v6 v7 v4 v3 图7-5(1)强连通图 (2)非强连通图与强连通分量 4、生成树 一个连通图的极小连通子图,称为其生成树。 它含有图中的全部顶点,但只有足以构成一棵 树的 N –1条边。右图为图74(1)的生成树。 图中共有10个顶点,所以生成树中有9条边。 一棵有N个顶点的生成树有且仅有N-1条边。如果 一个图有N个顶点和小于N –1条边,则该图为非连通图。 A B C D E F G H I K
如果,它多于n –1条边,则图中一定有环。但,有n-1条边的图不一 定是生成树。如果一个有向图,恰有一个入度为0的顶点,其余顶 点的入度均为1,则是一棵有向树。一个有向图的生成森林是有若 干棵有向树组成,且含有图中全部顶点,但只有足以构成若干棵不相 交的有向树的弧。图7.6所示为一例。 图7-6 一个有向图及其生成森林 三、图的存储结构 1、图的数组表示法(邻接矩阵法) 一个图可以用两个数组分别存储其数据元素(顶点)的信息和数据元素之间的关系(边或弧)的信息。其形式描述如下: A D A B C F B C E F E D
图的数组表示: #define INFINITY INT_MAX // 最大值∞ #define MAX_VERTEX_NUM 20 // 最大顶点个数 Typedef enum{DG,DN,UDG,UDN} GraphKind;//有、无向图、网 Typedef struct ArcCell { VRType adj ; //VERTYPE是顶点关系类型。对无权图,用1或0表示相邻否;对带权图,则为权值类型 InfoType *infor; //该弧相关信息指针 }ArcCell,AdgMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; Typedef struct { VertexType vexs[MAX_VERTEX_NUM]; //顶点向量 AdjMatrix arcs; //邻接矩阵 int vexnum, arcnum; //图的当前顶点数和弧数 GraphKind kind; //图的种类标志 }Mgraph;
对于含有n 个顶点的图可以用n*n的二维数组A表示(邻接矩阵) 即: 1 若 (vi,vj) ∈ E 或<vi,vj> ∈ E; Aij = 0 反之 1 1 2 3 由G1知无向图的邻接矩阵 是一个对称矩阵,反映了无向图中的边所代表的对称关系。另外矩阵中第i行或第i列中非零元素的个数为图中顶点vi的度可以用下面公式计算: TD(vi) = ∑A[i] [j] (n =MAX_VERTEX_NUM) 同时,我们知道对称矩阵在存储时, 可以用压缩的方式进行存储。 即上三角或下三角。 4 G1 0 1 1 1 n-1 1 0 1 1 j=0 G1 = 1 1 0 1 1 1 1 0 图7-7 无向图G1与邻接矩阵
1 1 2 3 由G2知有向图的邻接矩阵为不对称矩阵,它反映了有向图的 边代表的不对称关系。另外,图中第i行与第i 列中非零元素的个数 分别代表图中顶点vi的出度与入度。 例如,图中第一行共有三个非零元素,表示 图中顶点V1的出度为3。而第一列中没有非零 元素表示图中顶点V1的入度为0。 图中第四行没有非零元素,表示图中顶点 V4的出度为0 。而第四列中共有三个非零 元素,表示图中顶点V4的入度为3 。 同学们,如果有兴趣可以把一个有向 图的邻接矩阵进行自乘。然后,对结果阵中的非零元素的性质进行 讨论。看能得出什么结论? G2 4 0 1 1 1 0 0 0 1 G2 = 0 1 0 1 0 0 0 0 图7-8 有向图G2与邻接矩阵
如果,图为网(赋权图),那么其邻接矩阵可以表示为:如果,图为网(赋权图),那么其邻接矩阵可以表示为: Wij 若(vi,vj)或<vi,vj> E Aij =< ∞ 反之 ∞ 5 ∞ 7 ∞ ∞ v1 5 v2 4 ∞ ∞ ∞ ∞ ∞ 3 7 8 4 8 ∞ ∞ ∞ ∞ 9 v6 9 v3 N = ∞ ∞ 5 ∞ ∞ 6 1 6 5 v5 5 ∞ ∞ ∞ 5 ∞ ∞ v4 3 ∞ ∞ ∞ 1 ∞ (a ) 网 (b) 邻接矩阵 图7-9 网与邻接矩阵 在实际应用时,往往利用其计算最小值,所以,用无穷大。
图的邻接矩阵存储的实现算法 算法分为两大部分:主算法和子算法。主算法的功能是完成对子算法的调用。子算法完成邻接矩阵的存储。 Status CreatGraph(Mgraph & G) { (主算法) scanf( &G.kind); switch (G.kind) { case DG: return CreateDG(G); //造有向图G case DN: return CreateD N(G); //构造有向网G case UDG: return CreateUDG(G); //构造无向图G case UDN: return CreateUD N(G); //构造无向网G default : reaturn ERROR; } }CreateGraph 下面仅以无向网为例说明其构造子算法:
构造无向网: Status CreateUDN(MGraph &G) { // INFO为0则弧无其他信息 scanf(&G.vexnum,&G.arcnum;&IncInfo); for(i=0; i<G.vexnum;++i ) scanf(&G.vexs[i]);// 构造顶点向量 for (i=0; i<G.vexnum;++i ) // 初始化邻接矩阵 for(j=0; j<G.vexnum;++j ) G.arcs[i][j] ={INFINTY,NULL}; for(k=0; k<G. arcnum;++k ) { // 构造邻接矩阵 scanf(&v1,&v2,&w); // 输入一条边关联的顶点及权值 i=LocateVex(G,v1); j = LocateVex(G,v2); // 确定顶点位置 G.arcs[i][j]. adj = w; // 边( v1,v2) )的权值 if(IncInfo) Input(*Garcs[i][j].info) ;// 若边含有相关信息,则输入 G.arcs[j][i] = G.arcs[i][j]; // 置(v1,v2 )的对称边(v2,v1) } for return OK; }CreateUDN
Pdata firstarc Padjvex infor nextarc 2、图的邻接表存储 邻接表(Adjacency List)是图的一种链式存储结构。因为,图是 有顶点和边构成,所以,其存储结点也有下面两种: 头结点( 顶点形成的结点 ) 表结点( 边形成的结点) P->.data:存放顶点的信息; p->.firstarc:存放与该顶点相关联的第一条边结点地址; P->adjvex:与该顶点先相邻接的顶点的下标 p->infor:存放该条边的信息; p->nextarc:存放与下条边的结点地址; 对于含有n个顶点的图可以用n 个带头结点的单链表存储。其中, 第i个单链表是以图中顶点vi 为头结点,是以与顶点vi相关联的边结 点为数据结点而构成的。表头结点通常以顺序结构的形式存储,以 便随机访问任一顶点的链表。 头结点 边结点
网及其邻接表的存储如下: v1 5 v2 1 v1 2 5 4 7 2 5 4 7 3 7 8 4 v6 9 v3 2 v2 3 4 1 6 5 3 v3 1 8 6 9 v5 5 v4 4 v4 3 5 6 6 图7-10网及其邻接表 5 v5 4 5 6 v6 1 3 5 1 由邻接表中很容易求出每个顶点的出度。但要求入度必须遍历所有表结点的邻接顶点域。但在逆邻接表中求入度很方便(见下图)。
1 v1 3 8 6 3 v1 5 v2 2 v2 1 5 3 7 8 4 v6 9 v3 3 v3 2 4 4 5 1 6 5 4 v4 1 7 5 5 v5 5 5 v5 6 1 v4 图7-11 网及其逆邻接表的存储 6 v6 3 9 4 6 无向图的邻接表存储如下图7.12示: v1 1 v1 2 3 4 2 v2 1 3 4 v2 v3 3 v3 1 2 4 v4 图7-12无向图与邻接表 4 v4 1 2 3 注意无向图的邻接表存储时,每条边都存储两次。
3、图的十字链表存储 十字链表(Orthogonal List)存储,仅仅适用有向图。可以,看成是把一个有向图的邻接表存储与其逆邻接表存储结合起来得到的一种链表。图中每条弧形成一个弧结点,每个顶点形成一个顶点结点。其结构分别如下: 顶点结点 弧结点 data firstin firstout tailvex headvex hlink tlink info 顶点结点由三个域: data域存放该顶点的信息;firstin域存放指 向 该顶点的第一条弧结点地址;firstout域存放以该顶点为弧尾顶点的第一条弧当街点地址;弧结点由五个域:tailvex域存放该条弧的尾顶点下标; headvex域存放该条弧的头顶点下标;hlink域存放与该条弧具有相同的头顶点的下一条弧结点地址。 tlink域存放该条弧具有相同的尾顶点的下一条弧结点地址。 info域存放该条弧上的相关信息。
0 v1 1 2 1 3 1 v2 • 下面我们首先给出一个有向图的十字链表存储结构。 2 v3 3 1 3 4 3 v4 4 1 4 2 4 3 v1 v2 v3 v4 图7-13有向图及其十字链表的存储
有向图的十字链表 #define MAX_VERTEX_NUM 20 Typedef struct ArcBox { // 弧结点的结构 int tailvex,headvex; // 弧的尾与头顶点位置 struct ArcBox *hlink, *tlink; // 相同弧头、尾弧的指针 InfoType *info; // 弧的相关信息 }ArcBox; Typedef struct VexNode { // 顶点结构 VertexType data; ArcBox *firstin,*firstout; // 第一条入弧与第一条出弧 }VexNode; Typedef struct { VexNode xlist [ MAX_VERTEX_NUM ]; // 表头向量 int vexnum, arcnum; // 顶点数与弧数 }OLGraph;
具有N个顶点和E条边的有向图的十字链构造算法如下:具有N个顶点和E条边的有向图的十字链构造算法如下: Struct CreatDG(OLGraph &G){ scanf(&G.vexnum,&G.arcnum,&Incinfo); for(i=0;i<G.vexnum;++i) { //构造表头向量 scanf(&G.xlist[i].data); //输入顶点数据 G.xlist[i].firstin= null;G.xlist[i].firstout=null; //初始指针 } For (k=0;k<G.arcnum; ++k){ //输入各弧构造十字链 scanf(&v1,&v2); i = Locatevex(G,v1); j =Locatexex(G,v2); p = (ArcBox *)malloc(sizeof(ArcBox)); //有空间 *P={i, j, Gxlist[j].firstin, Gxlist[i].firstout, NULL} {tailvex, headvex, hlink, tlink, info} G.xlist[j].firstin=G.xlist[i].firstout = p; //完成行列链表首部插入 if(Incinfo)Input(*p->info); //若弧有其他信息,则输入 } } LocateVex(G,Vi)的功能见下页。
LocateVex(G,Vi):是一个取下标的函数。其功能就是把图G中顶点的下标与顶点上的其他信息分离开来。LocateVex(G,Vi):是一个取下标的函数。其功能就是把图G中顶点的下标与顶点上的其他信息分离开来。 v4 v10 v5 v6 v1 v8 V2 v3 练习题 1、根据右图回答下列各题: 1)写出V1到V5的所有简单路径。 2)写出V3的出度与入度。 3)写出该图的所有强连通分量。 2、判断右下图是否是连通图?若不是 写出所有的连通分量。 3、已知含有N个顶点E条边的无向图 以邻接表的方式存储,每个顶点与每条边 形成的结点都占用一个存储单元,问该图共需要 0 0 1 0 1 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1 0 1 1 0 v7 多少个存储单元? 4、已知图的邻接矩阵如右图示,要求画出这个 图的邻接表存储图。
四、图的遍历(Traversing Graph) 1、定义 图的遍历是从图中某一顶点出发对图中每个顶点访问且仅访问一次 的过程。图的遍历与树的遍历不同,在于树中不存在回路沿着分支 同一个顶点不可能被访问多次。但是,图中存在回路,所以同一个 顶点有可能被访问多次。为了避免这种现象发生,在图的遍历过程 中,必须设置该顶点已被访问的标志。一般设置一个数组 visited [0…n-I]其初值均为‘假’,一旦访问了顶点vi便将visited[vi]置‘真’。 2、两种遍历方式 1)深度优先搜索(Depth_First Search)深度优先搜索类似树的先 根序遍历。图是邻接表的方式存储,与其他邻接表不同的是头顶点 中有一个被访问标志位,其初值为假。
深度优先搜索的过程 在图中任选一个顶点作为发顶点V0,访问V0后,依次从V0的没 被访问过的邻接点出发进行深度优先搜索。直到与V0所连通的所有 顶点均被访问。如果,此时图中还有顶点尚未访问,则从剩余的顶 点中再任选一个顶点作为V0,重复上述过程,直到图中全部顶点均 被访问为止。如下图序列为(V1,v2,v4,v8,v5,v3,v6,v7) 1 V1 f 2 3 V1 2 V2 f 1 4 5 V2 V3 3 v3 f 1 6 7 V4 V5 V6 V7 4 4v f 2 8 5 v5 f 2 8 V8 6 v6 f 3 7 7 v7 f 3 6 图7-14图与邻接表 8 v8 f 4 5
深度优先搜索的算法设计与分析 Boolean visited[MAX]; // 访问标志数组 Status(*VisitFunc0(int v)); // 函数变量 Void DFSTraverse(Graph G, Status(*visit)(int v)) { // 深度遍历 VisitFunc = Visit; // 使用全局变量VISITFUNC for (v = 0;v <G.vexnum;++v)visited[v] = FALSE; // 初始假值 for (v = 0;v <G.VEXNUM; ++v) if(!visited[v]) DFS(G,v); // 对尚没访问的顶点调用DFS } Void DFS(Graph G,int v){ // 从第V个顶点出发递归深度优先 visited[v] =TRUE;VisitFunc(v); // 访问第V个顶点 for(w = FirstAdjVex(G,V); W>=0; w =NextAdjVex(G,v,w)) if(!visited[w]) DFS(G,w) ; // 对尚没访问的顶点调用DFS }
v1 由上述遍历可知在遍历过程中有的边发挥了作用、而有的边没发挥作用,如将没发挥作用的边去掉,便得到该图的深度优先生成树;另外,输出的序列方式也不是唯一的,因为出发顶点V是从图中任选的。但是,生成树必须与其遍历输出序列完全相对应。上图深度优先生成树如下: (V1,v2,v4,v8,v5,v3,v6,v7) 图7-15深度优先生成树 在实际系统中为保证深度优先遍历正确执行,需设置辅助空间栈,用来保存遍历过程中访问过的顶点。 还有,在遍历过程中,选择出发顶点V的次数为该图中连通分量的个数。该算法的时间复杂性为O(N+E)与下边所要将的广度优先遍历相同。它们的区别是广度优先遍历的过程使用队列与对顶点访问的顺序不同。 v2 v3 v4 v5 v6 v7 v8
2) 广度优先搜索(Breadth_First Search) 广度优先搜索是类似于树的按层次遍历的过程。假设从图中某个 顶点V出发,访问V以后紧接着访问V 的所有没被访问的过的邻接顶 点,然后分别从这些被访问过的邻接顶点出发做广度优先搜索直到 图中所有已被访问的顶点 的邻接顶都被访问到。如果,此时图中还 有顶点尚未访问 ,则从剩余的顶点中再任选一个顶点作为V0,重复上述过程,直到图中全部顶点都被访问为止。 广度优先搜索右图的输出序列为下面所示 V1 V2 V3 V4 V5 V6 V7 V8 。其广度优先搜索生成树如下面所示。 v1 v2 v3 v4 v5 v6 v7 v8 广度优先搜索算法如下页描述: 图7-16广度优先生成树
Void BFSTravrse(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]) { // V尚未访问 visited[v] = True;Visit(v); EnQueue(Q,V); (V入队列) while(!QueueEmpty(Q)) { DeQueue(Q,u); // 队头元素出队列并置为U for(w=FirstAdjVex(G,u);w>= 0;w =NextAdjVex(G,u,w)) if (! Visited[w]) { // W为U的尚未访问的邻接顶点 Visited[w] = TRUE; Visit(w); EnQueue(Q,w); }if } while }if }
五、图的应用 1、最小代价生成树 假设要在n个城市之间架设通信网络,因为,这是一个无向图所以,从理论上讲最多有n(n-1)/2条线路供选择,但,实际上仅需要其中的N-1条就够了。当然,每条线路都要付出代价。那么,如何得到总代价最少的N-1 条线路哪?这就是下边我们要讨论的问题。 1)、普里姆算法 假设 N =(V,{E})是连通网,TE是N上最小生成树中边的集合。算法从U= {uo}(uo∈ V),TE = {}开始,重复执行下述操作:在所有的边(u,v) (v ∈V-U)中选择代价最小的一条边(u0,v0)并入集合TE,同时将v0并入U,直到U=V为止。此时TE 集合中的边为最小代价生成树。 v1 v1 6 1 5 v2 5 v3 v4 v2 5 v3 5 v4 3 4 2 3 6 4 2 v5 v6 图 7-17最小代价生成树 V5 6 v6
2)普里姆算法之一 # difine INFINITY INT-MAX // 表示最大整数 # difine n 100 // 图的顶点数 Typedf int AdjMatrix[n] [n]; // 用邻接矩阵表示 Typedf struct { int fromvex,tovex; // 边的起点和终点 int Weight; // 边上的权 } Typedf TreeEdge Node MST[n-1]; // 最小生成树类型 AdjMatrix G; // 带权矩阵,作为算发的输入 MST T; // 存放最小生成树
Void InitCandidateSet(AdjMatrix G,MST T,int r) { int i, k = 0; // r为起始顶点,k为T的下标 For ( i = 0;i < n;i++) // 所有邻接边的权存放到T中 if ( i != r) { // i是除r以外的任一顶点 T[k].fromvex = r ; T[k].tovex = i ; // r为边起点, i 为边终点 T[k++]. Weight = G[r] [i] ; // 当前边的权存放到T数组 }if Int SelectMinEdge( MST T,int k ) { // 选择最小边构成树 int min = INFINITY ,i ,minpos ; for(i =k; i < n-1; i ++ ) // 在所有可选边中查找权值最小边 if (T[i]. Weight <min){ min = T[i]. Weight ; // 修改最小值 minpos = i ; // 记录最小边的位置 }if if (min == INFINITY) Error(“Graph is disconnected); return minpos; // 返回最小边 T[minpos]的位置 }
Void Modify(AdjMatrix G,MST T,int k,v) {// 调整预选边集T的有关值 int d, i ; for (i =k;i < n-1;i++) { d = G[v][T[i].tovex]; // d是从v到i最新选中的边的权值 if (d < T[i] Weight.) { // 最新选中的边的权值小于原来权值 T[i]. Weight = d; // 用最新选中的边的权值代替原来权值 T[i]. Fromvex = v; // 修改起始顶点 }if } for }
普里姆算法之一主算法: Void PrimMST(AdjMatrix G,MST T,int r) { int k, m ,v; TreeEdgeNode e; Init CandidateSet( G,T, r); // 调用边集初始子算法 For ( k = 0; k < n-1;k++ ) { m = SelectMinEdge(T, k ) ; // 调用选择最小边子算法 e = T[m]; T[m] = T[k] ; T[k] = e; v = T[k] .tovex; Modify(G, T,, k+1,v); // 调用调整边集子算法 }for }prim
3)、普里姆算法之二 Void MinSpanTree-PRIM(Mgraph G,VerType u){ struct { VertexType adjvex; // closedge包括两部分 VRType lowcost; // 边的代价 }closedge[MAX-VERTEX-NUM]; K = locateVex (G,u); // K是图中任一顶点 For( j=0;j<G.vexnum; ++j ) // 辅助数组closedge初始化 if ( j! = k) closedge[j] = {u,G.arcs[k] [j].adj } ;// u为顶点G.atcs[i].adj为代价, Closedge[k].lowcost = 0;(初始,U= {u} )
For( i=1; i < G.vexnum; ++ i) { // 选择其余G.vexnum-1个顶点 k = minmum(closedge); // 求出T的下一个结点;第K个顶点 (此时closedge[k].lowcost=MIN{closedge[vi].lowcost | closedge[vi]lowcost>0 Vi ∈ V-U) //加上大于0的比较,避免在正整数中0最小的情况 printf(closedge[k].adjvex,G.vexs[k]); // 输出生成树的边 closedge[k].lowcost = 0; // 第K个顶点并入U集合,代价置0 for ( j = 0;j<G.vexnum;++j) if (G.arcs[k] [j].adj<closedge[j].lowcost) // 新顶点并入U后重新选择最小边 closedge[j] = {G.vexs[k],G.arcs[k] [j].adj};// 用代价小的边带替大代价的边 } } }
I closedge (2) (3) (4)(5) (6) 1 2 3 4 5 U V-U K Adjvex v1 v1 v1 {v1} {v2v3v4v5v6} 2 Lowcost 6 1 5 构造最小生成树过程中辅助数组中各分量的值 Adjvex v3 v1 v3 v3 {v1v3} {v2v4v5v6 5 Lowcost 5 0 5 6 4 adjvex Adjvex v3 v6 v3 {v1v3v6} {v2v4v5} 3 Lowcost 5 0 2 6 0 adjvex Adjvex v3 v3 {v1v3v4v6}{v2v5} 1 Lowcost 5 0 0 6 0 Adjvex v2 {v1v2v3v4v6} {v5} 4 Lowcost 0 0 0 5 0 Adjvex {v1v2v3v4v5v6} {} Lowcost 0 0 0 0 0
2)克鲁斯卡尔算法 假设连通网N =(V,{E}),则令最小生成树的初始状态为只 有N个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个 连通分量。在E 中选择代价最小的边,若该边依附的顶点落在T中 不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下 一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分 量上为止。 v1 v1 v1 1 1 1 V2 V3 V4 V2 V3 V4 V2 V3 V4 2 3 2 V5 V6 V5 V6 V5 V6 v1 v1 1 v2 v3 v4 v2 v3 v4 3 4 2 3 4 2 v5 v6 图7-18克鲁斯卡尔法 v5 v6
2、拓扑排序(Topological Sort) 拓扑排序简单的说,是由某个集合上的一个偏序得到该集合上的一个全序,称这个操作为拓扑排序。 若集合X上的关系R是自反的、反对称的和传递的,则称R是集合X 上的偏序关系。 设R是集合X上的偏序(partial Order)),如果对每个x,y∈X必有xRy或yRx。则称R是集合X上的全序关系。 直观地看,偏序指集合中仅有部分成员之间可以比较,而全序指集合中全部成员之间均可以比较。如下图所示。 v2 v1 v4 v1 v2 v3 v4 v3 (a)表示偏序 (b)表示全序 图7-19偏序 和全序
一个表示偏序的有向图可用来表示一个工程流程图。一个表示偏序的有向图可用来表示一个工程流程图。 1)AOV网络(Activity On Vertex Network) 一个用顶点代表某项活动,用弧表示活动间的优先关系的有向图 称为AOV网络。我们知道AOV网络是描述一个工程流程图,因此在AOV网络中不能存在回路。否则该工程流程图是无法执行的。而拓扑排序的功能就是用来检测AOV 网络中是否有环。所以,拓扑排序实际上是把AOV 网络中的顶点按照工程允许执行的顺序整理成一个线性序列的过程。 2)拓扑排序的过程 (1)在图中查找入度为零的顶点。 (2)输出一个入度为零的顶点。 (3)把与输出顶点相邻接的所有的顶点入度减1。 ( 4)重复执行2、3步,直到图中没有入度为零的顶点。 (5)检查输出顶点的个数是否等于图中顶点的总个数,若相等,则该图正确。否则,该图有错误必须修改。 下面通过一个例子来进一步理解拓扑排序的过程。
拓扑排序举例 v2 v6 1 v1 0 2 3 V1 v3 v7 2 v2 1 3 6 v4 v5 v8 3 v3 4 6 7 T0p=0 0 0 0 Top=1 4 v4 0 3 5 1 1 1 4 4 4 5 v5 1 3 7 8 0 0 1 Top=4 6 v6 2 7 8 7 v7 3 8 8 v8 3 图7-20有向图及带入度域的邻接表存储
拓扑排序的算法设计 Status TopologicalSort(ALGraph G) { FindIndegree( G,indegree); // 对各顶点求入度 InitStack(S); // 置空栈 for( i= 0;i <G.vexnum; ++i ) // 查找入度为为零的顶点 if(! Indegree[i]) push (S,i ); // 把入度为零的顶点进栈 count = 0; // 计数器置0
while (! StackEmpty(s)) { // 栈不空时重复输出顶点 pop(s, i); printf(i,Gvextices[i].data; ++count; // 输出顶点、计数 for(p=G.vextices[i].firstarc; p ; p = p->nextarc) { k = p->adjvex; // 得到与输出顶点相邻接的顶点下标 if(! (--indegree[k])) push( s,k); // 将邻接点的入度减1,0度入栈 }for }while if (count < G.vexnum) return ERROR; // 该图有回路 else return OK; }TopologicalSort 该算法的时间复杂性为O(n+e)
3、关键路径 1)AOE(Activity On Edge)网络 即边活动网络。它是一个带权无回路的有向图。其中,顶点代表事件(Event),弧代表活动,权代表活动持续的时间。AOE网络中,开始只有一个入度为零的顶点,称为源点。也只有一个出度为零的顶点,称为汇点。其功能:可用来估算整个工程的完成时间;可找出工程的关键的子工程;可提供压缩整个工期的策略。 2)最早发生时间(ve) 源点的最早发生时间为0,其余任一 顶点Vj的最早发生时间,等于从源点 出发沿着各条路径达到Vj时每条路径上 权的累加和的最大值。一般用下面的计算 a3=3 V2 V5 a1=3 a4=2 a8=1 a7=2 源点V1 V4 V6 汇点 a2=2 a5=4 a6=3 v3 Ve(j)=Max{ve(i) + dut(<i ,j>)} 图 7-21AOE网络 <i,j>∈T, j = 1,2,3…n-1 其中,T为所有以第j个顶点为头的弧的集合。
a3=3 V2 V5 3)最迟发生时间 (vl) 汇点的最迟发生时间Vl[n]等于汇点的最早发生时间Ve[n]。其余任一 顶点Vi的最迟发生时间等于从汇点的最迟发生时间中减去 从顶点Vi出发沿着各条路径达到汇点 时 每条路径上权的累加和的最大值。 最迟发生时间一般用下式计算: V l(i)=M in{v l(j) -dut(<i ,j>)} <i,j>∈S,i =n-2,..,0 其中, S为所有以第i个顶点为头的弧的集合。 上述两个递推公式的计算必须分别在拓扑有序和逆拓扑有序的前提 下进行。即用拓扑有序计算最早发生时间,用逆拓扑有序计算最迟 发生时间。然后,通过对同一顶点的最早发生时间与最迟发生时间 比较是否相等,来确定该顶点是否是关键顶点。在次基础上就可以 a1=3 a4=2 a8=1 a7=2 • 源点V1 V4 V6 汇点 a2=2 a5=4 a6=3 v3
求出关键路径。 4)求出关键路径的算法描述: (1)输入e条弧〈j, k〉,建立AOE网的存储结构; (2)从源点v0出发,令V e[0]=0,按拓扑有序求其余顶点的最早发生时间Ve[i]。 (3)从汇点Vn出发,令Vl [n-1]=V e[n-1],按逆拓扑有序求其余顶点的最迟发生时间Vl[i]。 (4)比较两个时间,找出关键活动。 如上所述计算各顶点的最早发生时间 是在拓扑排序的过程中进行的, 需对拓扑排序的算法作如下修改: 其一,在拓扑排序之前设初值,令Ve[ i]= 0; 其二,在算法中增加一个计算Vj的直接后继Vk的最早发生时间的操作:若Ve[j] +dut(< j,k >) > Ve[ k],则Ve[ k] =Ve[ j]+dut(< j,k >); 其三,为了能按逆拓扑有序序列的顺序计算各顶点的Vl值,需记下在拓扑排序的过程中求得的拓扑排序序列,需要一个栈记录拓扑有序序列,在计算得到各顶点的Ve之后,从栈顶至栈底便为逆拓扑有序序列。 a3=3 V2 V5 a1=3 a4=2 a8=1 a7=2 • 源点V1 V4 V6 汇点 a2=2 a5=4 a6=3 v3
求出关键路径的算法(计算最早发生时间最大值):求出关键路径的算法(计算最早发生时间最大值): Status Topologicalorder(MLGraph G,Stack &T) { FindIndegree(G,indegree) ;// 求各顶点入度,并将入度为0的入S栈 IniStack(T);count=0;Ve[0…G.vexnum-1=0; // 排序栈T置初值 While(!StackEmpty(S)) { pop(S,j);push(T,j); ++count; // 入度为0的j出S栈,进T栈并计数 for( p = G.vextices[j].firstarc; p ; p = p->nextarc) { k = p->adjvex; // 找到J的邻接点.每个邻接点的入度减1 if (--indegree[ k] == 0) push(S,k) ;// 若入度为0,则入S栈 if(Ve[ j]+*(p->info) > Ve[ k]) Ve[ k]=Ve[ j]+*(p->info); // 计算Ve }for *(p->info=dut(<j ,k>) }while if(count<G.vexnum) return ERROR;(AOE有回路) else return OK; }Topologicalorder
Statcs CriticalPath(ALGraph) { if (!TopologicalOrder(G,T)) return ERROR; vl[0..G.vexnum-1] = ve[G.vexnum-1] ; // 每个点的最迟初值都=最早 while(!StackEmpty(T)) // 按拓扑逆序求各顶点的最迟时间 for(Pop(T,j),p =G.vextices[j].firstarc; p ; p = p->nextarc){ k =p->adjvex ; dut = *(p->info); if(vl[ k]- dut < vl[ j]) vl[ j] = vl[k] – dut; }for for(j = 0;j<G.vexnum; ++j ) //求EE和EL 关键活动 for(p =G.vextices[ j].firstarc; p ;p= p->nextarc) { k =p->adjvex; dut= *(p->jnfo); ee = ve[ j];el = vl[ k] –dut; tag = (ee == el ) ? ’*’ :”; printf( j, k, dut,ee,el ,tag ); // 出关键路径 } } 该算法总的时间复杂度为O(N+E)。
5)、关键顶点与关键活动 由上边的分析可知顶点1、3、4、6为关键顶点,而a2 、 a5 、 a7为关键活动。那么,顶点1、2、3、4、5、6的最早发生时间 和各项活动的开始时间存在什么关系、各项活动的开始时间又 是如何计算的呢?我们用右面的表来说明: 活动a1的开始时间为0,最晚为1也不 影响事件V2,因为V2最晚时间为4; 依次类推便可得到每项活动的最早 时间(e)和最晚时间(l)而L – e = 0 的活动便是关键活动. 顶点 ve vl 活动 e l l-e v1 0 0 a1 0 1 1 v2 3 4 a2 0 0 0 v3 2 2 a3 3 4 1 v4 6 6 a4 3 4 1 v5 6 7 a5 2 2 0 v6 8 8 a6 2 5 3 a7 6 6 0 a8 6 7 1 a3=3 V2 V5 a1=3 a4=2 a8=1 a7=2 v1 v4 v6 a2=2 a5=4 a6=3 图 7-22 关键活动 v3