现实生活中的许多问题都可以转化为图来解决。例如,如何以最小成本构建一个通信网络,如何计算地图中两地之间的最短路径,如何为复杂活动中各子任务的完成寻找一个较优的顺序等。
四个常用算法:最小生成树、最短路径、拓扑排序和关键路径。
假设要在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<的步骤(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<