数据结构 | 第6章 最小支撑树

156 阅读3分钟

6.10 最小支撑树

支撑树:

如图6.18所示,连通图G的某一无环连通子图T若覆盖G中所有的顶点,则称作G的一棵支撑树或生成树(spanning tree)。

image-20220714195856616.png

就保留原图中边的数目而言,支撑树既是“禁止环路”前提下的极大子图,也是“保持连通”前提下的最小子图。

在实际应用中,原图往往对应于由一组可能相互联接(边)的成员(顶点)构成的系统,而支撑树则对应于该系统最经济的联接方案。尽管同一幅图可能有多棵支撑树,但由于其中的顶点总数均为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不含边而仅含单个顶点,故可从原图的顶点中任意选取。

实例:

image-20220715104040359.png

image-20220715104102347.png

最终如图(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算法的时间复杂度O(n2)O(n^2)

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 12 天,点击查看活动详情