数据结构学习记录(三)
图
一、知识要点
1、图的基本概念
-
图的定义和术语
-
图的定义
- 图(Graph)是由两个集合构成,一个是非空但有限的顶点集合V,另一个是表述顶点之间边的集合E(可能是$\emptyset$)。图可表示为G = (V ,E ).
- 每条边是一顶点对(v, w)且v,w $\in$ V。通常用|V|表示顶点的数量,|E|表示边的数量。
- 在图中,至少要有一个顶点,但边集可以为空。
-
图的相关术语
-
无向图:无向图中顶点之间的所有边都可互通,不用标方向,起点和终点次序并不重要。用圆括号(v, w)表示无向图。
-
有向图:有向图中所有边都有方向,即<v, w>不同于<w, v>,无向图用尖括号<>表示。
-
简单图:所有边不重合的图叫简单图。
-
邻接点:在无向图中,一条边连接的两个顶点叫邻接点;在有向图中,如果<v, w>是一条边,那么可以说起点v邻接到终点w。
-
路径、简单路径和回路:
- 从顶点v沿着边可到顶点w,那么这些边就是路径,路径长度是这条路径所包含的边数。
- 在路径序列中,顶点不重复出现的路径称为简单路径。
- 第一个顶点和最后一个顶点相同的路径称为回路或环。若一个图有n个顶点,并且有大于n − 1条边,则此图一定有环。
- 除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
-
无向完全图:在无向图中,任意两个结点都有边相连,那么这个无向图称为无向完全图。可以证明:如果有n个结点,那么有n(n - 1) / 2条边。
-
有向完全图:在有向完全图中任意两个顶点之间都存在方向相反的两条弧。可以证明:如果有n个结点,那么有n(n - 1) 条边。
-
顶点的度、入度和出度:顶点的度是指依附于某节点的边数。在有向图中,一某顶点为终点的边数称为入度,反之则为出度。度 = 入度 + 出度。
-
稠密图、稀疏图:
-
边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常是相对而言的。一般当图G满足∣ E ∣ < ∣ V ∣ l o g ∣ V ∣时,可以将G视为稀疏图。
-
权、网图:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也称网。
-
子图:设有两个图G = ( V , E ) 和G ′ = ( V ′ , E ′ ), 若V ′是V的子集,且E ′ 是E的子集,则称G ′是G的子图。
-
连通、连通图和连通分量:在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。无向图中的极大连通子图称为连通分量。若一个图有n个顶点,并且边数小于n − 1,则此图必是非连通图。
-
强连通图、强连通分量:在有向图中,若从顶点v到顶点w和从顶点w到项点v之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为有向图的强连通分量。
-
生成树:连通图的生成树是包含其所有结点的一个极小联通子图,若图中结点数为n,那么其生成树有n-1条边。在非连通图中,连通分量的生成树构成了非连通图的生成森林。
-
-
-
图的抽象结构
- 类型名称:图(Graph)
- 数据对象集:一非空的顶点集合Vertex和一个边集合Edge,每条边用对应的一对定点表示。
- 操作集:对于任意的图G $\in$ Graph, 顶点V $\in$ Vertex, 边E $\in$ Edge, 以及任一访问顶点的函数Visit( ),我们主要有以下操作
Graph Create_Graph(int Vertex_Num)
:构造一个有Vertex_Num个结点但没有边的图。void Insert_Edge(Graph G, Edge E)
:在G中增加新边E。void Delete_Edge(Graph G, Edge E):
在G中删除边E。bool Is_Empty(Graph G)
:如果G为空则返回true,否则返回false。void DFS(Graph G, Vertex V, (*Visit)(Vertex))
:在图G中,从顶点V出发进行深度优先遍历void BFS(Graph G, Vertex V, (*Visit)(Vertex))
:在图G中,从顶点V出发进行广度优先遍历
2、图的储存结构
-
邻接矩阵
-
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图结点数据信息,一个二维数组(邻接矩阵)储存图中的边的信息。
-
假设邻接矩阵A是一个n * n的方阵,若(vi, vj)或<vi, vj>是图中的边,那么A[i] [j]为1,否则为0.
-
在无向图中:
- 无向图的邻接矩阵一定是一个对称矩阵(即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的)。 因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。
- 对于无向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的度 TD(vi)。比如顶点v1的度就是1 + 0 + 1 + 0 = 2
- 求顶点vi 的所有邻接点就是将矩阵中第i行元素扫描一遍, A [ i ] [ j ]为 1就是邻接点。
-
在有向图中:
- 主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称。
- 有向图讲究入度与出度,顶点v1的入度为1,正好是第v1列各数之和。顶点v1的出度为2,即第v1行各元素之和。
-
在网图中,A[ i ] [ j ]存的是边的权值。
-
//邻接矩阵的存储结构 #define MaxVertexNum 100 //定义顶点的最大数 typedef char VertexType; //顶点的数据类型 typedef int EdgeType; //带权图中边上权值的数据类型 struct GNode { VertexType Vex[MaxVertexNum]; //顶点表 EdgeType Edge[MaxVertexNum][MaxVertexNum];`//邻接矩阵,边表 int vexnum; //顶点数 int arcnum; //边数 }; typedef struct GNode *MGraph; //图类型 //创建图 #defind IFINITY 65535 //将无穷设为无符号整数最大值 typedef int Vertex; //用顶点下标表示顶点,为整型 //边的定义 struct ENode { Vertex V1, V2; //有向边<V1, V2> WeightType Weight; //边的权值 }; typedef struct ENode *Edge; //初始化一个有VertexNum个顶点但没有边的图 MGraph CreateGragh(int VertexNum) { Vertex V, W; MGraph Graph; Graph = (MGraph)malloc(sizeof(struct GNode)); //建立图 Graph->vexnum = VertexNum; Graph->arcnum = 0; //初始化邻接矩阵 for(V = 0; V < Graph->vexnum; V++) for(W = 0; W < Graph->arcnum; W++) Graph->G[V][W] = INFINITY; return Graph; } //插入边 void InsertEdge(MGraph Graph, Egde E) { Graph->G[E->V1][E->V2] = E->Weight; } //构建图 MGraph BuildGraph() { MGraph Graph; Edge E; Vertex V; int Nv, i; printf("输入顶点个数:"); scanf("%d", &Nv); Graph = CreateGraph(Nv); //初始化图 printf("输入边数:"); scanf("%d", &(Graph->arcnum)); if(Graph->arcnum != 0) //如果有边 { E = (Edge)malloc(sizeof(struct ENode)); //读入边 for(i = 0; i < Graph->arcnum; i++) { printf("输入起点、终点、权重:"); scanf("%d %d %d", &E->V1, &E->V2, &E->Weight); //插入边 InsertEdge(Graph, E); } } //读入顶点数据 for(V = 0; V < Graph->vexnum; V++) { printf("输入顶点数据:"); scanf("%c", &Graph->Vex[V]); } return Graph; }
-
稠密图适合使用邻接矩阵的存储表示。
-
-
邻接表
- 邻接表是图的一种顺序储存与链式储存相结合的储存方法。
-
邻接表有两种结点结构:
-
顶点表:由顶点数据域(Data)和指向第一条邻接边的指针域(FirstEdge)构成。
-
边表:由邻接点域(AdjV)和指向下一条邻接边的指针域(Next)构成。如果是网图结点,还需要增设一个权值域(Weight)。
-
顶点表用数组按下标存储,每个顶点将所有邻接于该顶点的其他顶点链成一个单链表。
-
图的邻接表储存特点
- 若无向图有V个顶点和E条边,那么他的邻接表需要V个头节点和2E个表结点,如果图相当稀疏的话,用邻接表比邻接矩阵节省空间。
-
若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低。
- 在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中的结点个数;但求其顶点的入度则需要遍历全部的邻接表。
-
#define MaxVertexNum 100 //最大顶点数设为100 typedef int Vertex; //用顶点下标表示顶点 typedef int WeightType; //边的权值设为整型 typedef char DataType; //顶点存储数据类型设为字符型 //边的定义 struct ENode { Vertex V1, V2; //边<v1, v2> WeightType Weight; //边的权值 }; typedef struct ENode *Edge; //设置为边结构 //边表的定义 struct AdjVNode { Vertex V; //顶点下标 WeightType Weight; //边权值 struct AdjVNode *Next; //指向下一邻接结点的指针 }; typedef struct AdjVNode *AdjV; //设置为邻接点结点结构 //顶点表头定义 struct VNode { AdjV FristEdge; //指向下一邻接结点的指针 DataType Data; //结点权值 }; typedef struct VNode AdjList[MaxVertexNum]; //设置为邻接表结构 //图节点定义 struct GNode { int VertexNum; //顶点数 int EdgeNum; //边数 AdjList AL; //邻接表 }; typedef struct GNode *LGraph; //设置为邻接表图类型 //创建没有边的图 LGraph CreateLGraph(int VertexNum) { int i; LGraph G; //创建图 G = (LGraph)malloc(sizeof(strcut GNode)); //赋予空间 G->VertexNum = MaxVertexNum; //输入顶点数 G->EdgeNum = 0; //边为0 //给每个表头置空 for(i = 0; i < G->VertexNum; i++) G->AL[i] = NULL; return G; } //插入边 void InsertEdge(LGraph G, Edge E) { //当图为有向图时,插入边<v1, v2> AdjV NewNode; //建立V2结点 NewNode = (AdjV)malloc(sizeof(struct AdjVNode)); //赋予空间 NewNode->V = E->V2; //存入下标 NewNode->Weight = E->Weight; //存入边权值 //将邻接结点插入头节点V1 NewNode->Next = G->AL[E->V1].FirstEdge; G->AL[E->V1].FirstEdge = NewNode; //如果图为无向图,还需把V1插入V2 AdjV New2; New2 = (AdjV)malloc(sizeof(struct AdjVNode)); New2->V = E->V1; New2->Weight = E->Weight; New2->Next = G->AL[E->V2].FirstEdge; G->AL[E->V2].FirstEdge = New2; } //创建图· LGraph BuildLGraph() { int i, VertexNum; LGraph G; Edge E; printf("请输入顶点数:"); scanf("%d", &VertexNum); G = CreateLGraph(VertexNum); printf("请输入边数:"); scanf("%d", &G->EdgeNum); if(G->EdgeNum != 0) { E = (Edge)malloc(sizeof(struct ENode)); for(i = 0; i < G->EdgeNum; i++) { scanf("%d%d%d", &E->V1, &E->V2, &E->Weight); InsertEdge(G, E); } } return Graph; }
-
3、图的遍历
-
深度优先搜索
-
深度优先搜索也称DFS算法,它类似于树的先序遍历,他从某个顶点v0出发,依次递归探索未被探索的顶点。
-
//假设图以邻接表的形式储存 //Visited数组记录顶点的探索情况,预先将所有顶点设置为未探索 bool Visited[MaxVertexNum] = {false}; //访问顶点函数 void Visit(Vertex V) { printf("正在访问结点%d\n", V); } //DFS函数深度优先搜索 void DFS(LGraph G, Vertes V) { //从顶点V出发对图G进行DFS搜索 AdjV W; //访问节点V Visit(V); //标记该节点 Visited[V] = true; //对V的邻接点进行访问 for(W = G->AL[V].FirstEdge; W; W->Next) { //当顶点未被访问时 if(Visited[W->V] == false) //递归访问 DFS(G, W->V); } }
-
-
广度优先搜索
-
广度优先搜索也称BFS算法,它类似于树的按层次遍历的过程,就是构造一个队列,每访问一个顶点就把该顶点的所有邻接顶点放入队列。
-
//假设图以邻接矩阵的方式存储 //Visited数组记录顶点的探索情况,预先将所有顶点设置为未探索 bool Visited[MaxVertexNum] = {false}; //判断<V,W>是否是边 bool IsEdge(MGraph G, Vertex V, Vertex W) { return (G->Edge[V][W] < INFINITY ? true : false); } //以S为出发点对图进行BFS搜索 void BFS(MGraph G, Vertex S) { Queue Q; Vertex V, W; //创建一个空队列 Q = CreateQueue(); Visit(S); //访问该顶点 //标记该顶点 Visited[S] = true; //S入队 AddQ(Q, S); //层序遍历顶点 while(!IsEmpty(Q)) { //当Q不为空时 V = DeleteQ(Q); //弹出顶点 //找到Q的所有邻接点放入队列 for(W = 0; W < MGraph->VertexNum; W++) { if(!Visit[W] && IsEdge(G, V, W)) { Visit(W); Visited[W] = true; AddQ(W); } } } }
-
4、最小生成树
-
生成树的构建与最小生成树的概念
- 对连通图不同的遍历,就可能得到不同的生成树。
- 如果无向连通图是一个网图,那么它的所有生成树中必有权值总和最小的生成树,称为最小生成树(MST),当然最小生成树也未必是唯一的l
- 有两种常用的构造最小生成树的方法:Prim算法和Kruskal算法。
-
构建最小生成树的Prim算法
-
从一个顶点出发,在保证不形成回路的前提下,每找到并添加一条最短的边,就把当前形成的连通分量当做一个整体或者一个点看待,然后重复“找最短的边并添加”的操作。
-
原图为稠密图的时候用Prim算法好一点
-
//最小生成树肯定是稀疏图,所以最小生成树用邻接表储存 //这里用邻接矩阵储存原图 #defind ERROR -1 //错误标记,表示生成树不存在 //prime算法,将图的最小生成树保存到邻接表储存的图MST中,返回最小权值或错误信息 int Prim(MGraph G, LGraph MST) { //定义一个数组用来存放下标代表的顶点到其父结点的权值 //定义最小权值和 WeightType dist[MaxVertexNum], TotalWeight = 0; //定义一个数组来存放下标代表的顶点的父结点 Vertex parant[MaxVertexNum], V, W; int VCount; //VCount表示收录顶点的数量 Edge E; //将所有顶点的父结点初始化为V0,并把每个顶点到v0的权值存入 for(V = 0; V < G->VertexNum; V++) { dist[V] = G->Edge[0][V]; parant[V] = 0; } VCount = 0; //创建一个空的邻接表来表示MST MST = CreateLGragh(G->VertexNum); E = (Edge)malloc(sizeof(struct ENode)); dist[0] = 0; //表示V0已被收录 VCount++; parant[0] = -1; //表示当前树根为V0 //核心代码,收录最小权值并更新未收录顶点的父结点 while(1) { V = FindMinDist(G, dist); //找到dist中的最小权值 if(V == ERROR) break; //如果最小权值不存在,则结束算法 //将V和相应的边<parant[V], V>收录进MST E->V1 = parent[V]; E->V2 = V; E->Weight = dist[V]; InsertEdge(MST, E); TotalWeight += dist[V]; dist[V] = 0; //标记V顶点已被收录 VCount++; //找到与当上一个被收录顶点相邻的的所有未收录顶点 //如果有未收录顶点到上一个被收录节点的边权值比dist数组存的数小,那么更新dist数组,并更新该未收录顶点的父结点 for(W = 0; W < G->VertexNum; V++) { if(G->Edge[V][W] < dist[W]) { dist[W] = G->Edge[V][W]; parant[W] = V; } } }//while结束 //如果收录的顶点未满就返回错误信息 if(VCount < G->VertexNum) return ERROR; //否则返回最小权值总和 return TotalWeight; } //返回dist中未收录顶点最小顶点 Vertex FindMinDist(MGraph G, WeightType dist[]) { Vertex MinV, V; WeightType MinDist = INFINITY; for(V = 0; V < G->VertexNum; V++) { if(dist[V] != 0 && dist[V] < MinDist) { MinDist = dist[V]; MinV = V; } } if(MinDist < INFINITY) return MinV; else return ERROR; }
-
-
构建最小生成树的Kruskal算法
-
当原图是稀疏图时,用Kruskal算法比prim算法好
-
Kruskal算法是一种按权值的递增次序选择合适的边来构造最小生成树,所以此算法是选择最小的边来构造的。
-
//这里用邻接表储存原图和最小生成树 int Kruskal(LGraph G, LGraph MST) { //初始化空MST MST = CreateLGraph(G->VertexNum); //设置权重和 WeightType Vcount = 0; //设置parant数组,并将其初始化 int parant[G->VertexNum] = -1; //设置一个边集,把图的所有边存入边集 Edge E[G->EdgeNum]; CreateEdge(G, E); //构造一个包含原图所有边的最小堆 MinHeap H = BuildMinHeap(E); //while循环构造最小生成树 while(MST->EdgeNum < G->Vertex - 1 && G->EdgeNum != 0) { //从最小堆取一个边 Edge E0 = DeleteMinHeap(H); //原图边集中删掉这个边 DeleteEdge(G, E0); //确认这个边是否会让MST形成回路 if(!Find(parant, E0)) //是则丢弃该边 //否则将这个边加入MST,并标记顶点 { InsertLGraph(MST, E); if(parant[E->V1] == -1) parant[E->V1] = E->V2; if(parant[E->V2] == -1) parant[E->V2] = E->V1; } //增加权重和 Vcount += E->Weight; //增加MST收集的边数 } //如果MST收集的边不足则返回错误信息 if(MST->EdgeNum != G->NertexNum) return ERROR; else return Vcount; //否则返回权重和 } //将图G的边存入边集E中 void CreateEdge(LGraph G, Edge E[]) { Vertex V, W = 0; for(V = 0; V < G->VerNum; V++) { AdjV H = G->AL[V]->FirstEdge; while(H != NULL) { E[W]->V1 = V; E[W]->V2 = H->V; E[W]->Weight = H->Weight; W++; H = H->Next; } } } //构造一个包含原图所有边的最小堆 MinHeap BuildMinHeap(E[]); { MinHeap H = CreateMinHeap(G->EdgeNum); int i; for(i = 0; i < G->EdgeNum; i++) { H->Data[i] = E[i]; } return H; }
-
5、最短路径
-
单源最短路径
-
从一个源点到其他各项顶点最短路径问题称为“单源最短问题”,该问题可描述为图G中的顶点V0到其余各顶点的最短路径。
-
用Dijkstra算法可以求单源最短路径,这个算法和prim算法几乎一样,都是用dist数组存最短边,parent数组存父结点
-
//利用Dijkstra算法求单源最短路径 //这里图用邻接矩阵存储 #defind ERROR -1 //设置报错符号 #defind INF 65535 //指定权值为无穷的边为INF //构建最短路径结构 struct MLNode { //源顶点和终顶点 Vertex HomeV; Vertex EndV; //包含最短路径中所有经过的顶点的数组 Vertex *SumV; //顶点个数 int VerNum; //路径总长度 WeightType SumLength; }; typedef struct MLNode *MinL; //求最短路径函数,函数形参是图、起始顶点和最终顶点 MinL MinLength(MGraph G, Vertex HV, Vertex EV) { //创建一个长度为G->VertexNum的一个parent数组,用来存每个顶点对应的父结点 int parent[G->VertexNum]; //创建一个长度为VertexNum的一个dist数组,用来存每个顶点到其父结点的权值 int dist[G->VertexNum]; //利用Dijkatra算法得到parent数组和dist中的元素 Dijkatra(G, HV, dist, parent); //调用FindSumV函数得到MinL->SumV和Min->VerNum Vertex *SV; int VN = FindSumV(EV, parent, SV); //调用FindSumL函数得到SumLength WeightType SL = FindSumL(dist, SV); //初始化MinL MinL ML = CreateMinL(ML, HV, EV, SV, VN, SL); return ML; } //Dijkatra算法,构造dist数组和parent数组 void Dijkatra(MGraph G, Vertex HV, int dist[], int parent[]) { Vertex V, W; //设置一个mark数组,用来标记已收录的顶点 int mark[G->VertexNum]; //初始化dist数组,将每个顶点到源顶点的权值存入 //初始化parent数组,将与源节点邻接的顶点的父结点设为源节点,不邻接的设为-1 //初始化mark数组,将所有顶点设置为未收录 for(V = 0; V < G->VertexNum; V++) { dist[V] = G->Edge[HV][V]; if(dist[V] < INF) parent[V] = HV; else parent[V] = -1; mark[V] = 0; } //标记源顶点 dist[HV] = 0; mark[V] = 1; //利用while函数循环收录顶点 while(1) { //调用FindMinDist函数找与父结点最小邻接边的顶点V V = FindMinDist(G, dist, mark); //没找到则结束循环 if(V == ERROR) break; //找到了则标记该顶点V已收录 mark[V] = 1; //利用for循环找能够让dist变小的顶点 for(W = 0; W < G->VertexNum; W++) { //如果循环到的邻接顶点的邻接边小于其父结点的邻接边 if(G->Edge[V][W] < dist[W]) { //将该边替换 dist[W] = G->Edge[V][W]; //更新parent数组 parent[W] = V; } } } } //从dist数组找未收录最小顶点 Vertex FindMinDist(MGraph G, dist[], mark[]) { int i, minV = 0; int flag = 0; for(i = 0; i < G->VertexNum; i++) { if(mark[i] && dist[minV] > dist[i]) { flag = 1; minV = i; } } if(flag == 1) return minV; else return ERROR; }
-
-
每一对顶点之间的最短路径
-
在稠密图中,求得每一顶点最短路径有一个算法比Dijkatra算法效率更高,也更简单,他是Floyd算法
-
Floyd算法的大概流程是
- 找一个初始顶点H,如果某个从A到B的路径比先A到H,再H,到B的路径长,那就可以说H是A到B的一个中转顶点,然后更新A到B的最短路径
- 依次用H结点循环所有起点到终点的路径,就可以得到初级的每一个顶点之间的最短路径。
- 然后再找别的顶点看看能不能充当初级最短路径图的中转顶点,然后又更新初级路径图,从此往复,直到循环完所有顶点,找到每对顶点最终最短路径图,和最终中转顶点表
-
//Floyd算法求各个顶点最短路径 //这里图用邻接矩阵储存 void Floyd(MGraph G, WeightType dis[][MaxVertexNum], Vertex path[][MaxVertexNum]) { Vertex i, j, K; //初始化最短路径图dis,将图G复制到dis for(i = 0; i < G->VertexNum; i++) for(j = 0; j < G->VertexNum; j++) { dis[i][j] = G->Edge[i][j]; //初始化中转顶点表path path[i][j] = -1; } //循环每个中转顶点 for(k = 0; k < G->VertexNum; k++) //循环每条邻接边,找到他们的中转顶点 for(i = 0; i < G->VertexNum; i++) for(j = 0; j < G->VertexNum; j++) { //如果i到j的路径比i到k再到j的路径长,就更新i到j的路径 if(dis[i][k] + dis[k][j] < dis[i][j]) { dis[i][j] = dis[i][k] + dis[k][j]; //更新中转顶点 path[i][j] = k; } } //最终path[i][j]存的中转顶点是邻接于顶点i的 } //利用中转顶点表来找最短路径路程 void Find(MGraph G, path[][MaxVertexNum], Vertex VH, Vertex VE); { printf("%d ", HV); Vertex k = path[VH][VE]; while(K != -1) { printf("-> %d ", k); k = path[k][HE]; } printf("-> %d\n", HE); }
-
6、拓扑排序
-
拓扑排序是对有向无环图进行排序,它的排序目的是为了排列顶点的先后顺序。
-
拓扑排序的基本步骤:找到任意一个入度为0的顶点,并从图中删除该顶点以及与其相邻的所有边,然后对改变后的图重复此操作。如果每个顶点入度都大于一,那么必定存在回路。
-
可用队列来储存入度为0的顶点
-
//拓扑排序排列有向无环图 //这里用邻接表表示图 bool TopSort(LGraph G, Vertex TopOrder[]) { //对图G进行拓扑排序,排序后的顶点放入TopOrder数组 //遍历图,得到所有顶点的入度,并存入Indegree数组 Vertex Indegree[G->VertexNum]; Vertex V; AdjV W; for(V = 0; V < G->VertexNum; V++) for(W = G->AL[V].FirstEdge; W == NULL; W = W->Next) { Indegree[W->V]++; } //创建队列,并将所有入度为0的顶点入队 Queue Q = CreateQueue(G->VertexNum); for(V = 0; V < G->VertexNum; V++) { if(Indegree[V] == 0) AddQ(Q, V); } //核心代码 //设置一个计数器来观察所有出过队的顶点数量 int cnt = 0; //while循环来实现拓扑排序 while(!IsEmpty(Q)) //当队列为空时说明没有入度为0的顶点,退出循环 { //出队一个顶点,把它存入TopOrder数组,并将计数器加一 V = DeleteQ(Q); TopOrder[cnt] = V; cnt++; //把这个顶点的邻接顶点的入度数减一,并且把入度减少后度数为0的邻接顶点入队 for(W = G->AL[V].FirstEdge; W; W = W->Next) { Indegree[W->V]--; if(Indegree[W->V] == 0) AddQ(Q, W->V); } } //如果计数器计算的顶点小于原图中顶点数量,说明原图有回路,返回false if(cnt < G->VertexNum) return false; else return tru //结束算法 }
7、关键路径计算
-
AOE网:即边表示活动的网。
- AOE网是一个带权的有向无环图,其中顶点表示事件,弧表示活动,权值表示为活动持续的时间。
- AOE中入度为0的点称为源点,出度为0的点称为汇点。
- 用AOE网可以表示一个工程,源点表示接到任务,汇点表示完成任务,完成某一个任务就到达某一个顶点,边表示完成该任务需要的时间。
-
ETV:表示事件最早发生的时间,也就是顶点最早发生时间。
-
LTV:事件最晚需要开始时间,如果超过了这个时间就会延误工期。
-
关键路径:ETV和LTV相同的顶点是关键顶点,因为关键顶点不能延误,从源点到汇点和经过的关键顶点形成的路径就是关键路径。
-
//关键路径的计算 //用拓扑排序可求得顶点的ETV和LTV //这里用邻接表储存图 //拓扑排序求ETV和LTV bool TopSort(LGraph G, int ETV[], int LTV[]) { //建立一个TopOrder数组,用来存排序好的顶点 Vertex TopOrder[G->VertexNum]; //遍历图,得到所有顶点的入度,并存入Indegree数组 Vertex Indegree[G->VertexNum]; Vertex V; AdjV W; for(V = 0; V < G->VertexNum; V++) for(W = G->AL[V].FirstEdge; W == NULL; W = W->Next) { Indegree[W->V]++; } //创建队列,并将所有入度为0的顶点入队,更新ETV数组 Queue Q = CreateQueue(G->VertexNum); for(V = 0; V < G->VertexNum; V++) { if(Indegree[V] == 0) { AddQ(Q, V); } } //核心代码 //设置一个计数器来观察所有出过队的顶点数量 int cnt = 0; //while循环来实现拓扑排序 while(!IsEmpty(Q)) //当队列为空时说明没有入度为0的顶点,退出循环 { //出队一个顶点,把它存入TopOrder数组,并将计数器加一 V = DeleteQ(Q); TopOrder[cnt] = V; cnt++; //遍历顶点V的邻接顶点,如果V的开始时间加活动时间大于邻接顶点的开始时间,则替换邻接顶点时间 for(W = G->AL[V].FirstEdge; W; W = W->Next) { if(ETV[V] + W->Weight > ETV[W->V]) ETV[W->V] = ETV[V] + W->Weight; } //把这个顶点的邻接顶点的入度数减一,并且把入度减少后度数为0的邻接顶点入队 for(W = G->AL[V].FirstEdge; W; W = W->Next) { Indegree[W->V]--; if(Indegree[W->V] == 0) AddQ(Q, W->V); } } //如果计数器计算的顶点小于原图中顶点数量,说明原图有回路,返回false if(cnt < G->VertexNum) return false; //根据TopOrder数组从最后元素开始计算LTV到下标为0的元素 //初始化LTV数组,将汇点值赋予LTV数组 for(V = 0; V < G->VertexNum; V++) { LTV[V] = TopOrder[G->VertexNum - 1]; } for(V = G->VertexNum - 2; V >= 0; V--) { //遍历顶点V的邻接顶点,如果邻接点最晚时间减活动时间小于V的最晚时间,则替换V顶点时间 for(W = G->AL[V].FirstEdge; W; W = W->Next) { if(LTV[W->V] - W->Weight < LTV[V]) LTV[V] = LTV[W->V] - W->Weight; } } //算法结束 return true; } //求关键路径函数,将关键顶点存入CriticalV[]数组中 bool CriticalPath(LGraph G, Vertex CriticalV[]) { //设置ETV数组存每个顶点最早开始时间 //设置LTV数组存每个顶点最晚需开始时间 int ETV[G->VertexNum] = {0}; int LTV[G->VertexNum]; //利用拓扑排序得到ETV和LTV if(TopSort(G, ETV, LTV) == false) return false; //将每个顶点的ETV和LTV比较,如果相等就放入CriticalV数组 Vertex V; int cnt = 0; for(V = 0; V < G->VertexNum; V++) { if(ETV[V] == LTV[V]) { CriticalV[cnt] = V; cnt++; } } return true; }
8、应用实例
-
六度空间理论:“你和任何一个陌生人之间所间隔的人不会超过六个”。
-
六度分隔理论的论证:在人际关系网络图G中,任意两个顶点之间都有一条路径长度不超过6的路径。可以采用广度优先算法对图G进行6层遍历,统计这些路径长度不超过6的顶点数,计算这些顶点数与所有顶点数的比例
-
#define SIX 6 //标记顶点是否被访问的数组 int Visited[MaxVertexNum] = {0}; //以S点出发,对图G进行6次广搜,返回路径顶点数 int SDS_BFS(LGraph G, Vertex S) { Vertex V; AdjV W; //创建空队列 Queue Q; Q = CreateQueue(MaxVertexNum); //设置两个计数器,Count计数顶点数,Level计数当前层数 int Count = 1, Level = 0; //设置一个顶点Last表示每层最后的顶点,Tail表示每层待更新的层尾顶点 Vertex Last, Tail; Last = S; //将S放入空队列 AddQ(Q, S); //标记S; Visited[S] = 1; //利用循环广搜六次 while(!IsEmpty(Q)) { //弹出V V = DeleteQ(Q); //将V的邻接点加入队列 for(W = G->AL[V].FirstEdge; W; W = W->Next) { if(Visited[W->V] == 0) { Visited[W->V] = 1; //入队 AddQ(Q, W->V); //统计人数 Count++; //更新层尾 Tail = W->V; } } //如果顶点V是层尾顶点则层数加一 if(V == Last) { Level++; //更新下一层层尾顶点 Last = Tail; } //如果到了六层,那么退出循环 if(Level == SIX) break; } //删除队列 DestoryQueue(Q); return Count; } //检验六度空间理论 void Six_Degrees_of_Separation(LGraph G) { Vertex V; int Count; for(V = 0; V < G->VertexNum; V++) { Count = SDS_BFS(G, V); printf("%d, %.2lf%%\n", V, 100.0 *(double)Count/ (double)G->VertexNum); } }
二、感想
这一章学得真久/(ㄒoㄒ)/~~