这篇介绍的是最小支撑树,常见的有Prim算法和Krustal算法。
支撑树:连通图G的某一无环连通子图T若覆盖G中所有的顶点,则称作G的一颗支撑树或生成树(spanning tree)。
支撑树必须覆盖所有的顶点,并且不能有环路,因此是禁止环路前提下的极大子图,也是保持通路前提下的最小子图。一个图可能有很多支撑树,它们都包含n个顶点和n-1条边。
最小支撑树:在带权网络G所有的支撑树中,成本最低的称为最小支撑树(MST)。
Prim算法
割(cut):在图G=(V;E)中,顶点集V的任一平凡子集U及其补集V\U构成一个割。如果边uv满足u属于U且v不属于U,称作是该割的一条跨越边(cross)。跨边联接于V及其补集之间,也称作该割的一座桥。
Prim算法迭代的准则:最小支撑树总是会采用联接每一割的最短跨越边。
算法的策略是贪心迭代:从一个点出发,每次都寻找已经得到的支撑树子树T'与剩余的点构成的割的最短跨越边,并把它加入支撑树。算法复杂度为O(n^2),适合于稠密图、
考虑算法的过程,我们可以在发现一个顶点时,把它的优先级设置为与子树T’的联边的权重。实际上,也就是把顶点的优先级设置为跨越边的权重,然后寻找那个最短的跨越边,加入到子树中。因此,这个问题也可以归于优先级搜索的框架:
1 template<typename Tv, typename Te> struct PrimPU 2 { 3 virtual void operator()(Graph<Tv, Te>* g, int uk, int v) 4 { 5 if (g->status(v) == UNDISCOVERED)//对于uk每个尚未被发现的邻接顶点v 6 if (g->priority(v) > g->weight(uk, v)) 7 { 8 g->priority(v) = g->weight(uk, v);//更新优先级数 9 g->parent(v) = uk;//更新父节点 10 } 11 } 12 };
只需要重写优先级更新器即可。
Krustal算法
前面的prim算法是以每一割的最短跨越边来扩展支撑树,通过更新点的优先级可以选取点和树边。
如果不考虑动态边的情况,而是直接选择边的长度,似乎可以更加直观一点。这也是krustal算法的思想。
把图中所有的边,按照权重进行排序,然后从最短的边开始,把边和它的两个端点u和v加入到支撑树中。然后按照边的顺序,继续选择最短边,检查两个端点是否已经在支撑树中,如果两个点都在,那么舍弃这条边继续进行;只有一个端点在,把这条边和新增的顶点加入支撑树,此时可能会是两个子树的合并操作;两个端点都不在,成为一个新的子树。知道已经有n-1条边时算法结束。
krustal算法在某些时候的效率可能会很高,但有时效率可能会很低,需要检查完所有的边才能结束算法,比如其他点之间的边权重很小,而某一点只与很少的点有联边且联边权重很大的情况。
另外,krustal算法要求图可以提供两种基本操作:一种是并,即把两个支撑树子树合并起来;另一个是查,检查一个顶点是否在子树中。支持这样的操作,称为并查集。
算法时间复杂度为O(eloge)。复杂度与边的数量有关,因此,该算法适合边比较稀疏的图。
我没有想到能把krustal纳入到优先级搜索框架中的方法,因为krustal算法需要实现并查集的操作。可以考虑用这样的结构来实现:把所有的顶点放在一个向量结构中,子集用多叉树表示;当采用一条边的时候,更新一个顶点的parent指针;在选择边的过程中,需要在各个子树中进行查找操作;合并的操作即相当于两棵树的合并;最终的结果必然是得到了一个顶点作为根节点的多叉树结构。
总结来看,两种算法的最大不同之处,在于krustal对边采取了排序操作,而prim算法没有进行预处理。