本文来自我的博客 最小生成树 Minimum Spanning Tree - Snow’s Blog (ivansnow02.github.io)
最小生成树 Minimum Spanning Tree
加权无向图的最小生成树(Minimum Spanning Tree,简称MST)是一棵生成树,其权(所有边的权值之和)不会大于其它任何生成树的权。
一个带权连通图G(假定每条边上的权值均大于零)可能有多棵生成树.
每棵生成树中所有边上的权值之和可能不同。
其中边上的权值之和最小的生成树称为图的最小生成树。
MST算法有很多,但其中最知名的是Prim算法和Kruskal算法
Prim算法
基本思路
从任何一个顶点开始作一棵单顶点MST,再为之增加V-1条边,每次增加的边都是将MST上的一个顶点和尚未在此MST上的一个顶点相连接的最小边。
假设G=(V,E)是一个具有n个顶点的带权连通图,T=(U,TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,则由G构造从起始点v出发的最小生成树T的步骤如下:
- 初始化U={v}。以v到其他顶点的所有边为候选边。
- 重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
- 从候选边中挑选权值最小的边加入TE(所有候选边一定是连接两个顶点集U和V-U的边),设该边在V-U中的顶点是k,将顶点k加入U中。
- 考察当前V-U中的所有顶点j,修改候选边:若(k,j)的权值小于原来和顶点j关联的候选边,则用(k,j)取代后者作为候选边。
寻找最小边在这里使用小根堆
基于堆的Prim算法代码
struct Edge {
int from, to, cost;
Edge(int u, int v, int w):from(u), to(v), cost(w) {}
};
struct HeapNode {
int d, u; // d表示点u到MST的距离
bool operator< (const HeapNode& rhs) const {
return d > rhs.d; // 小根堆
}
};
vector<Edge> edges; // edges存所有边
vector<int> G[MAXN]; // G[i]是顶点i发出的所有边
bool done[MAXN]; // i是否已经加入了MST
int d[MAXN]; // 每个点到MST的最小距离
int Prim() {
priority_queue<HeapNode> Q;
int ans = 0; // 总权值
for (int i = 0; i < V; ++i) d[i] = INF; // V是图的节点数
d[0] = 0; // 从点0开始
memset(done, 0, sizeof(done));
Q.push((HeapNode){0, 0});
while (!Q.empty()) {
HeapNode x = Q.top(); Q.pop;
int u = x.u; // u为队头元素的序号
if (done[u]) continue; // 已经在MST中就跳过
ans += x.d; // 加上出队的队头元素到MST的距离
done[u] = true;
for (int i = 0; i < G[u].size; ++i) { // 更新最小生成树的权值
Edge& e = edges[G[u][i]]; // 顶点to能修改最小距离
if (d[e.to] > e.cost) {
d[e.to] = e.cost;
Q.push((HeapNode){d[e.to], e.to});
}
}
}
return ans;
}
注意
本段代码用邻接表来存图,输入可以使用
for (int i = 0; i < E; ++i) { // E是边数
int u, v, cost;
cin >> u >> v >> cost; // 输入边的起点、终点和权值
G[u].push_back(i); // 存储边的索引
edges.push_back(Edge(u, v, cost));
G[v].push_back(i); // 存储边的索引
edges.push_back(Edge(v, u, cost));
}
分析
Prim时间复杂度为 O ( V l o g V + E l o g V ) O(VlogV + ElogV) O(VlogV+ElogV)。在稀疏图的情况下,E的数量通常远小于 V 2 V^2 V2,因此可以将时间复杂度近似为 O ( E l o g V ) O(ElogV) O(ElogV)。而在稠密图的情况下,E的数量接近 V 2 V^2 V2,时间复杂度会接近 O ( V 2 l o g V ) O(V^2logV) O(V2logV)。
Kruskal算法
基本思路
以边的长度(从小到大)为顺序来处理,若一条边与前面加入到MST中的边未形成环,则将这样的边加入到MST中,增加了V-1条边后停止。也就是说开始是一个森林,每个顶点就是一棵独立的树,然后逐渐把这些树合并(通过一条最小边),最后形成的一棵树就是MST。
假设G=(V,E)是一个具有n个顶点的带权连通图,T=(U,TE)是G的最小生成树,则构造最小生成树的步骤如下:
- 置U的初值等于V(即包含有G中的全部顶点),TE的初值为空集(即图T中每一个顶点都构成一个分量)。
- 将图G中的边按权值从小到大的顺序依次选取:若选取的边未使生成树T形成回路,则加入TE;否则舍弃,直到TE中包含n-1条边为止。
如果把一棵树看成一个集合,那么对于新加入的边,要判断它的两个顶点是否已经在同一个集合了?是的话就跳过,处理下一条边;不是的话就把这条边加入到MST,同时把该边两顶点所处的两个集合合并。这个实际就是不相交集合(Disjoint Set)的并查(Union-Find)操作。
Kruskal算法代码
struct edge {
int u, v, cost;
};
bool cmp(const edge& e1, const edge& e2) {
return e1.cost < e2.cost;
}
edge es[MAX_E]; // 存储边的信息
int par[MAX_V]; // 存储父节点
// 初始化并查集
void init_union_find(int V) {
for (int i = 0; i < V; ++i) {
par[i] = i; // 初始时每个节点的父节点是自身
}
}
// 查找根节点
int find(int x) {
if (par[x] == x) {
return x; // 根节点的父节点是自身
} else {
return par[x] = find(par[x]); // 路径压缩,将x的父节点设为根节点,加速后续查找
}
}
// 合并集合
void unite(int x, int y) {
x = find(x); // 查找x的根节点
y = find(y); // 查找y的根节点
if (x != y) {
par[x] = y; // 将x的根节点设为y,合并两个集合
}
}
// 判断两个节点是否属于同一个集合
bool same(int x, int y) {
return find(x) == find(y); // 若两个节点的根节点相同,则属于同一个集合
}
int Kruskal(int V, int E) {
sort(es, es + E, cmp); // 按权值从小到大排序
init_union_find(V); // 并查集初始化
int res = 0;
for (int i = 0; i < E; ++i) {
edge e = es[i];
if (!same(e.u, e.v)) { // u和v不属于一个集合
unite(e.u, e.v); // 合并u和v集合的元素
res += e.cost;
}
}
return res;
}
分析
Kruskal算法的时间复杂度为 O ( E l o g E + E α ( V ) ) O(ElogE + Eα(V)) O(ElogE+Eα(V))。在稀疏图的情况下,E的数量通常远小于 V 2 V^2 V2,因此可以将时间复杂度近似为 O ( E l o g E ) O(ElogE) O(ElogE)。而在稠密图的情况下,E的数量接近 V 2 V^2 V2,时间复杂度会接近 O ( E α ( V ) ) O(Eα(V)) O(Eα(V))。