Kruscal 算法(C++)
Kruscal 算法(C++)
1. Kruscal 算法简介
Kruscal 最小生成树算法起始于一个空的图,并按照以下规则从边集中选择边。
不断重复地选择未被选中的边中权重最轻且不会形成环的一条。
Kruscal 算法通过逐条增加边来构造最小生成树。在保证不出现环的同时,它总简单地选择当前所余的权重最轻的边。这是一个典型的贪心算法,即每次决策都对应于最明显的即时利益。
下面是《算法概论》一书中给出的伪代码。
2. 代码部分
下面将伪代码改写成 C++ 程序。
-
确定图的表示方式
这里我选择建立一个结构体表示边的信息,u、v、w 分别表示边的 2 个端点和边权。
struct Edge { int u, v, w; }; Edge graph[MAXN];
-
编写 _find 和 _union
有两种方法,分别是基于等级的合并和路径压缩,这里先把它看做黑箱,接下来再详细阐述。
但这两种方法都要使用 Rank 数组,下面先给出一些要用到的全局变量以及初始化 Rank 的 makeset 函数。
int n, m; // n个结点,m条无向边 int parent[MAXN], Rank[MAXN]; // 基于等级的合并 int min_weight = 0; // 最小生成树的权值 int cnt = 0; // 统计最小生成树的边数,判断是否连通 void makeset(int x) { parent[x] = x; Rank[x] = 0; }
-
Kruscal 核心代码——参照伪代码写出
void kruskal() { for (int i = 0; i < n; ++i) { makeset(i); // 初始化 } sort(graph, graph + m, [](const Edge &a, const Edge &b) -> bool { return a.w < b.w; }); // 这里使用C++11的lambda,也可以写一个cmp函数 for (int i = 0; i < m; ++i) { int x = _find(graph[i].u), y = _find(graph[i].v); if (x != y) // 第i条边的两个端点u、v属于不同集合 { min_weight += graph[i].w; _union(x, y); ++cnt; } //if (cnt == n-1) break; // 选了n-1条边就结束了,也可以不加,因为剩下的边的顶点肯 // 定都在最小生成树内了,接下来的循环中if总是判断为假。 } }
3. 并查集的编写方式
3.1 基于等级的合并
存储集合的方法之一是采用有向树,树的结点对应集合中的元素,每个结点都包含一个父指针(使用parent数组实现)。父指针使得结点一级级相连并最终指向树的根,我们用树根元素来代表整个集合。
与其他元素不同,树根的父指针指向该元素自身,所以我们使用 makeset 初始化时将每个元素的 parent 初始化为自身。对于节点的等级信息,我们将其解释为其下悬挂的子树的高度。
void makeset(int x)
{
parent[x] = x;
Rank[x] = 0;
}
find 函数的实现很简单,只需要沿着节点的父指针找到树的根。_find 的执行时间和树的高度成正比。
int _find(int x)
{
while (x != parent[x])
x = parent[x];
return x;
}
合并两个树(集合)的过程也很简单,只需要将一个树的根(的父指针)指向另一个树的根。由于树的高度影响计算效率,我们应该尽可能使树的高度小一点。因此,我们选择让较低的树的根指向较高的树的根,这样一来,除非将要合并的树等高,否则不会使合并后的树总高度增加。
void _union(int x, int y)
{
int rx = _find(x);
int ry = _find(y);
if (rx == ry)
return; // x和y的根相同,已经在同一个集合中
if (Rank[rx] > Rank[ry])
{
parent[ry] = rx; // 让较低的树根指向较高的树根
}
else
{
parent[rx] = ry;
if (Rank[rx] == Rank[ry]) // 两个树等高的情形
++Rank[ry];
}
}
下面是合并的具体过程。
3.2 路径压缩
由上文易知,find 操作的时间复杂度为 O(logh),h 为树高,采用路径压缩可以将总的代价分摊,由 O(logh) 下降到略微超过 O(1)。在每次 find 操作中,当沿着一系列的父指针找到树根后,我们可将这些父指针直接指向树根。
int _find(int x)
{
if (x != parent[x]) parent[x] = _find(parent[x]);
return parent[x];
}
4. 测试
测试题目,洛谷 P3366 【模板】最小生成树
#include <iostream>
#include <algorithm>
using namespace std;
#define MAXN 200001
int n, m; // n个结点,m条无向边
int parent[MAXN], Rank[MAXN];
int min_weight = 0; // 最小生成树的权值
int cnt = 0; // 统计最小生成树的边数,判断是否连通
struct Edge
{
int u;
int v;
int w;
};
Edge graph[MAXN];
void makeset(int x)
{
parent[x] = x;
Rank[x] = 0;
}
int _find(int x)
{
while (x != parent[x])
x = parent[x];
return x;
}
void _union(int x, int y)
{
int rx = _find(x);
int ry = _find(y);
if (rx == ry)
return; // x和y的根相同,已经在同一个集合中
if (Rank[rx] > Rank[ry])
{
parent[ry] = rx; // 让较低的树根指向较高的树根
}
else
{
parent[rx] = ry;
if (Rank[rx] == Rank[ry]) // 两个树等高的情形
++Rank[ry];
}
}
void kruskal()
{
for (int i = 0; i < n; ++i)
{
makeset(i); // 初始化
}
sort(graph, graph + m, [](const Edge &a, const Edge &b) -> bool
{ return a.w < b.w; });
for (int i = 0; i < m; ++i)
{
int x = _find(graph[i].u), y = _find(graph[i].v);
if (x != y) // 第i条边的两个端点u、v属于不同集合
{
min_weight += graph[i].w;
_union(x, y);
++cnt;
}
//if (cnt == n-1) break; // 选了n-1条边就结束了,也可以不加,因为剩下的边的顶点肯
// 定都在最小生成树内了,接下来的循环中if总是判断为假
}
}
int main()
{
cin >> n >> m;
for (int i = 0; i < m; ++i)
{
cin >> graph[i].u >> graph[i].v >> graph[i].w;
}
kruskal();
if (cnt != n - 1)
cout << "orz" << endl;
else
cout << min_weight << endl;
}