近几周学习了图的存储与有关图的各种算法,相比于上一章的二叉树而言更加的难以理解与掌握,emm学习的过程就是在老师讲课的基础上再加上课后openjudge的练习题,这里如果你实在不理解的话个人建议把不明白的算法的整个的演变过程在草稿纸上一步一步的演化,就可以很好的理解(吐槽:oj的题好像就只有一组测试数据)
图的存储结构
由于图的结构比较复杂,任意两个顶点之间都可能存在联系,所以无法以数据元素在存储区中的物理位置来表示元素之间的关系,即图没有顺序的存储结构,但可以借助数组的数据类型表示元素之间的关系。
另一方面,用链表表示图是自然的事,它是一种简单的链式结构,即以顶点表和边表表示图,其中数据域存储该顶点的信息,边索指向的顶点,指针域存储指向其邻接点的指针。图常用的存储结构有邻接矩阵,邻接表、邻接多重表和十字链表。以及边集数组等存储方式。下面写几种常用的。
邻接矩阵存储
基本思想:
1.用一维数组存储顶点 – 描述顶点相关的数据;
2. 用二维数组存储边 – 描述顶点间的边。图的邻接矩阵为 Edge[n][n](数值表示i与j相不相连)。
邻接表存储
这是一种顺序存储与链式存储相结合的存储方式,类似于树的孩子表示法。对于图的每个顶点V,将V的所有邻接点链成一个单链表,称为顶点V的边表。为了方便对所有边表的头指针进行存储操作,可以采用顺序存储。存储边表头指针的数组和存储顶点的数组构成了邻接表的表头数组,称为顶点表。
边集数组存储
边集数组由两个一维数组构成:
1. 一个存储顶点信息。
2. 一个存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)、和权(weight)组成。
当然,没有任何一种存储方式是完美的,每种方式都有它的优缺点,比如邻接矩阵存储实现简单,使用方便。但是空间太大,适用于稠密图的存储,而邻接表的存储呢,随机数据下空间较小,适用于稀疏图的存储,边集数组存储节约空间,但是搜索时又需要把所有的边枚举一遍,太浪费时间。总之碰到要解决的问题,我们要根据问题所要使用的信息来选择不同的存储方式。
下面介绍生成树的概念与它经典的两种算法
生成树:一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边。
最小生成树:我们把构造连通网的最小代价生成树称为最小生成树。(一棵生成树的代价就是树上各边的代价之和)
Prime算法(加点法)
假设全部顶点的集合是V,已经被挑选出来的是U,那么从集合V-U中不断挑选权值最低的点。(这里的权值最低,是相对于U中所有的点来说,是把U看成一个整体。Prim算法和后面的求最短路径的Dijkstra算法有些相似,以某起点为顶点,逐步找各顶点上最小权值的边来构建最小生成树。
Kurskal算法(加边法)
我的理解就是将所有的边排成序列,依次从小到大找边来构建最小生成树。若加入某条边构成回路则舍弃这条边。
Prim算法主要针对顶点展开的,对于稠密图,即边数非常多的情况会更好一些;
Kruskal算法主要针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势。
接下来是求结点之间的最短路径的两个经典算法
Dijkstra算法(一点到其他顶点的路)
它的主要特点是以起始点为中心向外层层扩展(广度优先搜索),直到扩展到终点为止,是一种贪心的策略,声明一个数组dist来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0(dist[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dist[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,此时完成一个顶点, 然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。 然后,又从dist中找出最小值,重复上述动作,直到T中包含了图的所有顶点。
Floyd算法(任意一对顶点之间的路)
它是运用了动态规划的思想来进行问题求解。动态规划解题的关键在于找好子结构。Floyd构造的结构非常巧妙:找i和j之间通过编号不超过k(k从1到n)的节点的最短路径(一定要注意,这里是当前最短路径,当k=n时达到最终最短路径)。为了便于说明,我们可以弄一个三维数组f[k][i][j]表示i和j之间可以通过编号不超过k的节点的“最短路径”。对于k-1到k,只有两种可能,经过编号为k的点,要么不能找到一条从i到j的更短路,此时有f[k][i][j]= f[k-1][i][j] ;要么能找到,那这个最短路径一定是d[i][k]+d[k][j],那么就用这个较小的距离去更新d[i][j]。综合以上两种情况,f[k][i][j] = min(f[k-1][i][j] , f[k-1][i][k]+f[k-1][k][j])。
拓扑排序
对一个有向无环图G进行拓扑排序,就是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)属于E(G),则u在序列中出现在v之前。
拓扑排序对施工有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行。用图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做AOV网。一个AOV网应该是一个有向无环图,不带有回路,因为若带有回路,则回路上的所有活动都无法进行。
实现步骤
- 在有向图中选一个没有前驱的顶点并且输出
- 删除所有和它有关的边
- 重复上述两步,直至所有顶点输出
关键路径
简单说就是开始顶点到完成顶点的最长路径称为关键路径。作为一个关键路径,那么它不能出现回路,可以用上面提到的拓补排序中判断,如果出现回路就不用判断关键路径。在拓补排序完成之后,会有一个数组按顺序存储入度为0的点,用这些点先求出与之相邻的顶点(事件)的最早最晚时间,用数组ve[],vl[]存储,求出之后,需要求出每个活动最早晚开始时间ee[]。el[]
当活动的最晚时间与最早时间求出之后,关键路径的点就是最晚时间与最早时间相等的点,只需要进行判断输出即可。