图的存储与应用

十字链存储
那么对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况,把邻接表与逆邻接表结合起来,这就是十字链表。

其中\(tailvex\)是指弧起点在顶点表的下标,\(headvex\)是指弧终点在顶点表中的下标,\(headlink\)是指入边表指针域,指向终点相同的下一条边,\(taillink\)是指边表指针域,指向起点相同的下一条边。如果是网,还可以再增加一个\(weight\)域来存储权值。

虚线箭头其实就是此图的逆邻接表的表示。对于\(v_0\)来说,它有两个顶点\(v_1\)和\(v_2\)的入边。因此\(v_0\)的\(firstin\)指向顶点\(v_1\)的边表结点中\(headvex\)为0的结点,如上图中的①。接着由入边结点的\(headlink\)指向下一个入边顶点\(v_2\),如图中的②。对于顶点\(v_1\),它有一个入边顶点\(v_2\),所以它的\(firstin\)指向顶点\(v_2\)的边表结点中\(headvex\)为1的结点,如图中的③。顶点\(v_2\)和\(v_3\)也是同样有一个入边顶点,如图中④和⑤。
十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以\(v_i\)为尾的弧,也容易找到以\(v_i\)为头的弧,因而容易求得顶点的出度和入度。而且它除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图的应用中,十字链表是非常好的数据结构模型。
邻接多重表
如果在无向图的应用中,关注的重点是顶点,那么邻接表是不错的选择,但如果更关注边的操作,比如对已访问过的边做标记,删除某一条边等操作,却是比较繁琐的。因此可以仿照十字链表的方式,对边表结点的结构进行一些改造。
**重新定义的边表结点结构 **

其中\(ivex\)和\(jvex\)是与某条边依附的两个顶点在顶点表中的下标。\(ilink\)指向依附顶点\(ivex\)的下一条边,\(jlink\)指向依附顶点\(jvex\)的下一条边。这就是邻接多重表结构。
**邻接多重表构造过程: **
- 先把顶点与边表节点画出;如下图,由于是无向图,所以\(ivex\)是\(0\)、\(jvex\)是\(1\)还是反过来都是无所谓的,不过为了绘图方便,都将\(ivex\)值设置得与一旁的顶点下标相同。 
- 开始连线,首先连线的①②③④就是将顶点的\(firstedge\)指向一条边,顶点下标要与\(ivex\)的值相同; 
- 接着,由于顶点\(v_0\)的\((v_0,v_1)\)边的邻边有\((v_0,v_3)\)和\((v_0,v_2)\)。因此⑤⑥的连线就是满足指向下一条依附于顶点\(v_0\)的边的目标,注意\(ilink\)指向的结点的\(jvex\)一定要和它本身的\(ivex\)的值相同??。
- 同样的道理,连线⑦就是指\((v_1,v_0)\)这条边,它是相当于顶点\(v_1\)指向\((v_1,v_2)\)边后的下一条。\(v_2\)有三条边依附,所以在③之后就有了⑧⑨。连线⑩的就是顶点\(v_3\)在连线④之后的下一条边。
邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。这样对边的操作就方便多了,若要删除左图的\((v_0,v_2)\)这条边,只需要将右图的⑥⑨的链接指向改为\(∧\)即可。

边集数组
边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标\((begin)\)、终点下标\((end)\)和权\((weight)\)组成,如下图:

边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。
最小生成树
- 连通图的生成树:所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的\(n\)个顶点,但只有足以构成一棵树的\(n-1\)条边。
- 最小生成树:具有权值最小的生成树称为图的最小生成树。
\(Prim\)算法
\(Prim\)算法是一种构造性的算法。假设\(G=(V,E)\)是一个具有\(n\)个顶点的带权连通无向图,\(T=(U,TE)\)是\(G\)的最小生成树,其中\(U\)是\(T\)的顶点集,\(TE\)是\(T\)的边集,则由\(G\)构造从起始顶点\(u\)出发的最小生成树\(T\)的步骤如下:



该算法的时间复杂度为\(O(n^2)\),其中\(n\)为网中节点个数,适合于边数目较多(顶点较少)的网。
\(Kruskual\)算法
普里姆\((Prim)\)算法是以某顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树的,同样的,也可以以边为目标去构建,因为权值是在边上,直接去找最小权值的边来构建生成树是很自然的想法,只不过构建时要考虑是否会形成环路。此时要用到了图的存储结构中的边集数组结构。以下是\(edge\)边集数组结构的定义代码:





拓扑排序
给定一个\(AOV网(Activity \ On \ Vertex Network)「顶点表示活动,边表示活动间的先后关系的有向图称为顶点活动网」\),\(A→B\)表示活动\(A必须在活动B\)之前完成。请给出一个合理的活动顺序。\(AOV\)网中不应该出现有向环,因为出现了环就无法拓扑排序,因此可以用拓扑排序来判断图中是否存在环。
- 在有向图中选一个没有前驱的顶点,输出
- 从有向图中删除顶点和所有以该顶点为尾的有向边
- 重复上述步骤,直到全部顶点都已经输出或者图中剩余的顶点中没有前驱顶点为止(这说明有向图中存在环)
设计一个算法,判断有向图中是否存在回路


最短路径
最短路径问题要求解的是:如果从图中某一顶点(称为源点)到达另一顶点(称为终点)的路径可能不止一条,如何找到一条路径,使得沿此路径各边上的权值总和(即从源点到终点的距离)达到最小,这条路径称为最短路径\((shortest \ path)\)。而不是指路径上的边数为最少的路径。
\(Dijkstra\)算法求图中某一顶点到其余各顶点的最短路径。算法思想:设有两个顶点集合\(S\)和\(T\),集合\(S\)中存放图中已经找到最短路径的点,集合\(T\)中存放剩余顶点。初始状态时,集合\(S\)中只包含源点\(v_0\),然后不断从集合\(T\)中选取到顶点\(v_0\)路径长度最短的顶点\(v_u\)并入到集合\(S\)中, 集合\(S\)中每并入一个新的顶点\(v_u\),都要修改顶点\(v_0\)到集合\(T\)中顶点的最短路径长度值,不断重复此过程,直到集合\(T\)的顶点全部并入到\(S\)中为止。


\(3\)个数组
- \(dist[v_i]\)表示当前已经找到从\(v_0\)到每个终点\(v_i\)的最短路径的长度。初始状态为:若从\(v_0\)到\(v_i\)有边,则\(dist[v_i]\)为边上的权值,否则置\(dist[v_i]\)为\(\infty\).
- \(path[v_i]\)中保存从顶点\(v_0\)到\(v_i\)最短路径上\(v_i\)的前一个顶点,假设最短路径上的顶点序列为\(v_0,v_1,v_2,...,v_{i-1},v_i\)则\(path[v_i]=v_{i-1}\),\(path[v_i]\)的初始状态为,如果\(v_0\)到\(v_i\)有边,则\(path[v_i]=v_0\),否则\(path[v_i]=-1\)
- \(set[]\)为标记数组,\(set[v_i]=0\)表示\(v_i\)在\(T\)中,即没有并入到最短路径中;\(set[v_i]=1\)表示\(v_i\)在\(S\)中,即已经并入到最短路径中。\(set[v_i]\)的初始状态为\(set[v_0]=1\),其余元素为\(0\).    时间复杂度为\(O(n^2)\)
\(Floyd\)算法

\[A_{-1}[i][j]=cost[i][j]\]
\[A_{k+1}=MIN{A_k[i][j],A_k[i][k+1]+A_k[k+1][j] } \quad 0\le k \le (n-2)\]
时间复杂度为\(O(n^3)\)
关键路径
在带权有向图\(G\)中以顶点表示事件,以有向边表示活动的先后关系,边上的权值表示该活动的持续时间的网,称为\(AOE\)。用\(AOE\)网表示一项工程计划时,顶点表示的事件实际上就是该顶点所有进入边表示的活动均已经完成,而该顶点的出发边所表示的活动均可开始的一种状态。
- \(AOE\)网中至少一个开始顶点(称为源),其入度为0;
- \(AOE\)网中同时应该有一个结束顶点(称为汇点),其出度为\(0\);
- \(AOE\)网中不存在回路,否则整个工程无法完成
** 相关计算**
- 事件\(v\)的最早发生时间\(ve(v)\):表示从源点到顶点\(v\)的最长路径长度,事件的最早发生时间决定了所有从顶点\(v\)发出的有向边所代表的活动能够开工的最早时间 \[ve(v)=0 \qquad 当v为源点\] \[ve(v)=\max \{ ve(j)+ \lt j,v \gt上的权值\}\qquad 当v为其他顶点\]
- 事件\(v\)的最迟发生时间\(vl(v)\):它是指在不推迟整个工程完成(即保证汇点\(n\)在\(ve(n)时刻发生\)的前提下,该事件最迟必须发生的时间。 \[vl(v)=n \qquad 当v为汇点\] \[vl(v)=\min \{ vl(j)- \lt v,j \gt上的权值\}\qquad\]
- 活动\(a_i\)的最早开始时间\(e(a_i)\):是指该活动的起点所表示的事件最早发生时间。如果由边\( \lt j,k \gt\)表示活动\(a_i\),则有\(e(a_i)=ve(j)\)
- 活动\(a_i\)的最迟开始时间\(l(a_i)\):指该活动的终点所表示的事件最迟发生时间与该活动所需的时间之差\(l(a_i)=vl(k)-a_i的权值\)
- 一个活动的\(a_i\)的最迟开始时间\(l(a_i)\)和其最早开始时间\(e(a_i)\)的差额\(d(a_i)=l(a_i)-e(a_i)\):它是指该活动完成时间的余量,这是在不增加完成整个工程所需要的总时间的情况下,活动\(a_i\)可以拖延的时间。
- 当活动的时间余量为0时,说明该活动必须如期完成,否则就会拖延整个工程的进度
可以通过加快关键活动(即缩短它的持续时间)来实现缩短整个工程的工期。但并不是加快任何一个关键活动都可以缩短整个工程的工期,只有加快了那些包括在关键路径上的关键活动才能达到缩短工期的目的。也不是任意缩短关键活动,因为一旦缩短到一定的程度,该关键活动可能变成非关键活动。