链接
Kruskal算法 ,也是一种寻找无向图中最小生成树的算法。
1.基本思想:
维持一个森林,森林是很多树的集合。
- 初始的时候,一共有n棵树,每个节点是一棵树。初始的时候,森林里没有边。
- 每一轮循环会检查一条边。如果这条边满足某些性质,那么就选中这条边。让两棵树合并起来。每合并一次会减少一棵树。
- 当只剩下一棵树的时候,终止循环。
- 每一次循环研究一条边。所以最终循环次数最多不会超过图中边的数量。
2.实例:
算法的输入是图,如下,7
个节点和12
条边:
创建一个队列queue,里面存储所有的边edge,所有的边都按照权重weight做排序,权重小的在上面,权重大的在下面:
用集合T
表示被选中的边。
-
初始的时候:
集合T
是空集,没有边。当前一共有7棵树,每个节点都是一棵树,这些树里面一条边也没有。
每一次循环研究一条边。 -
第1轮循环:
- 取队列顶端的元素,边 e 1 , 4 \color{red}e_{1,4} e1,4权重为1,在图中找到这条边;
- 判断这条边
e
1
,
4
\color{red}e_{1,4}
e1,4的两个节点
v1
和v4
是否在同一棵树里面:
节点v1
和v4
之间没有红色路径,所以节点v1
和v4
不在同一棵树里面,就接受节点v1
和v4
之间的这条边 e 1 , 4 \color{red}e_{1,4} e1,4,在图中用红色标出这条边 e 1 , 4 \color{red}e_{1,4} e1,4,并且把这条边 e 1 , 4 \color{red}e_{1,4} e1,4放进集合T
;
- 第2轮循环:
- 取队列顶端的元素,边 e 6 , 7 \color{red}e_{6,7} e6,7权重为1,在图中找到这条边;
- 判断这条边
e
6
,
7
\color{red}e_{6,7}
e6,7的两个节点
v6
和v7
是否在同一棵树里面:
节点v6
和v7
之间没有红色路径,所以节点v6
和v7
不在同一棵树里面,就接受节点v6
和v7
之间的这条边 e 6 , 7 \color{red}e_{6,7} e6,7,在图中用红色标出这条边 e 6 , 7 \color{red}e_{6,7} e6,7,并且把这条边 e 6 , 7 \color{red}e_{6,7} e6,7放进集合T
;
- 第3轮循环:
- 取队列顶端的元素,边 e 1 , 2 \color{red}e_{1,2} e1,2权重为2,在图中找到这条边;
- 判断这条边
e
1
,
2
\color{red}e_{1,2}
e1,2的两个节点
v1
和v2
是否在同一棵树里面:
节点v1
和v2
之间没有红色路径,所以节点v1
和v2
不在同一棵树里面,就接受节点v1
和v2
之间的这条边 e 1 , 2 \color{red}e_{1,2} e1,2,在图中用红色标出这条边 e 1 , 2 \color{red}e_{1,2} e1,2,并且把这条边 e 1 , 2 \color{red}e_{1,2} e1,2放进集合T
;
- 第4轮循环:
- 取队列顶端的元素,边 e 3 , 4 \color{red}e_{3,4} e3,4权重为2,在图中找到这条边;
- 判断这条边
e
3
,
4
\color{red}e_{3,4}
e3,4的两个节点
v3
和v4
是否在同一棵树里面:
节点v3
和v4
之间没有红色路径,所以节点v3
和v4
不在同一棵树里面,就接受节点v3
和v4
之间的这条边 e 3 , 4 \color{red}e_{3,4} e3,4,在图中用红色标出这条边 e 3 , 4 \color{red}e_{3,4} e3,4,并且把这条边 e 3 , 4 \color{red}e_{3,4} e3,4放进集合T
;
- 第5轮循环:
- 取队列顶端的元素,边 e 2 , 4 \color{red}e_{2,4} e2,4权重为3,在图中找到这条边;
- 判断这条边
e
2
,
4
\color{red}e_{2,4}
e2,4的两个节点
v2
和v4
是否在同一棵树里面:
节点v2
和v4
之间有红色路径可以到达,所以节点v2
和v4
在同一棵树里面,拒绝接受节点v2
和v4
之间的这条边 e 2 , 4 \color{red}e_{2,4} e2,4,否则会创造出一条回路;
所以本次循环并没有增加边和节点,如下:
- 同理,第6次循环,第7次循环,第8次循环,第9次循环,最终得到,如下:
上面的图,有7个节点,6条边,所有节点都是连通的,任意两条节点之间都有路径,因此终止程序,返回集合T
。
3.Kruskal算法总结如下:
4.How to maintain the forest?
我们需要一种数据结构来维持很多棵树,每一轮循环都需要判断两个节点是否属于相同的树,有时候还需要合并两棵树。
4.1 判断两个节点是否属于相同的树
有很多算法都可以判断两个节点之间是否存在一条红色的路径,但是寻找路径的算法都很慢。
最好的办法是使用并查集 - Disjoint Sets
。
下面是一个无向图,可以看出,该图有三棵树:
把同一棵树的节点都放在同一个集合里面,当前有三棵树,就用三个集合来存三棵树的节点,如下:
使用并查集
的话,可以快速的判断两个节点是否在同一个集合里面。做一次判断的时间复杂度非常低,接近常数时间,时间复杂度为O(1).
4.2 如何合并两棵树?
举个例子:
如果算法接受了节点v3
与v4
之间的这条边,那么我们就需要把这条边连接的两棵树合并起来,只需要对紫色和绿色部这两个集合做Union
操作。
用并查集
的好处在于可以快速合并集合。Union
操作之后,两棵树就合并成了一棵树。
Union
操作的时间复杂度接近常数时间,时间复杂度为O(1).
5. Kruskal算法 时间复杂度
该算法的时间复杂度为 O ( m ∗ l o g m ) \color{red}O(m*log{ m}) O(m∗logm),其中,m是边的数量。
-
算法一开始需要对图中的边做排序,排序需要时间 O ( m ∗ l o g m ) \color{red}O(m*log{ m}) O(m∗logm);
-
然后开始做循环,每一轮循环都会从队列里取出一条边,所以循环的次数不会超过边的总数m;
-
每一轮循环的时间复杂度都接近常数, O ( 1 ) \color{red}O(1) O(1),实际上是比常数稍微高一点,但是小于对数。
每一轮循环的代价主要有两个来源:- 1.一个代价是判断两个节点是不是属于同一棵树;
- 2.另一个代价是合并两棵树。
这两种操作的时间复杂度都接近常数。
综上所述:算法的时间复杂度是: O ( m ∗ l o g m ) + m ∗ O ( 1 ) \color{red}O(m*log{ m})+m*O(1) O(m∗logm)+m∗O(1),也就是 O ( m ∗ l o g m ) \color{red}O(m*log{ m}) O(m∗logm)。