一个 连通图的生成树是一个极小连通子图 ,它含有图中 全部n 个顶点和构成 一棵树的(n- -1) 条边 。此时,如果我们在这棵生成树上添加一条边,必定构成一个环。如果你忘记了什么是连通图,请查看之前的文章 图的存储结构:邻接表和邻接矩阵 。
那么我们该如何生成一棵生成树呢?根据之前介绍的图的遍历方法可知,它通过深度优先遍历得到的生成树称为 深度优先生成树。例如:
同样,由 广度优先遍历得到的生成树称为 广度优先生成树。
由此我们可以看出,一个连通图的生成树不一定是唯一的。了解了深度/广度优先生成树,可是还是不知道我们本问介绍的最小生成树到底是什么,别急,这就给大家说说什么是最小生成树。
一个连通图的生成树不一定是唯一的,那么对于 带权连通图G ( 每 条边上的权均为大于零的 实数 ), 其同样可能 有多棵不同生成树,而每棵生成树的所有边的权值之和可能不同 。我们将其中权 值之和最小的生成树 称为图的最小生成树。
上面我们讨论的生成树都是基于一个图是连通图的前提下进行讨论的,对于连通图仅 需调用遍历过程(DFS 或BFS )一次,从图中任一顶点出发,便可以遍历图中的各 顶点,产生相应的生成树。那么对于非连通图呢? 非连通图 需多次调用遍历过程。这样每个 连通分量中的顶点集和遍历时走过的边一起构成一棵 生成树。所有连通 分量的生成树组成非连通图的生成森林。
我们重点是求带权连通图的最小生成树。其中最重要的两种方法是普里姆算法 和 克鲁斯卡尔算法。下面我们仔细的介绍这两种算法的实现原理以及代码编程。
二普里姆算法普里姆(Prim )算法是一种构造性算法,用于构造最小生成树。其主要的思路是:
首先初始化初始点v,并将其加入到最小生成树顶点集合中,使得U={v} 。并且v 到其他顶点的所有边为候选边;
假设连通图中所有的顶点集合为V,我们重复以下步骤n- -1 次,使得其他n- -1 个顶点被加入到U 中: 从候选边中挑选权值最小的边输出,设该边在剩余顶点集合V -U 中的顶点是k ,将k 加入U 中; 考察当前剩余顶点集合 V-U 中的所有顶点 j , 若(j ,k) 的权值小于原来和顶点j关联的候选边,则用(k ,j) 取代 后者作为候选边 。
下面我们以一个具体的实例说明该算法的具体流程。假设给定一个连通图G如图所示,我们通过普里姆算法求解最小生成树的过程可以表示为:
最终生成的最小生成树结果为:
通过上面的介绍,大家应该对普里姆算法的实现思路有了一定的了解,那么该如何通过代码设计实现该算法呢?在展示代码之前,我们需要先明确以下几个问题:
如何求U 、V-U 两个顶点集之间的 最小 边?(只求一条)
只考虑V- -U 中顶点j 到 U顶点集的最小边(无向图),比较来找最小边,也就是先找局部最优(类似于我们机器学习中的梯度下降算法)。
如何存储顶点j到 U 顶点集的 最小边?
我们需要一个辅助数组closest来记录从U到V-U的最小代价的边。同时,用相应的数组lowcost来记录最小代价边对应的权值。从依附于顶点v或k的各条边中找一条代价最小的边,当找到一条最小代价的边之后,将其作为生成树上的一条边,并将相应的顶点 j 加入到U集合中, 然后修改候选数组中的值,将与顶点j相连接的那条边的权值设置为0,视为加入成功。可视化为:
图采用哪种存储结构更合适?
邻接矩阵
明确了以上问题,我们也就会写普里姆算法的代码了,如下:#define INF 32767 //INF 表示∞void Prim(MGraph g,int v) //v为初始点{ int lowcost[MAXV]; int min; int closest[MAXV], i, j, k; for (i=0;i<g.n;i++) // 给lowcost[] 和closest[] 置初值,如果于v有边相连就初始化为相应边的权值,否则为0 { lowcost[i]=g.edges[v][i]; closest[i]=v;}
该代码片对应的图解为:
for (i=1;i<g.n;i++) // 输出(n-1) 条边 { min=INF; for (j=0;j<g.n;j++) // 在(V-U) 中找出离U最近的顶点k if (lowcost[j]!=0 && lowcost[j]<min) { min=lowcost[j]; k=j; //k 记录最近顶点编号 } printf(" 边(%d,%d) 权为:%d\n",closest[k],k,min); lowcost[k]=0; // 标记k 已经加入U
该代码片对应的图解为:
for (j=0;j<g.n;j++) // 修改数组lowcost 和closest if (lowcost[j]!=0 && g.edges[k][j]<lowcost[j])//lowcost[j]!=0表示仅仅考虑V-U中的顶点 { lowcost[j]=g.edges[k][j]; closest[j]=k; } }}
最后,我们将上面的代码综合到一起:
#define INF 32767 //INF 表示∞void Prim(MGraph g,int v) //v为初始点{ int lowcost[MAXV]; int min; int closest[MAXV], i, j, k; for (i=0;i<g.n;i++) // 给lowcost[] 和closest[] 置初值,如果于v有边相连就 初始化为相应边的权值,否则为0 { lowcost[i]=g.edges[v][i]; closest[i]=v; } for (i=1;i<g.n;i++) // 输出(n-1) 条边 { min=INF; for (j=0;j<g.n;j++) // 在(V-U) 中找出离U最近的顶点k if (lowcost[j]!=0 && lowcost[j]<min) { min=lowcost[j]; k=j; //k 记录最近顶点编号 } printf(" 边(%d,%d) 权为:%d\n",closest[k],k,min); lowcost[k]=0; // 标记k 已经加入U for (j=0;j<g.n;j++) // 修改数组lowcost 和closest,重新选择最小边 if (lowcost[j]!=0 && g.edges[k][j]<lowcost[j])//lowcost[j]!=0表示 仅仅 考虑V-U中的顶点 { lowcost[j]=g.edges[k][j]; closest[j]=k; } }}
总之,Prim() 算法中有两重for 循环,所以时间复杂度为O(n 2 )。普里姆算法的整体思路可以总结为 局部最优 + 调整 = 全局最优。
思考: 为什么说普里姆算法更适合稠密图求最小生成树?
篇幅有限,我们下次再介绍克鲁斯卡尔算法求最小生成树的方法。end
点击“❀在看”,让更多朋友们看到吧~