浅谈最小生成树
浅谈最小生成树
———\(\rm 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\n",ans);
return 0;
}
}
printf("orz\n"); //否则输出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\n",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.结语
以上便是此博客的全部内容,其主要适用于没学过最小生成树的人食用,有什么写得不好也欢迎大佬在评论区指出,感谢大佬的阅读。
-----------------------------------------------
个性签名:天生我材必有用,千金散尽还复来!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
万水千山总是情,打赏一分行不行,所以如果你心情还比较高兴,也是可以扫码打赏博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!