初衷:
最近在看算法相关的东西,看到贪心法解决mst的问题,可惜树上讲解的不是很清新,到网上找了很多资料讲解的也不透彻
只是随便带过就草草了事、这几天抽空看了下,总算基本思路理清楚了
主要还是得感谢强大的google,帮我找到一个很好的英文资料。(下面有链接,有兴趣的同学可以看看)
理顺了思路,就和大家分享下~希望对学习贪心法的同学会有所帮助。
这篇博客的主要内容是贪心法求解Minimum Spanning Tree (MST)(最小生成树)的问题
贪心法求解最小生成树常用的有两种算法,分别是Prim’s MST algorithm和Kruskal's MST algorithm(prim算法和kruskal算法)
这里主要说的是kruskal算法
最小生成树的简单定义:
给定一股无向联通带权图G(V,E).E 中的每一条边(v,w)权值位C(v,w)。如果G的子图G'是一个包含G中所有定点的子图,那么G'称为G的生成树,如果G'的边的权值最小
那么G'称为G的最小生成树。
kruskal算法的基本思想:
1.首先将G的n个顶点看成n个孤立的连通分支(n个孤立点)并将所有的边按权从小大排序。
2.按照边权值递增顺序,如果加入边后存在圈则这条边不加,直到形成连通图
对2的解释:如果加入边的两个端点位于不同的连通支,那么这条边可以顺利加入而不会形成圈
本例中用到的图:
权值递增排序:
kruskal加边后情况:
所以对于任意边(u,v)要判断这两个点是不是存在于同一个连通支里。
如果是,则舍弃这条边,接着判断另一条边
如果不是,则把这条边加入到图中,并且把u,v属于的连通支合并
然后操作下一条边
这个算法执行的过程就是按照规定一个个连通支合并的过程,使最后只剩一个连通支。
What kind of data structure supports such operations?
这是一个值得思考的问题、、、逛社区的时候,有用链表的、二维数组的、、这里不讨论这些存储结构的可行性
这里要讨论的是有向树的存储
some implementation details(基本操作)
makeset(x): create a singleton set containing just x //初始化的时候把整个图分为n个独立连通块
find(x): to which set does x belong? //对于任意给定点x,判断x属于哪一个连通块
union(x, y): merge the sets containing x and y //合并两个连通块其中,x,y为某边的两个端点,如果通过上面的find操作属于不同的连通块才把他们合并
Algorithm(算法实现) :
Kruskal(G)
makeset(u); //初始化,让每个点成为独立的连通块
2. X={Æ};
3. Sort the edges E by weight; //按边的权值大小排序
4. For all edges (u, v) ∈ E in increasing order of weight do //对于条边e(u,v)(权值递增顺序)判断能否加入到图中
if find(u) ≠find(v) then //如两个端点不在同一个连通块,则合并这两个连通块
add edge (u, v) to X;
union(u, v);
下面是算法中的实现细节
How to store a set? (如何存储连通块)
例子;
{B, E}
{A, C, D, F, G, H}
对于每一个联通块,还有两个需要保存的,也就是树的根节点rank和树高height
Root: its parent pointer points to itself.
Rank: the height of subtree hanging from that node.
还有一个会用到的关系,对于树中的点x,p(x)表示x的父节点
下面是函数实现
Makeset(x)
2.Rank(x)=0;
Find(x)
x=P(x);
2. return(x);
执行上述操作后的实例:
After makeset(A), makeset(B), …, makeset(G).(执行makeset后)
每个点成为了孤立的连通支,右上角的数字代表树的rank
After union(A,D), union(B,E), union(C, F).(合并AD,BE,CF后)
After union(C,G), union(E,A).(合并CG,EA后)
注意看新的连通支右上角的rank有变化,合并的过程中尽量使得rank达到最小
After union(B,G).
关于Rank的几点说明:
Property 1: For any x, rank(x) < rank(P(x)). 对于任意x,x的rank小于他的父节点的rank
Property 2: Any root node of rank k has at least 2k nodes in its tree. 任何rank 为k 的连通支至少有2k个节点
Property 3: If there are n elements overall, there can be at most n/2k nodes of rank k. 如果一共有n个节点,那么rank 为k的连通支一共有n/2k个
对property2的解释:因为union的原则是让union后的树rank最小,所以union后的树至少是二叉树,也就是说除叶子节点外的节点至少有两个孩子。
对property3的解释:因为rank为k 的树至少有2k 个节点,所以最多有n/2k个
算法效率分析:
Kruskal(G)
makeset(u); //初始化,让每个点成为独立的连通块
2. X={Æ};
3. Sort the edges E by weight; //按边的权值大小排序
4. For all edges (u, v) ∈ E in increasing order of weight do //对于条边e(u,v)(权值递增顺序)判断能否加入到图中
if find(u) ≠find(v) then //如两个端点不在同一个连通块,则合并这两个连通块
add edge (u, v) to X;
union(u, v);
上面的算法中
makeset():可以在常数时间内完成
sort edges :对边的权值进行排序的效率O(|E|log|V|) (排序算法的时间效率、自己google不啰嗦)
find():由给定的点往上查找,直到树根为止所花时间为树的高度即log|V|。
注意:如何确定find()执行的次数是一个值得考虑的问题
如果从点的角度,是很难得到准确答案的,因为每个对于某一个点,和他相连的点是不确定的, 即不通过的点情况不同,要逐一考虑岂不很麻烦
其实find()执行的次数是和边数紧密相连的。请看算法中,循环的体是依据边的权值顺序展开的。而对于每一条我们考虑的边,都要考虑它连接的两个点
所以,find()的执行次数就是边数的两倍。执行一个finad()的效率是log|V|,而union基本可在常数时间内完成
所以
Union and Find operations: O(|E|log|V|)
其实这个算法的思想很简单、每一次选最小的,如果符合条件就把它加入到我们的结果中,如果不满足条件,则选下一个最小的
只是实现起来考虑的需要多一些、比如用何种数据结构存储、判断两个点是不是属于同一个连通支等等
可见,贪心法只是提供一种解决的基本思路,要真正解决问题还要考虑如何实现、这也是很关键的一点。
如果你看到这里,可能会发现这个算法的效率不是很可观、在最后的union and find 中所用的时间和点的数量n有很大的关系。
如果n很大的话,就会花很多时间。
那么、这个算法的效率可以提高吗?
答案是肯定的。用到的技术为
Amortized Analysis 平摊分析(也叫摊销分析),这可以说是一种很神奇的思想,先透露一下吧
对于这个问题的union and find 操作,本文中的效率为O(|E|log|V|)。Amortized Analysis后,可以让复杂度为:O(|E|Log*n)
Log*n是个什么东西呢?当n为宇宙中所有物质的数量的时候,Log*n<=8
也就是让上文中的log(n)的最大值降到8.
如何?是不是很客观的效率提升。。。。
有兴趣的同学可以关注我的下一篇博客、会针对本文的问题详细解释Amortized Analysis !!
参考资料:
http://en.wikipedia.org/wiki/Minimum_spanning_tree
http://www.cs.berkeley.edu/~vazirani/algorithms/chap5.pdf
如果有不对的地方、希望各位能指出。