最小生成树
最小生成树(minimum spanning tree)是由n个顶点,n-1条边,将一个连通图连接起来,且使权值最小的结构(代价最小)。
比如给N个村庄架设通信网络,该如何将这几个村庄都连接起来,成本还最小的问题。
最小生成树的三个性质:
- 最小生成树是树,因此其边数等于顶点数减1,且树内一定不会有环。
- 对于给定的图G,其最小生成树可以不唯一,但其边权之和一定是唯一的
- 由于最小生成树是在无向图上生成的,因此其根结点可以实这棵树上的任意一个结点。
无论是Prim算法还是Kruskal算法,都肯定要注意满足这三个性质。
Prim算法:(普里姆算法)
时间复杂度:O(N^2)(N为顶点数)
prim算法又称加点法,用于边数较多的带权无向连通图
- 方法:每次找与之连线权值最小的顶点,将该点加入最小生成树集合中
- 注意:相同权值任选其中一个即可,但是不允许出现闭合回路的情况。
基本思路是:取一个顶点,选与之连线权值最小的顶点,然后再选择与该顶点连线权值最小的点,但如果都标记过了!就退回,不然会出现闭合回路的情况。
搬运一张过程图
kruskal(克鲁斯卡尔)算法
这里主要学习Kruskal 算法。Kruskal比较适用于稀疏图,是一种贪心算法:为使生成树上边的权值和最小,则应使生成树中每一条边的权值尽可能地小。
Kruskal算法又称为加边法,用于边数较少的带权无向连通图
- 方法:每次找最小权值边,将边连接的两个顶点加入最小生成树集合中。
- 注意:相同权值任选其中一个即可,但是不允许出现闭合回路的情况。
具体做法:找出森林中连接任意两棵树的所有边中,具有最小权值的边,如果将它加入生成树中不产生回路,则它就是生成树中的一条边。这里的关键就是如何判断”将它加入生成树中不产生回路”。 多棵树要连接起来,而且不得产生回路,这不就可以应用前几天学的并查集! 如果两棵树的根节点一样,连起来必定有回路。
搬运一下过程图:
要想不连成闭合回路,连接的时候判断一下两个点是否属于同一棵树就好了,这里采用并查集的算法。
然后树的合并的时候可以按秩合并 rank(X)。(其实我觉得不按秩合并当然也没问题)那什么是树的秩呢,其实可以理解为树高。给每个点一个秩(初始值为1),每次合并的时候都用秩小的指向秩大的,可以保证树高最高为log(n)
然后更新 rank ( rank[x]指的是点x秩),在一次合并后,假设是点x和点y,x的秩小,就将 pre[x] 指向 y (意思是y是x的爸爸~),然后将 rank[y] 的与 rank[x+1] 取 max
因为rank[x]为区间x的高,将它连向y之后,y的树高就会是x的树高+1,当然也可能y在另一边树高更高,所以取max
以下为各代码块的详细说明:
- 定义边(x,y),权为w
/* 定义边(x,y),权为w */
struct node{
int x,y;//树一条边的起点和终点;
int w;// 边权(边的价值或大小)
}a[110];
- cmp排序(按权值进行排序)
bool cmp(struct node a,struct node b)
{
return a.w<b.w;
}
- 并查集的相关函数
int pre[maxn];//存放根节点
int rank[maxn] = {0};//存放x的秩
int Find(int x){ //找根节点
int r=x;
while(r!=pre[r]){
r=pre[r];
}
// int i=x,j;
// while(pre[i]!=r)//这里因为树的深度一般不深,所以可以省去路径压缩
// {
// j=pre[i];
// pre[i]=r;
// i=j;
// }
return r;
}
void join(int x, int y, int w){
sum += w;
if(x == y) return ;
if(rank[x] > rank[y]) pre[y] = x;//如果y的秩小于x的秩,x就成为y的前驱
else{
if(rank[x] == rank[y]) rank[y]++;
pre[x] = y;
}
} //这里的join函数是按秩排序的,但其实可以直接写进去main函数emm
这个是模版,用一下c++里的sort,其他都是c语言
#include<bits/stdc++.h>
using namespace std;
struct node{
int x,y,w;
}a[110]; //一条边的起点和终点
int pre[110] = {0}; //存储上级是谁的那个数组
int find(int root)
{
int r = root;
while(r!=pre(r)){
r=pre[r];
}
return r;
} //用来找根节点的
bool cmp(node a,node b)
{
return a.w<b.w;
} //按照权值进行排序
int main()
{
int n,m;
while(scanf("%d%d",&m,&n),m)
{
int sum=0,num=0; //总成本,总连线数
//memset(pre,0,sizeof(pre));
for(int i=1;i<=n;i++) pre[i]=i;//自己做自己的上级
for(int i=1;i<=m;i++)
scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].w);//分别输入该结点
sort(a+1,a+1+m,cmp); //按照边的权值进行排序
for(int i=1;i<=m;i++) //经过排序后,从小到大开始枚举测试边
{
int f1=find(a[i].x);
int f2=find(a[i].y);
if(f1!=f2)//如果不是同一棵树,代表不会构成环
{
sum+=a[i].w;//sum加上这条边的权值
pre[f1]=f2;//修这条路
num++;
}
if(num==n-1) break; // 边的条数==点的个数-1
}
if(num==n-1) printf("%d\n",sum);
else printf("NULL\n");
}
return 0;
}
一道经典例题:畅通工程
#include<bits/stdc++.h>
using namespace std;
struct node{
int x;
int y;
int w;
}a[150];
int pre[150]={0};
int Find(int x){
int r=x;
while(r!=pre[r]){
r=pre[r];
}
return r;
}
bool cmp(node a,node b){
return a.w<b.w;
}
int main(){
int m,n;
while(scanf("%d%d",&m,n),m){
int num=0;
int sum=0;
for(int i=1;i<=m;i++){
pre[i]=i;
}
for(int j=1;j<=n;j++){
scanf("%d%d%d",&a[j].x,&a[j].y,&a[j].w);
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++){
int find1=Find(a[i].x);
int find2=Find(a[i].y);
if(find1 != find2){
pre[find1]=find2;
num++;
sum+=a[i].w;
}
if(num == m-1) break;
}
if(num == m-1) printf("%d\n",sum);
else printf("?\n");
}
}