最小生成树定义
图的所有生成树中具有边上的权制之和最小的树称为图的最小生成树(Minimum Spanning Tree)
最小生成树在许多领域都有重要的作用,例如:
要在 n 个城市之间铺设光缆,使它们都可以通信
铺设光缆的费用很高,且各个城市之间因为距离不同等因素,铺设光缆的费用也不同
如何使铺设光缆的总费用最低?
图
最小生成树
Prim算法
Prim算法本质是一种贪心算法:直接寻找已知点的最小邻边加入即可。
图解
随机构建一个无向图:
同时创建一个dist数组,来记录每个点到当前连通块的距离,起始节点的初始值为0,其它都为正无穷
1 | 2 | 3 | 4 | 5 | 6 | 7 | |
dist | 0 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
选择其中一个节点作为起始节点
更新dist值
1 | 2 | 3 | 4 | 5 | 6 | 7 | |
dist | 0 | 3 | 9 | 7 | ∞ | ∞ | ∞ |
接下来找的最小的邻边,并其连接的点将加入到连通块,
该图中为点2
再由点2更新dist值
1(visited) | 2(visited) | 3 | 4 | 5 | 6 | 7 | |
dist | 0 | 3 | 9 | 1 | 10 | ∞ | ∞ |
重复以上过程
点4更新dist值
1(visited) | 2(visited) | 3 | 4(visited) | 5 | 6 | 7 | |
dist | 0 | 3 | 5 | 1 | 8 | 11 | 13 |
点3更新dist值
1(visited) | 2(visited) | 3(visited) | 4(visited) | 5 | 6 | 7 | |
dist | 0 | 3 | 5 | 1 | 8 | 9 | 13 |
加入1,2,3,4点后,接下来最短的相邻边长度是7,但是这条边连接的两点都已经加入集合了,所以不能选择这条边
最终得到了如下图的最小生成树
题目链接:Prim – Woozie's Blog
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int N=510,M=1e5+10;
int n,m;
int g[N][N];
int dis[N];
bool vis[N];
int prim()
{
int res=0;//记录最小生成树中边的长度
memset(dis,0x3f3f3f,sizeof(dis));
for(int i=0;i<n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)//找最短边
if(!(vis[j])&&(t==-1||dis[j]<dis[t]))
t=j;
if(i&&dis[t]==0x3f3f3f3f)//无最短距离
return 0x3f3f3f3f;
if(i)//从第二个开始加入边权
res+=dis[t];
vis[t]=true;
for(int j=1;j<=n;j++)//更新dis值
dis[j]=min(dis[j],g[t][j]);
}
return res;
}
int main()
{
memset(g,0x3f,sizeof(g));
scanf("%d%d",&n,&m);
while(m--)
{
int u,v,m;
scanf("%d%d%d",&u,&v,&m);
g[u][v]=min(g[u][v],m);
g[v][u]=min(g[v][u],m);
}
int ans=prim();
if(ans==0x3f3f3f3f)
printf("impossible");
else
printf("%d",ans);
}
Kruskal算法
Kruskal算法的思路是先将边从小到大排序,再依次考察每条边的两点(u,v)来判断是否加入该边
图解
先加入最小的边,连接了2,4
剩余边中连接5,7的最小
连接1,2的最小,以此类推
此时连接1,4的边最小,但是1,4已经连通了,所以不加入该边
最终得到最小生成树
有一个关键问题需要解决:如何判断两点是否在同一个连通块中?
最简单的方法是用并查集
代码如下:
for(int i=1;i<=n;i++) fa[i]=i;//初始化
int find(int x)//寻找并压缩路径{
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
fa[find(y)]=fa[find(x)];//合并x所在集合和y所在集合
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int M=2e5+10;
int n,m;
int cnt;//记录边数
int res;//记录最小生成树长度
int fa[N];
struct Edge{
int a,b,v;
}edge[M];
bool cmp(Edge x,Edge y)//根据边的大小从小到大排序
{
return x.v<y.v;
}
int find(int x)//并查集
{
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
bool Kruskal()
{
int count=0;//记录加入边的个数
for(int i=1;i<=cnt;i++)//从小到大循环边
{
int x=edge[i].a,y=edge[i].b,z=edge[i].v;
if(find(x)!=find(y))
{
res+=z;
fa[find(y)]=fa[find(x)];//合并x所在集合和y所在集合
count++;
}
}
if(count==n-1)//边数为n-1则能构成最小生成树
return true;
return false;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)//初始化
fa[i]=i;
while(m--)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
if(u==v) continue;
edge[++cnt]={u,v,w};
}
sort(edge+1,edge+cnt+1,cmp);
if(Kruskal())
printf("%d",res);
else
printf("impossible");
}
效率上来讲kruskal算法优于Prim,只需要排序一次就能找到最小生成树
但kruskal算法是基于边的算法,更适合于边数少的稀疏图,而Prim算法则更适合稠密图