关于最小生成树的话,其实很早之前就接触了,当时也写了一篇关于最小生成树的文章,但一直没有好好刷题。
接下来几天会持续更新维护KB-最小生成树专题
最小生成树的算法没有其他算法那么复杂,算法思想比较简单,代码也比较容易。
常见的最小生成树算法由Kruskal
算法和Prim
算法。
1.Kruskal算法 -- 时间复杂度\(O(m * log m)\)
算法思想:
- 建立一个并查集,每个点构成一个集合;
- 将边进行从小到大进行排序,依次扫描边edge(u,v,w);
- 如果u和v 属于同一个集合,那么跳过这轮循环;
- 如果不属于同一个集合,则把u、v合并为同一个集合;
- 当集合中的顶点数为n或者扫遍所有边edge,则构成最小生成树。
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int manx=1e5+5; //对应顶点数目
const int mamx=1e5+5; //对应边的数目
int n,m,u,v,total=1;
struct edge{
int start,to;
long long val;
}bian[mamx];
int a[manx];
long long ans;
int find(int x) //并查集
{
if(a[x]==x) return x;
else return a[x]=find(a[x]);
}
bool cmp(edge x,edge y)
{
return x.val<y.val;
}
inline void kruskal()
{
for(int i=1;i<=m;i++)
{
u=find(bian[i].start);
v=find(bian[i].to);
if(u==v) continue; //如果两个点存在于同一个集合则跳过循环
ans+=bian[i].val;
a[u]=v;
total++;
if(total==n-1) break;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) //并查集的初始化,祖先为自己
a[i]=i;
for(int i=1;i<=m;i++)
scanf("%d%d%d",&bian[i].start,&bian[i].to,&bian[i].val);
sort(bian+1,bian+1+m,cmp);
kruskal();
cout<<ans;
return 0;
}
2.Prim算法
Prim算法跟最短路中的Dijkstra 算法思想相近,有兴趣的可以了解一下:
https://www.cnblogs.com/RioTian/p/12597634.html
算法思想:
1.把1作为起点加入最小生成树集合S;
2.在未访问过的集合T中找出一点距离集合S最近的点,并在T中将其剔除,移入S中;
3.重复2步骤,直到所有点加入S。
朴素算法 时间复杂度 \(O(n^2)\)
const int inf=2147483647;
int a[manx][manx], d[manx], n, m, ans;
bool vis[manx];
void prim(){
for(int i=1;i<=n;i++) d[i]=inf,vis[i]=0,a[i][i]=0; //初始化各数组
d[1]=0; //1作为起点
for(int i=1;i<n;i++) //重复n-1次
{
int x=0;
for(int j=1;j<=n;j++)
if( !vis[j] && (x==0||d[j]<d[x]) ) //寻找未访问过且离集合S最近的点
x=j;
vis[x]=1;
for(int y=1;y<=n;y++) //第一轮d[y]中由起点可到达的点得到更新
d[y]=min( d[y ], a[x][y]) //这里与Dij不同,因为Dij求的是两点间的距离,而这里是点到集合距离
}
}
Prim算法使用堆优化可达到与Kruscal一样的复杂度 \(O(m * log m)\)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int manx=5e3+5;
const int mamx=1e5+5;
int k,n,m,cnt,sum,a,b,c;
int head[manx],dis[manx],vis[manx];
struct node{
int v,w,next;
}e[mamx];
typedef pair<int,int> p;
priority_queue<p,vector<p>,greater<p> >q; //堆优化
void add(int u, int v, int w) //链式前向星建图
{
e[++k].v=v;
e[k].w=w;
e[k].next=head[u];
head[u]=k;
}
int main()
{
memset(dis,127,sizeof(dis));
memset(head,-1,sizeof(head));
scanf("%d%d",&n,&m);
for(R i=1;i<=m;i++)
{
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
//无向图 add(b,a,c);
}
dis[1]=0; //一般将1作为最小生成树扩展的起点
q.push(make_pair(0,1));
while(!q.empty() && cnt<n) //仔细观察可以发现堆优化的Prim也和堆优化的Dij的代码实现有很多相似之处,这是因为两者都是基于贪心的算法
{
int d=q.top().first,u=q.top().second;
q.pop();
if(vis[u]) continue;
cnt++; //计算S集合中顶点的个数
sum+=d; //计算最小权值和
vis[u]=1;
for(R i=head[u];i;i=e[i].next)
if(e[i].w<dis[e[i].v])
dis[e[i].v]=e[i].w,q.push(make_pair(dis[e[i].v],e[i].v));
}
printf("%d",sum);
}
尽管堆优化,但不如直接使用Kruskal算法更加方便,因此,稀疏图用Kruskal,稠密图用Prim 。