主要内容
现实生活中的许多问题都可以转化为图来解决。例如,如何以最小成本构建一个通信网络,如何计算地图中两地之间的最短路径,如何为复杂活动中各子任务的完成寻找一个较优的顺序等。
下面将介绍图的四个常用算法:最小生成树、最短路径、拓扑排序和关键路径。
最小生成树
假设要在n个城市之间建立通信联络网,则连通n个城市只需要n-1条线路。这时,自然会考虑如何在最省经费的前提下完成任务。
在一个连通网的所有生成树中,各边的代价之和最小的那棵生成树称为该连通网的最小生成树。
MST性质:最小生成树中必定存在一条具有最小权值的边。普利姆(Prim)算法和克鲁斯卡尔(Kruskal)算法是两个利用MST性质构成最小生成树的算法。
普利姆算法的核心思想是归并点,时间复杂度为O(n²),适用于稠密网;
克鲁斯卡尔算法的核心思想是归并边,时间复杂度为O(elog2e),使用与稀疏网。
普利姆算法(加点法)
<逻辑思路>
(1)设所有顶点保存在集合V中,已被归并的点保存在集合U中,则未被归并的点保存在集合V-U中;
(2)在图中任意找一个起始顶点v1,v1归入U,离开V-U;
(3)顶点v1存在v2,v3,v4三个邻接顶点,找出权值最小的边(v1,v3);
(4)顶点v3归入U,离开V-U;
(5)顶点v1剩余邻接顶点v2,v4,顶点v3有邻接顶点v2,v4,v5,v6;
(6)比较(v1,v2)和(v3,v2),得出权值更小边(v3,v2);比较(v1,v4)和(v3,v4),两边权值相同;
*在逻辑思路中,其实第(6)步可以省略,直接比较所有边的权值,再从中选择权值最小的边。但代码实现中,应该避免数据冗余,先筛选部分意义重合的数据。
(7)比较(v3,v2),(v1,v4)或(v3,v4),(v3,v5),(v3,v6),找出权值最小的边(v3,v6);
(8)顶点v6归入U,离开V-U;
(9)顶点v1剩余邻接顶点v4(因为(v3,v2)的权值更小,所以不再需要考虑v1到v2的情况),顶点v3剩余邻接顶点v2,v4,v5,顶点v6有邻接顶点v4,v5;
(10)到这里思路应该清晰了。
<实现思路>——以邻接矩阵为存储结构的无向网
(1)顶点集合为V等价于邻接矩阵图中用于存储顶点信息的一维数组vexs[vexnum];
(2)算法最巧妙的地方——结构体数组closedge[vexnum],包含信息:最小边在集合U中的那个顶点(adjvex)和最小边的权值(lowcost)。
结构体数组closedge[]的使用正是<逻辑思路>中步骤(6)的体现。
closedge[vi-1]表示顶点vi,当lowcast不为0时,vi在集合V-U中;当lowcast记为0时,vi归并到集合U中;
(3)循环执行某一段代码,直至closedge[]中所有元素的lowcast属性都归0,即所有顶点都并入到集合U中。
(看代码前可以先回顾下“邻接矩阵”的知识)
typedef struct /*定义结构体数组closedge[vexnum]*/
{
Vextype adjvex;
Arctype lowcast;
} closedge[vexnum];
void MiniSpanTree_Prim(AMGraph G, Vextype vi)
{
int i = LocateVex(G, vi); /*确定起始顶点vi的编号*/
closedge[i] = {NULL, 0}; /*将vi归并到集合U中*/
for(int vj = 1; vj <= G.vexnum; vj++) /*对于V-U中的每个顶点vj,初始化closedge[vj-1]*/
{
int j = LocateVex(G, vj);
if(j != i) closedge[j] = {vi, G.arcs[i][j]};
}
for(int k = 1; k < G.vexnum; k ++) /*直到所有顶点归并到集合U前,循环执行某一段代码*/
{
i = Min(closedge); /*函数Min()找出closedge[]中lowcast最小的元素,并返回下标i*/
/*即找出权值最小的边,并找出位于V-U中的顶点vj*/
closedge[i].lowcast = 0; /*将顶点vj归并到集合U中*/
u0 = closedge[i].adjvex /*u0为最小边在U中的点*/
v0 = G.vexs[i]; /*v0为最小边在V-U中的点*/
cout<<u0<<v0; /*输出u0,v0以记录路线*/
for(int j = 0; j < G.vexnum; j++)
{
/*之前对closedge[]的元素进行过初始化,并入新的点后要重新选出权值更小的边,对应<逻辑思路>的步骤(6)*/
if(G.arcs[i][j] < closedge[j].lowcast)
closedge[j] = {G.vexs[i], G.arcs[i][j]};
}
}
克鲁斯卡尔算法(加边法)
如果说普利姆算法是“加点法”,那么克鲁斯卡尔算法就是“加边法”。
<逻辑思路>
(1)将由n个顶点组成的连通图拆分成n个连通分量,即每个顶点为一个连通分量;
(2)将图上的所有边按权值排序;
(3)从最小边开始操作,归并边的判断条件是下一条被选边不能使连通分量形成回路,即被选边的两个顶点head和tail不能在同一个连通分量上;
(4)在循环中执行某段代码,直至所有顶点被归并到同一连通分量。
<实现思路>——以邻接矩阵为存储结构的无向网
(1)按权值排序可以使用“冒泡法”和“选择法”;
(2)从步骤(3)可以看出,我们需要类似于普利姆算法中的closedge[]那样的辅助数组。包含的信息:每一条边的头顶点和尾顶点以及边上的权值;
(3)同时,我们还需要一个标记数组辅助判断每一个顶点所属的连通分量;
(4)归并一个顶点后,将顶点的连通分量改为并入它的连通分量。
typedef struct /*结构体数组的各元素代表各边*/
{
Vextype head;
Vextype tail;
Arctype lowcast;
} Arcs[arcnum];
int VexSet[vexnum]; /*VexSet[vi-1] = i;表示vi所在的连通分量编号为i,即它本身*/
void MiniSpanTree_Kruskal(AMGraph G)
{
for(int t = 0; t < arcnum; t++) /*输入各边信息*/
cin>>Arcs[t].head>>Arcs[t].tail>>Arcs[t].lowcast;
Sort(Arcs); /*按权值将图上各边从小到大排序*/
for(int t = 0; t < arcnum; t++) /*对图上所有边(权值从小到大)依次进行操作*/
{
Headv = LocateVex(G, Arcs[t].head); /*确定头尾顶点的编号*/
Tailv = LocateVex(G, Arcs[t].tail);
VS_h = VexSet[HeadV]; /*确定头尾顶点所在的连通分量*/
VS_t = VexSet[Tailv];
if(VS_h != VS_t) /*若两个顶点不在同一连通分量*/
{
cout<<Arcs[t].head<<Arcs[t].tail; /*输出符合要求的边*/
for(int v = 0; v < G.vexnum; v++) /*对所有顶点进行判断*/
if(VexSet[v] == VS_t)
VexSet[v] = VS_H; /*将顶点归并到同一连通分量*/
}
}
}
最短路径
最简单的最短路径是求中转次数最少的路径,而不考虑每条边的权值。而在实际问题中,路径长度的度量就不再是路径上的边数,而是路径上所有边的权值之和。
在有向网中,习惯上称路径的第一个顶点为源点(Source),最后一个顶点为终点(Destination)。
下面主要讨论两种最常见的最短路径问题:
一、从某个源点到其余顶点的最短路径;
二、求每一对顶点之间的最短路径。
迪杰斯特拉(Dijkstra)算法用于求解第一种问题,时间复杂度为O(n²);弗洛伊德(Floyd)算法用于求解第二种问题,时间复杂度为O(n³)。
从实现形式上来说,弗洛伊德算法比迪杰斯特拉算法更为简洁。
迪杰斯特拉算法(单源点)
<逻辑思路>
(1)从单源点出发,求到各个顶点的最短路径。该问题类似于就有向连通网的最小生成树;
(2)因此同样需要辅助数组来记录顶点是否被归并和最短路径长度;
(3)迪杰斯特拉算法的巧妙之处就在于设计了三个辅助数组:
1. 一维数组S[]:记录顶点vi是否被确定最短路径长度,即该顶点是否被归并到终点集合中。初始化:S[v0-1] = TRUE;
2. 一维数组Path[]:记录顶点vi的直接前驱。譬如存在最短路径<v1,v4,v3>,v4的直接前驱是v1,v3的直接前驱是v4,这样一来,依靠直接前驱数组就能将路径连接起来。初始化:若源点v0到vi有弧,则Path[vi-1]为v0-1(编号/下标);否则为-1。
3. 一维数组D[]:记录从源点v0到终点vi的最短路径长度。初始化:若源点v0到vi有弧,则D[vi-1]为弧上的权值;否则为∞。
(4)算法开始时,先找到D[]上的最小值,然后并入第一个终点v1,并将S[v1-1]的值设为TRUE;
(5)由于加入了终点v1,相当于v0多了一个中转点,所以需要对D[]上的值进行更新;
(6)更新后继续找D[]上的最小值,继而找到了顶点v2,因为S[v2-1]的值为FALSE,即v2未被归并到终点集合,所以可以将它并入;
(7)若到v2的最短路径是<v0,v1,v2>,则将v2的直接前驱改为v1,即Path[v2-1] = v1-1;
(8)反复执行以上过程(n-1次),直至所有顶点被并入终点集合。
<实现思路>——以邻接矩阵为存储结构的有向网
(1)对应<逻辑思路>步骤(3)的初始化:
1. S[v0-1] = TRUE;
2. D[vi-1] = G.arcs[v0-1][vi-1];
3. if(G.arcs[v0-1][vi-1] != MaxInt) Path[vi-1] = v0-1; /*MaxInt表示无穷∞,值为326767*/
(2)对应<逻辑思路>步骤(8)的n-1次循环
1. 步骤(4):D[v1] = Min(D);
2. 步骤(6):if(S[v1-1] = FALSE) S[v1-1] = TRUE;
3. 步骤(5)和步骤(7): if(D[v1-1] + G.arcs[v1-1][v2-1] < D[v2-1]) {D[v2-1] = D[v1-1] + G.arcs[v1-1][v2-1]; Path[v2-1] = v1-1;}
#define TRUE 1
#define FALSE 0
void ShortestPath_DIJ(AMGraph G, int v0)
{
int S[vexnum];
int Path[vexnum];
int D[vexnum];
int i = LocateVex(G, v0);
for(int vi = 1; vi <= G.vexnum; vi++) /*初始化*/
{
int j =LocateVex(G, vi);
S[j] = FALSE;
D[j] = G.arcs[i][j];
if(D[j] < MaxInt) Path[j] = i; /*小于MaxInt即v0与vi间存在弧*/
else Path[j] = -1; /*若vi的直接前驱不是v0,则置为-1*/
}
S[i] = TRUE;
D[i] = 0; /*源点到源点的路径长度为0*/
/*----------初始化结束-----------*/
for(int t = 1; t < G.vexnum; t++) /*循环n-1次*/
{
int v; /*下一个终点*/
for(vi = 1; vi <= G.vexnum; vi++)
{
int min = MaxInt; /*min保存最小值*/
int j = LocateVex(G, vi);
if(S[j] = FALSE && D[j] < min)/*找出最小权值的边*/
{v = vi; min = D[j];}
}
S[v-1] = TRUE; /*将v加入终点集合*/
int k = LocateVex(G, v);
for(vi = 1; vi <= G.vexnum; vi++) /*更新最短路径和直接前驱数组*/
{
int j = LocateVex(G, vi);
if(S[j] = FALSE && (D[k] + G.arcs[k][j] < D[j])
{
D[j] = D[k] + G.arcs[k][j];
Path[j] = k;
}
}
}
}
弗洛伊德算法(顶点对)
求解每一对顶点之间的最短路径有两种方法:
一种是n次调用迪杰斯特拉算法;
另一种是采用下面介绍的弗洛伊德算法。
两种算法的时间复杂度均为O(n³),但弗洛伊德算法的形式更为简洁。
<逻辑思路>
(1)如果说迪杰斯特拉算法是一个“加边”的过程,那么弗洛伊德算法就是一个“加点”的过程;
(2)先确定两个目标顶点,源点为vs,终点为ve;
(3)若vs和ve之间存在弧,初始化为arcs[vs-1][ve-1](编号),反之置为∞;
(4)依次判断剩余顶点vi,若在两个顶点间插入vi后,使得<vs,vi>+<vi,ve>小于<vs,ve>,则置换为vs和ve间的最短路径;
(5)直至所有剩余顶点判断完毕,最短路径确定;
(6)其余的每一对顶点都重复上述过程。
<实现思路>——以邻接矩阵为存储结构的有向网
(1)该算法的核心思想是设置了两个二维辅助数组:
1. 二维数组Path[][]:行表示顶点vi的直接前驱,列表示顶点vi;
2. 二维数组D[]][]:记录vs和ve间的最短路径。
(2)弗洛伊德算法的代码思路比较清晰,插入、比较、更新。
void ShortestPath_Floyd(AMGraph G)
{
/*-----------初始化------------*/
for(int vs = 1; vs <= G.vexnum; vs++) /*源点vs*/
{
int i = LocateVex(G, vs);
for(int ve = 1; ve <= G.vexnum; ve++) /*终点ve*/
{
int j = LocateVex(G, ve);
D[i][j] = G.arcs[i][j];
if(D[i][j] < MaxInt && i != j)
Path[i][j] = i;
else Path[i][j] = -1;
}
/*---------初始化结束--------*/
for(int vi = 1; vi <= G.vexnum; vi++) /*插入点vi*/
for(int vs = 1; vs <= G.vexnum; vs++) /*源点vs*/
for(int ve = 1; ve <= G.vexnum; ve++) /*终点ve*/
{
int k = LocateVex(G, vi);
int i = LocateVex(G, vs);
int j = LocateVex(G, ve);
if(D[i][k] + D[k][j] < D[i][j]) /*更新*/
{
D[i][j] = D[i][k] + D[k][j];
Path[i][j] = k;
}
}
}
路过的圈毛君:“‘拓扑排序’和‘关键路径’的内容可能要先放一放,以后一定会补上的!_(:з」∠)_”