最小生成树
文章目录
最小生成树
给定一个无向图,如果它的某个子图中任意两个顶点都互相连通且是一棵树,那么这棵树就是生成树 其中,边权和最小的生成树就是最小生成树(MST)
一些性质:
n-1条边
若T1,T2都是最小生成树,则T1,T2的各边权是相同的(可能连接的边不同),换句话说,若边权各不相同,则最小生成树唯一(实际上也是拟阵贪心的性质)
往生成树上加一条边就形成一个环,假设是(u,v)加边,那么就形成(u,…,lca(u,v),…,v,u)这个环
基本算法
Prim
与Dijkstra比较像 Dijkstra是什么(大雾
我们先在某一个集合中搞一个点
定义d[i]表示点i到这个集合的最小距离
然后不断往这个集合里添加离这个集合最近的点,直到所有的点都加入了这个集合。
每一次加入一个点 都要去搞一下这个点的相邻的点 来更新d[i]
感觉一下正确性显然(什么鬼操作
证明 略~~(其实讨厌这个字~~
详见白书106页
int prim()
{
d[0]=0;
int ans=0;
while(1)
{
int u=-1;
for(int v=0;v<=n;v++)
if(!vis[v]&&(u==-1||d[v]<d[u]))
u=v;
if(u==-1) break;
vis[u]=1;
ans+=d[u];
for(int v=0;v<=n;v++)
d[v]=min(d[v],c[u][v]);
}
return ans;
}
同Dijkstra一样,Prim也可以用堆优化 mark一下
但是我感觉优化效果不明显(可能要在一些特殊数据下或者数据较大的时候吧
Kruskal
按照边的权值从小到大依次check一下,如果不产生环,就把这条边加进去,直到联通所有的点(根据树的性质,则为有n-1条边时
判断是否产生环,主要是看这条边的两个端点是否已经连通
就需要并查集来维护
void Init()
{
for(int i=1;i<=n;i++)
f[i]=i;
}
int Find(int x)
{
if(f[x]!=x)
return f[x]=Find(f[x]);
return f[x];
}
bool Union(int x,int y)
{
int u=Find(x),v=Find(y);
if(u==v) return 0;
if(u>v) f[u]=v;
else f[v]=u;
return 1;
}
int Kruskal()
{
Init();
int num=0,ans=0;
for(int i=1;i<=m;i++)
if(Union(edge[i].u,edge[i].v))
{
num++;
ans+=edge[i].w;
if(num==n-1) break;
}
if(num!=n-1) return -1;//没有生成树
return ans;
}
考虑到边一般是u,v,w三元组,交换两个结构体的时间开销比较大,所以我们也可以对下标搞排序,就只用交换1个变量就可以啦
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAXN 110
#define MAXM 5010
struct node{
int u,v,w;
}edge[MAXM];
int n,m;
int f[MAXN],order[MAXM];
void st()
{
for(int i=1;i<=m-1;i++)
for(int j=1;j<=m-i-1;j++)
if(edge[order[j]].w>edge[order[j+1]].w)
{
int temp=order[j];
order[j]=order[j+1];
order[j+1]=temp;
}
}
void Init()
{
for(int i=1;i<=n;i++)
f[i]=i;
}
int Find(int x)
{
if(f[x]!=x)
return f[x]=Find(f[x]);
return f[x];
}
bool Union(int x,int y)
{
int u=Find(x),v=Find(y);
if(u==v) return 0;
if(u>v) f[u]=v;
else f[v]=u;
return 1;
}
int Kruskal()
{
Init();
int num=0,ans=0;
for(int i=1;i<=m;i++)
if(Union(edge[order[i]].u,edge[order[i]].v))
{
num++;
ans+=edge[order[i]].w;
if(num==n-1) break;
}
return ans;
}
int main()
{
while(scanf("%d",&n)!=EOF&&n)
{
m=0;
for(int i=1;i<=n*(n-1)/2;i++)
{
int uu,vv,ww;
scanf("%d %d %d",&uu,&vv,&ww);
edge[++m].u=uu,edge[m].v=vv,edge[m].w=ww,order[m]=i;
}
st();
printf("%d\n",Kruskal());
}
}//这个代码好像没有调出来qwq
Prim VS Kruskal
我感觉更喜欢Kruskal一点
也不知道为什么 它看起来比较快一点
(这两个都是打的单纯的未优化版的板子)
总的来说
他们都是基于贪心的思想
Prim与点的关系比较大,适用于稠密图
Kruskal与边的关系比较大,适用于稀疏图
拟阵
这个要展开的话就非常冗杂了
所以大概简单说一下
另外 更深入了解的话 mark一下 https://blog.csdn.net/qingyingliu/article/details/82055737
必选某些边的最小生成树,就可以把那些必选的边先搞进去,再开始算法。
例题
先上板子
HDU 1233 还是畅通工程
某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。
测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( < 100 );随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。
当N为0时,输入结束,该用例不被处理。
对每个测试用例,在1行里输出最小的公路总长度。
Sample Input
3
1 2 1
1 3 2
2 3 4
4
1 2 1
1 3 4
1 4 1
2 3 3
2 4 2
3 4 5
0
Sample Output
3
5
Prim
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define MAXN 2005
#define INF 0x3f3f3f3f
int c[MAXN][MAXN],d[MAXN];
bool vis[MAXN];
int n,m;
int prim()
{
d[1]=0;
int ans=0;
while(1)
{
int u=-1;
for(int v=1;v<=n;v++)
if(!vis[v]&&(u==-1||d[v]<d[u]))
u=v;
if(u==-1) break;
vis[u]=1;
ans+=d[u];
for(int v=1;v<=n;v++)
d[v]=min(d[v],c[u][v]);
}
return ans;
}
int main()
{
while(scanf("%d",&n)!=EOF&&n)
{
memset(d,INF,sizeof(d));
memset(c,INF,sizeof(c));
memset(vis,0,sizeof(vis));
m=0;
for(int i=1;i<=n*(n-1)/2;i++)
{
int uu,vv,ww;
scanf("%d %d %d",&uu,&vv,&ww);
c[uu][vv]=c[vv][uu]=ww;
}
printf("%d\n",prim());
}
return 0;
}
Kruskal
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAXN 110
#define MAXM 5010
struct node{
int u,v,w;
}edge[MAXM];
int n,m;
int f[MAXN];
bool cmp(node p,node q)
{
return p.w<q.w;
}
void Init()
{
for(int i=1;i<=n;i++)
f[i]=i;
}
int Find(int x)
{
if(f[x]!=x)
return f[x]=Find(f[x]);
return f[x];
}
bool Union(int x,int y)
{
int u=Find(x),v=Find(y);
if(u==v) return 0;
if(u>v) f[u]=v;
else f[v]=u;
return 1;
}
int Kruskal()
{
Init();
int num=0,ans=0;
for(int i=1;i<=m;i++)
if(Union(edge[i].u,edge[i].v))
{
num++;
ans+=edge[i].w;
if(num==n-1) break;
}
return ans;
}
int main()
{
while(scanf("%d",&n)!=EOF&&n)
{
m=0;
for(int i=1;i<=n*(n-1)/2;i++)
{
int uu,vv,ww;
scanf("%d %d %d",&uu,&vv,&ww);
edge[++m].u=uu,edge[m].v=vv,edge[m].w=ww;
}
sort(edge+1,edge+m+1,cmp);
printf("%d\n",Kruskal());
}
}
建图比较巧妙的题目
BZOJ 1601
BZOJ 3714
比较暴力的方式求解生成树问题的变式
数据范围较小
一般枚举一个什么东西(比方说 生成树上的边,所有边,某一权值