前言
好的编程是要爬上抽象的阶梯才能到达最通用的解决方案
如这句话所言,最小生成树其实是一个相对比较抽象的概念,它是针对于图的概念,因为树是一种特殊的图,下面将详细讲解最小生成树的概念及三种求最小生成树的方法,思路都十分简单。
目录
正文
---最小生成树的概念
首先,要了解最小生成树,我们知道什么是图和树,如上文所说的,树是一种特殊的图,所以,此处我们先来看图的概念。
先来看严格定义:
图是由顶点的有穷非空集合V ( G ) 和顶点之间边的集合E ( G ) 组成,通常表示为: G = ( V , E ) ,其中,G表示个图,V是图G中顶点的集合,E是图G中边的集合。若V = { v 1 , v 2 , . . . , v n } ,则用∣ V ∣表示图G 中顶点的个数,也称图G的阶,E = { ( u , v ) ∣ u ∈ V , v ∈ V } ,用∣ E ∣表示图G 中边的条数。
看起来是不是很复杂,其实如果简单的来说,就是在一堆顶点之间连边,根据图的种类的不同,图也有很多不同的形式,一般来说,根据边的不同来分类有如下几类(可以结合图片理解):(但要特别注意,图不能是空的)
1.有向图(即边有方向意义的图)
2.无向图(即边无方向意义的图,也可以理解为双向)
3.带权图(即边有权值的图)
4.连通图(即所有点相连的图)
5.强连通图(本身是有向图且是连通图)
而在最小生成树中,最重要的其实是第三种,带权图,但是在了解最小生成树之前,我们要先了解什么是树。
定义很简单:
如果一个无向连通图不包含回路(连通图中不存在环),那么就是一个树。
显然,根据这句话,我们可以推出树有很多性质:
1.n个顶点的树有n-1条边
2.树中不存在环
3.树的根节点没有前驱,其他结点都只有1个直接前驱。
4.树是连通的
最后,我们就要来介绍最小生成树了,对于一个连通图来说,由于其中可能存在环,所以其不一定是一棵树,而最小生成树,就是对一个带权无向连通图,将其内部的部分边删除,使其变成一棵各边权值之和最小的树。
---破圈法
算法为王,数学先行。
对于人们来说,目前破圈法是最快且最简单的求最小生成树的方法,并且更偏向一种数学的方法,其思想十分简单,在图中找到所有的环,并且对一个环,删去其中权值最大的一条边,使得其不是一个环,这样得出来的生成树一定是最小的,如图:
但是,破圈法很难作为一个算法在代码中被实现,原因主要是因为其过程中的删除操作,不管是在什么数据结构中,删除操作都是很难实现的,他会涉及到很多其它数据的变动,因此,这种方法更适合在目测时使用。
---kruskal(克鲁斯卡尔)
kruskal是在算法竞赛中最常见且最实用的方法,其本质是一种贪心的思想,将每条边存下来后,按权值进行排序,然后由小到大遍历,如果将当前这条边加入图中后不会形成环就加入,否则就跳过,这样,形成的生成树也一定是最小的,但是因为其过程更偏向代数,所以此处就不放题解了。
那么如何进行判环呢?将时间与适应性综合考虑,并查集是此处最好的判环方式,详见:
模板:
void kruskal(){
int i=0,ans=0,num=0;//此处数组已经排好序
for(int i=1;i<=cnt;i++){
if(Union(a[i].x,a[i].y)){//并查集合并并判断是否为环
num++;//可以
ans+=a[i].z;//权值累加
}
if(num==n-1){
break;//不行
}
}
cout<<ans;
}
---prim(普里姆)
这个算法虽然并不是非常实用,但是其思想非常重要,基本与求最短路的算法dijskra相同,用蓝点表示已经走过的点,白点表示没有走过的点,每循环一次,将一个白点变成蓝点,而这个白点应是目前所有与蓝点直连的白点中连边边权最小的一个,当循环结束时,就能确保得到的生成树一定是最小的,如图:
模板:
void prim(){
int ans=0;
for(int i=1;i<=n;i++){
int t=-1;
for(int j=1;j<=n;j++){
if(vis[j]==0&&(t==-1||dis[t]>dis[j])){//没有走过且是最优选
t=j;
}
}
if(dis[t]==INT_MAX){
cout<<"orz";
return;
}
vis[t]=1;
ans+=dis[t];
for(int j=head[t];j!=-1;j=ne[j]){
int x=ver[j];
if(vis[x]==0&&dis[x]>=w[j]){
dis[x]=w[j];
}
}
}
cout<<ans;
}
(长度对比很明显....)
例题
T1.最小生成树
题面描述:
给定一个图边的所有信息,求这个图的最小生成树的权值之和。
思路:
模板题,直接按照上面的模板写即可。
代码:
kruskal
#include<bits/stdc++.h>
using namespace std;
int pre[5005],cnt;
struct node{
int x;
int y;
int z;
}a[200005];
bool cmp(node a,node b){
return a.z<b.z;
}
int Find(int x){
if(pre[x]==x) return x;
return pre[x]=Find(pre[x]);
}
bool Union(int x,int y){
int xx=Find(x);
int yy=Find(y);
if(xx==yy){
return 0;
}
pre[xx]=yy;
return 1;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>a[i].x>>a[i].y>>a[i].z;
}
sort(a+1,a+1+m,cmp);
for(int i=1;i<=n;i++) pre[i]=i;
int i=0,ans=0;
for(int i=1;i<=m;i++){
//cout<<a[i].z<<" ";
if(Union(a[i].x,a[i].y)){
cnt++;
ans+=a[i].z;
}
if(cnt==n-1){
break;
}
}
if(cnt<n-1){
cout<<"orz";
return 0;
}
cout<<ans;
return 0;
}
prim
#include<bits/stdc++.h>
using namespace std;
int n,m;
int pre[5005],cnt;
int vis[5005],dis[5005],head[400005],ver[400005],ne[400005],tot=-1,w[400005];
priority_queue<int,vector<int>,greater<int> > q;
void add(int x,int y,int z){
tot++;
ver[tot]=y;
w[tot]=z;
ne[tot]=head[x];
head[x]=tot;
}
void prim(){
int ans=0;
for(int i=1;i<=n;i++){
int t=-1;
for(int j=1;j<=n;j++){
if(vis[j]==0&&(t==-1||dis[t]>dis[j])){
t=j;
}
}
if(dis[t]==INT_MAX){
cout<<"orz";
return;
}
vis[t]=1;
ans+=dis[t];
for(int j=head[t];j!=-1;j=ne[j]){
int x=ver[j];
if(vis[x]==0&&dis[x]>=w[j]){
dis[x]=w[j];
}
}
}
cout<<ans;
}
int main(){
memset(head,-1,sizeof head);
cin>>n>>m;
for(int i=1;i<=n;i++) dis[i]=INT_MAX;
dis[1]=0;
for(int i=1;i<=m;i++){
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
add(y,x,z);
}
prim();
return 0;
}