浅谈最小生成树
———(
m BiuBiu\_Miku)
1.一些概念
· 树:在一个图中,满足边数等于点数减一的条件。(如图1所示)
· 生成树:在一个连通图中,截取一个子图,此子图满足树的性质,且通过每一个节点的树称为生成树。(如图2所示)
· 最小生成树:在一个包含 (n) 个节点的加权连通图中,所有边的边权之和最小的树,且通过每一个节点的树,即为最小生成树。(如图3所示)
2.例题引入(题皆出自洛谷)
【模板】最小生成树
题目描述
给出一个无向图,求出最小生成树,如果该图不连通,则输出 (orz)。
输入格式
第一行包含两个整数 (N,M),表示该图共有 (N) 个结点和 (M) 条无向边。
接下来 (M) 行每行包含三个整数 (X_i,Y_i,Z_i) 表示有一条长度为 (Z_i) 的无向边连接结点 (X_i,Y_i) 。
输出格式
如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 (orz)。
输入输出样例
输入
4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3
输出
7
3.算法实现
关于算法的实现一般有两种算法,第一种称为 (Kruskal) ,第二种称为 (Prim)
实现方法一.(Kruskal) 时间复杂度 (O( MlogM )) ((M)为边数)
(Kruskal) 是一种利用并查集来实现最小生成树的办法,其算法流程大致为。
先将读进来的边按照边权排序,然后利用并查集建立关系,一但发现如果两个点不存在关系,那么就建这条边,不然的话就不建,最后把建了边的边权统计起来就好了。
为什么这样就可以找到最小生成树呢?因为我们已经将边权排序过了,也就是说越前面的边他的边权就越小,因此我们就可以直接通过简单的步骤得到最小生成树了!
以上面的题目的样例来做例子,来模拟一下该算法全过程:
1.排序,排序后得到以下的数据
1 2 2
1 3 2
1 4 3
3 4 3
2 3 4
2.发现 (1) 到 (2) 不连通,于是建边,如下图所示:
3.发现 (1) 到 (3) 不连通,于是建边,如下图所示:
4.发现 (1) 到 (4) 不连通,于是建边,如下图所示:
5.发现 (3) 到 (4) 已经连通,于是不建边。
6.发现 (2) 到 (3) 已经联通,于是不建边。
7.经统计,一共建的边的边权之和为 (7) 于是就输出 (7) 。
(Code:)
#include<bits/stdc++.h>
using namespace std;
int f[200005],m;
struct edge{
int a,b,lo;
}E[200005]; //边
int n,ans;
bool cmp(edge x,edge y){return x.lo<y.lo;} //排序
int getfather(int k){ //并查集
if(f[k]==k)return k;
return f[k]=getfather(f[k]);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)f[i]=i;
for(int i=1;i<=m;i++)scanf("%d%d%d",&E[i].a,&E[i].b,&E[i].lo);
sort(E+1,E+1+m,cmp); //排序
for(int i=1;i<=m;i++){
if(getfather(E[i].a)!=getfather(E[i].b)){ //判断两个点是否连通
ans+=E[i].lo; //若不连通建边
f[getfather(E[i].a)]=getfather(E[i].b);
n--; //减掉一个未连通点
}
if(n==1){ //若全部连通就输出答案
printf("%d
",ans);
return 0;
}
}
printf("orz
"); //否则输出orz
return 0;
}
实现方法二.(Prim) (时间复杂度根据不同情况,不同计算)
(Prim) 是一种利用不断寻找最小值来实现找到最小生成树的方法。
具体来说就是以某一个点为起点(一般选择节点 (1) 为起点),寻找能走的边中边权(代价)最小的边,再将此点收入囊中,也就是看做是一段子图。
然后更新能走的边所花费的代价,因为当我加入一个节点后,该节点也可以通到别的点,而且从此新节点出发走原来可以走的边,可能存在更小代价.
例如:若此时 (A) 已经与 (B) 已经连接,(A) 到 (C) 代价为 (3) , (B) 存在一条边到 (C) 的代价为 (1) ,那么此时就可以更新当前可以走的边。
以上面的题目的样例来做例子,来模拟一下(Prim) 算法全过程:
1.选择节点 (1) 为起点,并发现当前 (1) 到 (2) 和 (1) 到 (3) 都是当前能走的边中边权最小的,于是任意走一条即可(这里选择走 (1) 到 (2) ),如下图所示。
2.当前能走的边中发现 (1) 到 (3) 是边权最小的边,于是走此边,如下图所示。
3.当前能走的边中,发现当前 (1) 到 (4) 和 (3) 到 (4) 都是当前能走的边中边权最小的,于是任意走一条即可(这里选择走 (1) 到 (4) ),如下图所示。
4.找到最小生成树,经统计,走过的边权总和为 (7) 于是输出 (7)。
(Code:) (这里用邻接矩阵存图,复杂度为 (O(N^2)) N表示点的数量 )
#include<bits/stdc++.h>
using namespace std;
int n,mmin=INT_MAX,dis[200005],ans,w[5005][5005],walk,m;
bool vis[200005]; //vis表示该边是否被访问过
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
w[i][j]=1e9; //矩阵初始化
for(int i=1;i<=m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
w[a][b]=min(c,w[a][b]);
w[b][a]=min(c,w[b][a]); //邻接矩阵存图
}
for(int i=1;i<=n;i++)dis[i]=w[1][i]; //初始化从起点开始能到达的边,能到达就赋值走到该点要付出的代价,否则为1e9
dis[1]=0;
vis[1]=true;
for(int i=2;i<=n;i++){
mmin=1e9;
for(int j=2;j<=n;j++)
if(dis[j]<mmin&&!vis[j]){ //找到当前所有能走的边的最小值
mmin=dis[j];
walk=j;
}
ans+=mmin; //走这条边
vis[walk]=true; //标记已经走过
for(int j=2;j<=n;j++)if(!vis[j])dis[j]=min(dis[j],w[walk][j]); //更新能走的边花费的价值
}
printf("%d
",ans);
return 0;
}
4.题目推荐
[USACO05MAR]Out of Hay S:相当于模板(双倍经验,岂不美哉)。
[USACO3.1]最短网络 Agri-Net:本题用 (for) 建边后跑流程即可。
公路修建:本题要运用两点之间的距离公式 (sqrt{( x_1 - x_2 )^2+( y_1 - y_2 )^2}) 来计算边权,然后跑流程就好了,本题推荐使用 (Prim) 来实现。
(PS) :以上题目适合初学者食用。
5.结语
以上便是此博客的全部内容,其主要适用于没学过最小生成树的人食用,有什么写得不好也欢迎大佬在评论区指出,感谢大佬的阅读。