本文已参与「新人创作礼」活动,一起开启掘金创作之路
最小生成树
给定一张边带权的无向图 G = (V,E), n = |V|,m = |E|。由 V 中全部 n 个顶点和 E 中 n-1 条边构成的无向连通子图被称为 G 的一颗生成树。边的权值之和最小的生成树被称为无向图 G 的最小生成树。
定理: 任意一颗最小生成树一定包含无向图中权值最小的边。
证明: 反证法。假设无向图 G = (V,E) 存在一颗最小生成树不包含权值最小的边。设e = (x,y,z)是权值最小的边。把 e 添加到树中, e 会和树上从 x 到 y的路径一起构成一个环,并且环上其他的边的权值都比 z 大。因此,用 e 代替环上的其他任意一条边,会形成一颗权值和更小的生成树,与假设矛盾。故假设不成立,原命题成立。
推论: 给定一张无向图 G = (V,E),n = |V|,m = |E| 。从 E 中选出 k < n-1 条边构成 G 的一个生成森林。若再从剩余的 m-k 条边中选择 n-1 -k 条添加到生成森林中,使其成为G的生成树,并且选出的边的权值之和最小,则该生成树一定包含这m-k 条边中连接生成森林的两个连通节点的权值最小的边。
Kruskal 算法
Kruskal 算法就是基于上述推论的。Kruskal 算法总是维护无向图的最小生成森林。最初,可认为生成森林右零条边构成,每个节点各自构成一颗仅包含一个点的树。
在任意时刻,Kruskal 算法从剩余的边中选出一条值最小的,并且这条边的两个端点属于生成森林中两颗不同的树(不连通),把该边加入生成森林。图中节点的连通可以用并查集维护。
详细来说,Kruskal 算法的流程如下。
- 建立并查集,每个点各自构成一个集合。
- 把所有的边按照权值从小到大进行排序,依次扫描每条边(x,y,z)。
- 若 x,y属于同一集合(连通),则忽略这条边,继续扫描下一条。
- 否则,合并x,y所在的集合,并把 z 累加到答案中。
- 所有边扫描完成后,第4步处理过的边就构成最小生成树。时间复杂度为 O(m log n)。
Kruskal 算法代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 5e5+10;
struct rec{int x,y,z;}edge[N];
int fa[N],n,m,ans;
bool operator <(rec a,rec b) /// 重载这个结构体的排序方式
{
return a.z<b.z;
}
int get(int x)
{
return x == fa[x]?x:fa[x] = get(fa[x]);
}
int main()
{
ios::sync_with_stdio(false); cin.tie(0);
cin>>n>>m;
for(int i=1; i<=m; i++)
{
int x,y,z; cin>>x>>y>>z;
edge[i] = {x,y,z};
}
//按照边权排序
sort(edge+1,edge+1+m);
// 并查集初始化
for(int i=1; i<=n; i++) fa[i] = i;
/// 求最小生成树
for(int i=1; i<=m; i++)
{
int x = get(edge[i].x);
int y = get(edge[i].y);
if(x==y) continue;
fa[x] = y;
ans+=edge[i].z;
}
cout<<ans<<"\n";
return 0;
}
Prim算法
Prim算法同样基于上述推论,但思路略有改变。Prim 算法总是维护最小生成树的一部分。最初,prim 算法仅确定 1号节点属于最小生成树。
在任意时刻,设已经确定属于最小生成树的节点集合为T,剩余节点集合为S。Prim算法找到两个端点分别属于集合S,T的权值最小的边,然后把点 x 从集合 S 中删除,加入到集合T,并把 z 累加到答案中。 具体来说,可以维护数组 d:若x 属于s,则d[x] 表示节点 x 与集合 T 中的节点之间权值最小的边的权值。若 x 属于 T,则 d[x] 就等于 x 被加入 T 时选出的最小边的权值。 可以类比 Dijkstra 算法,用一个数组标记节点是否属于T。每次从未标记的节点中选出d值最小的,把它标记(新加入T),同时扫描出所有边,更新另一个端点的d值。最后,最小生成树的权值综合就是 x = 2~ x = n d[x] 。 prime的算法复杂度为O(n*n) ,可以用二叉堆优化到O(m log n)。但用二叉堆优化不如直接使用 kruskal 算法更加方便。 因此,prime 主要用于稠密图,尤其是完全图的最小生成树的求解。
Prime算法代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 3010;
int a[N][N],d[N],n,m,ans;
bool v[N];
void prim()
{
memset(d,0x3f,sizeof d);
memset(v,0,sizeof v);
d[1] = 0;
for(int i=1; i<n; i++)
{
int x = 0;
for(int j = 1; j<=n; j++)
if(!v[j]&&(x==0||d[j]<d[x])) x = j;
v[x] = 1;
for(int y =1; y<=n; y++)
if(!v[y]) d[y] = min(d[y],a[x][y]);
}
}
int main()
{
ios::sync_with_stdio(false); cin.tie(0);
// 构建邻接矩阵
cin>>n>>m;
memset(a,0x3f,sizeof(a));
for(int i=1; i<=n; i++) a[i][i] = 0;
for(int i=1; i<=m; i++)
{
int x,y,z; cin>>x>>y>>z;
a[x][y] = a[y][x] = min(a[x][y],z);
}
// 求最小生成树
prim();
for(int i=2; i<=n; i++) ans+=d[i];
cout<<ans<<"\n";
return 0;
}