Acwing-基础算法课笔记之搜索与图论(Kruskal算法)
一、Kruskal算法
1、概述
Kruskal算法的原理简单直接,有以下两个基本操作:
(1)对边的长度贪心并加如T中。先对边长排序,一般直接用sort()函数排序,然后依次把最短的边加入T中。
(2)判断圈。每次加入新的边,判断它是否和已经加入T的边形成了圈,也就是判断连通性。用BFS或DFS也能判断,但是最高效的方法是并查集,并查集是Kruskal算法的绝配。
Kruskal算法复杂度分析:
1、主要操作是排序,复杂度为
O
(
m
log
2
m
)
O(m\log_{2}m)
O(mlog2m);
2、用并查集能以
O
(
m
)
O(m)
O(m)的复杂度完成所有新加入边的判圈操作。
以上两者相加,总复杂度仍然为
O
(
m
log
2
m
)
O(m\log_{2}m)
O(mlog2m)。
2、过程模拟
下图是Kruskal算法的执行过程,重点是用并查集判圈:
(1)初始时最小生成树T为空。令S是以点 i 为元素的并查集,开始时,每个点属于独立的集。下面区分了节点 i 和并查集S:
S: | 1 ‾ \underline{1} 1 | 2 ‾ \underline{2} 2 | 3 ‾ \underline{3} 3 | 4 ‾ \underline{4} 4 | 5 ‾ \underline{5} 5 |
---|---|---|---|---|---|
i i i: | 1 1 1 | 2 2 2 | 3 3 3 | 4 4 4 | 5 5 5 |
(2)加入第一条最短边(1-2),T={1-2}。并查集S中,把点2合并到节点1,也就是把点2的集 1 ‾ \underline{1} 1。
S: | 1 ‾ \underline{1} 1 | 1 ‾ \underline{1} 1 | 3 ‾ \underline{3} 3 | 4 ‾ \underline{4} 4 | 5 ‾ \underline{5} 5 |
---|---|---|---|---|---|
i i i: | 1 1 1 | 2 2 2 | 3 3 3 | 4 4 4 | 5 5 5 |
(3)加入第2条边(3-4),T={1-2,3-4}。并查集S中,点4合并到点3。
S: | 1 ‾ \underline{1} 1 | 1 ‾ \underline{1} 1 | 3 ‾ \underline{3} 3 | 3 ‾ \underline{3} 3 | 5 ‾ \underline{5} 5 |
---|---|---|---|---|---|
i i i: | 1 1 1 | 2 2 2 | 3 3 3 | 4 4 4 | 5 5 5 |
(4)加入第3条最短边(2-5),T={1-2,3-4,2-5}。并查集S中,把点5合并到点2,也就是把点5的集 5 ‾ \underline{5} 5改成点2的集 1 ‾ \underline{1} 1。在集 1 ‾ \underline{1} 1中,所有点都指向了根,这样做能避免并查集的长链问题,这是“路径压缩”。
S: | 1 ‾ \underline{1} 1 | 1 ‾ \underline{1} 1 | 3 ‾ \underline{3} 3 | 3 ‾ \underline{3} 3 | 1 ‾ \underline{1} 1 |
---|---|---|---|---|---|
i i i: | 1 1 1 | 2 2 2 | 3 3 3 | 4 4 4 | 5 5 5 |
(5)加入第4条最短边(1-5),检查并查集S,发现点5已经属于集
1
‾
\underline{1}
1,丢弃这条边。这一步实际上是发现了一个圈。并查集的作用就体现在这里。
(6)加入第5条最短边(2-4)。并查集S中,把点4的集并到点2的集。注意这里点4原来属于
3
‾
\underline{3}
3,实际上修改的是把节点3的集
3
‾
\underline{3}
3改成集
1
‾
\underline{1}
1。
S: | 1 ‾ \underline{1} 1 | 1 ‾ \underline{1} 1 | 1 ‾ \underline{1} 1 | 3 ‾ \underline{3} 3 | 1 ‾ \underline{1} 1 |
---|---|---|---|---|---|
i i i: | 1 1 1 | 2 2 2 | 3 3 3 | 4 4 4 | 5 5 5 |
(7)对所有边执行上述操作,直到结束。读者可以练习加最后两条边(3-5)、(4-5),这2条边都会形成圈。
3、Kruskal算法模板
Kruskal算法的编码很简单,不管是存图的数据结构还是算法,都很容易编码。
(1)存图。不需要用邻接矩阵、邻接表、链式前向星等,只用最简单、最省空间的结构体数组存边。
(2)编码。基本上就是并查集操作。
时间复杂度是 O ( m l o g m ) O(mlogm) O(mlogm), n n n表示点数, m m m表示边数
int n, m; // n是点数,m是边数
int p[N]; // 并查集的父节点数组
struct Edge // 存储边
{
int a, b, w;
bool operator< (const Edge &W)const
{
return w < W.w;
}
}edges[M];
int find(int x) // 并查集核心操作
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal()
{
sort(edges, edges + m);
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ )
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b) // 如果两个连通块不连通,则将这两个连通块合并
{
p[a] = b;
res += w;
cnt ++ ;
}
}
if (cnt < n - 1) return INF;
return res;
}