6.10 最小支撑树
支撑树:
如图6.18所示,连通图G的某一无环连通子图T若覆盖G中所有的顶点,则称作G的一棵支撑树或生成树(spanning tree)。
就保留原图中边的数目而言,支撑树既是“禁止环路”前提下的极大子图,也是“保持连通”前提下的最小子图。
在实际应用中,原图往往对应于由一组可能相互联接(边)的成员(顶点)构成的系统,而支撑树则对应于该系统最经济的联接方案。尽管同一幅图可能有多棵支撑树,但由于其中的顶点总数均为n,故其采用的边数也均为n - 1。
最小支撑树:
若图G为一带权网络,则每一棵支撑树的成本(cost)即为其所采用各边权重的总和。在G的所有支撑树中,成本最低者称作最小支撑树(minimum spanning tree, MST)。
蛮力算法:
时间复杂度O(nn−2)需要优化。
事实上基于PFS搜索框架,并采用适当的顶点优先级更新策略,即可得出如下高效的最小支撑树算法。
Prim算法:
Prim算法的正确性基于以下事实:最小支撑树总是会采用联接每一割的最短跨越边。
贪心迭代:
由以上性质,可基于贪心策略导出一个迭代式算法。每一步迭代之前,假设已经得到最小支撑树T的一棵子树Tk = (Vk; Ek),其中Vk包含k个顶点,Ek包含k - 1条边。于是,若将Vk及其补集视作原图的一个割,则在找到该割的最短跨越边ek = (vk, uk)(vk∈Vk且uk∉Vk)之后,即可将Tk扩展为一棵更大的子树Tk+1 = (Vk+1; Ek+1),其中Vk+1 = Vk ∪ {uk},Ek+1 = Ek ∪ {ek}。最初的T1不含边而仅含单个顶点,故可从原图的顶点中任意选取。
实例:
最终如图(i)所示得到:T8 = ({A, B, D, G, C, E, F, H}; {AB, AD, DG, DC, CE, CF, FH})
实现:
方法:每次由Tk扩充至Tk+1时,可以将Vk之外每个顶点u到Vk的距离视作u的优先级数。如此,每一最短跨越边ek对应的顶点uk都会因拥有最小的优先级数(即最高的优先级)而自然地被选中。
顶点优先级更新器思路:uk和ek加入Tk之后,应如何快速更新Vk+1以外顶点的优先级数呢?实际上,与uk互不关联的顶点都无需考虑,故只需遍历uk的每一邻居v,若边ukv的权重小于v当前的优先级数,则将后者更新为前者。
0001 template <typename Tv, typename Te> struct PrimPU { //针对Prim算法的顶点优先级更新器
0002 virtual void operator() ( Graph<Tv, Te>* g, Rank v, Rank u ) {
0003 if ( UNDISCOVERED == g->status ( u ) ) //对于v每一尚未被发现的邻接顶点u
0004 if ( g->priority ( u ) > g->weight ( v, u ) ) { //按Prim策略做松弛
0005 g->priority ( u ) = g->weight ( v, u ); //更新优先级(数)
0006 g->parent ( u ) = v; //更新父节点
0007 }
0008 }
0009 };
复杂度:
顶点优先级更新器只需常数运行时间,由6.10.3节分析结论,Prim算法的时间复杂度
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 12 天,点击查看活动详情”