最小生成树算法及例题整理

最小生成树是无向图中的算法,最基础的是在一张无向图中求一棵树,该树包含n个点,同时树上所有边的边权和最小。

性质:
包含n个点,n-1条边,
任意两点之间都是连通的,
边权和最小,
可能不止一个,
但是边权和是固定的。

有两种算法可以实现:Prim(普利姆算法)和kruskal(克鲁斯卡尔算法)

Prim

时间复杂度:O(n^2)

Prim算法的本质是从任意一点开始往外扩展,将点放进集合中,同时每次循环找到不在集合中且距离集合最近的点(这里也是它和dijkstra算法的区别,dijkstra算法每次循环找的是距离起点最近的点),把它加进集合,统计将这条边计入最短路,然后再用它去更新其他的点,当所有点都被加进集合的时候,那么就找到了一棵最小生成树。

int g[][],st[][];
int d[];
int res;
void prim()
{memset(d,0x3f,sizeof d);d[1]=0;//这里其实无所谓,从任意一点开始都可以for(int i=1;i<=n;i++){int t=-1;for(int j=1;j<=n;j++)if(!st[j]&&(t==-1||d[j]<d[t])) t=j;st[t]=1;res += d[t];for(int i=1;i<=n;i++)if(d[i]>g[t][i])//t已经放入集合中了,所以g[t][i]才是距离集合的距离d[i]=g[t][i];}
}

ps:乍一看真的和朴素版dijkstra算法很像,唯一的区别就在距离的更新上

kruskal

时间复杂度:O(mlogm)

这个算法的思路就是将所有的边排序,然后从小到大遍历所有的边,如果这条边连接的两点不在一个集合中,那么就将它们并成一个集合,否则就略过这条边。所以这里的时间复杂度是sort排序的时间,实际上如果m不大的话,可以视为O(m)。另外这种算法比较有趣的一点在于边可以用任意方式存,最简单的就是直接用结构体来存。

struct edge{int a,b,c;
}e[];
bool cmp(edge x,edge y)
{return x.c<y.c;
}
int p[];
int find(int x)
{if(p[x]!=x) p[x]=find(p[x]);return p[x];
}
int main()
{...sort(e+1,e+m+1);int res=0;for(int i=1;i<=m;i++){int a=e[i].a,b=e[i].b,c=e[i].c;a=find(a),b=find(b);//此时a变成a的祖先节点,b变成b的祖先节点if(a!=b) p[a]=b,res+=c;//直接将两个并查集的祖先节点合到一块儿}...
}

一般来说,可以用prim算法的都可以用kluskal算法,但能用kluskal算法的却未必能用prim算法。

1140. 最短网络(活动 - AcWing)

这题其实很裸,连接所有点,边权和最小,那么就是求最小生成树。这题给的是邻接矩阵,所以prim算法要更方便一些。

#include<bits/stdc++.h>
using namespace std;
int g[120][120],n,res,d[120],st[120];
void prim()
{memset(d,0x3f,sizeof d);d[1]=0;for(int i=1;i<=n;i++){int t=-1;for(int j=1;j<=n;j++)if(!st[j]&&(t==-1||d[t]>d[j]))t=j;st[t]=1;res += d[t];for(int j=1;j<=n;j++)if(d[j]>g[t][j]) d[j]=g[t][j];}
}
int main()
{scanf("%d",&n);for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)scanf("%d",&g[i][j]);prim();cout<<res;
}

 1141. 局域网(活动 - AcWing)

思路:边都是无向边,我们要去除一些网线使得网络中没有回路且不影响连通性,不影响连通性那么就是原本连通的去完还是连通的,然后还要没有回路,那么就是树,但是要注意一点,原本各个点未必都是连通的,所以我们最后得到的相当于是一个森林。然后去除的最大,那么留下的就是最小的,我们可以形象的称之为“最小联通森林”。原图相当于有一个一个的连通块,这里kluskal算法就更好了,prim算法相当于就是一个连通块一个连通块的来处理,而kluskal算法则是整个图来处理,原本连通的就说明有边,我们会访问到所有的边,如果这个边不用就说明它们已经被其他的边连接起来了,所以不影响连通性。

#include<bits/stdc++.h>
using namespace std;
struct edge{int a,b,c;
}e[210];
bool cmp(edge x,edge y)
{return x.c<y.c;
}
int n,m;
int p[120];
int find(int x)
{if(x!=p[x]) p[x]=find(p[x]);return p[x];
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++) p[i]=i;int sum=0;for(int i=1;i<=m;i++){int a,b,c;scanf("%d%d%d",&a,&b,&c);e[i]={a,b,c};sum += c;}sort(e+1,e+1+m,cmp);int res=0;for(int i=1;i<=m;i++){int a=e[i].a,b=e[i].b,c=e[i].c;a=find(a),b=find(b);if(a!=b) p[a]=b,res+=c;}cout<<sum-res;
}

1142. 繁忙的都市(活动 - AcWing)

有n个节点和m条道路,需要挑选一些道路进行改造,被改造的道路需要满足三个条件,由条件1,2可以得到需要求的是一棵树,由条件3可以得到需要求的是最小生成树。那么问题就解决了,不过这里求的是最小生成树的边中最大值最小。

#include<bits/stdc++.h>
using namespace std;
int n,m;
struct edge{int a,b,c;
}e[8010];
bool cmp(edge x,edge y)
{return x.c<y.c;
}
int p[400];
int find(int x)
{if(x!=p[x]) p[x]=find(p[x]);return p[x];
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++) p[i]=i;for(int i=1;i<=m;i++){int a,b,c;scanf("%d%d%d",&a,&b,&c);e[i]={a,b,c};}sort(e+1,e+1+m,cmp);int res=0,cnt=0;for(int i=1;i<=m;i++){int a=e[i].a,b=e[i].b,c=e[i].c;a=find(a),b=find(b);if(a!=b) p[a]=b,res=max(res,c),cnt++;}cout<<n-1<<" "<<res;
}

ps:当然这道题也可以用二分来做,二分出一个最大长度,dfs或bfs查询小于等于这个长度的边能否连通所有的点即可,如果不能连通,那么上调l,否则下调r即可。

1143. 联络员(活动 - AcWing)

题目大意:这题很显然也是求最小生成树,因为连通然后边权和最小。 但是有一点很特殊,这里有一些边是必选的,所以相当于是图中有一些边了然后开始选边,那么很显然prim算法就不合适了,我们可以用kluskal算法,因为它可以访问到所有的边,所以本题的思路就是先将必选边选上,然后再去遍历非必选边,看看是否需要选上。

#include<bits/stdc++.h>
using namespace std;
struct edge{int a,b,w;
}e[10010];
int p[2010];
bool cmp(edge x,edge y)
{return x.w<y.w;
}
int find(int x)
{if(x!=p[x]) p[x]=find(p[x]);return p[x];
}
int main()
{int n,m;scanf("%d%d",&n,&m);for(int i=1;i<=n;i++) p[i]=i;int cnt=0,res=0;for(int i=1;i<=m;i++){int t,a,b,c;scanf("%d%d%d%d",&t,&a,&b,&c);if(t==1) {res += c;a=find(a),b=find(b);p[a]=b; }else e[++cnt]={a,b,c};}sort(e+1,e+cnt+1,cmp);for(int i=1;i<=cnt;i++){int a=e[i].a,b=e[i].b,c=e[i].w;a=find(a),b=find(b);if(a!=b) p[a]=b,res += c;}cout<<res;
}

1144. 连接格点(活动 - AcWing)

思路这题也是用最小的代价将所有点连通,所以也是最小生成树问题。

首先这里的点给的是二维坐标,我们可以用映射处理一下。

但是这题的边显然有些多了,排序的话时间复杂度有点高,但是它只有两种边权的边,我们可以先建立竖边,再建立横边不重复建边,而且为了不重复建边,我们可以指定两点的大小,比如前一个点小于后一个点才建边,因为对于每个点都会遍历到,所以并不会遗漏。 

然后再把已经有的边先加入,再遍历所有的边即可。

#include<bits/stdc++.h>
using namespace std;
const int N=1000010;
int g[1010][1010];
int st[1010][1010];
int p[N];
struct edge{int a,b,c;
}e[2*N];
int dx[]={1,0,-1,0};
int dy[]={0,1,0,-1};
int dw[]={1,2,1,2};
int find(int x)
{if(x!=p[x])p[x]=find(p[x]);return p[x];
}
int main()
{int n,m;scanf("%d%d",&n,&m);for(int i=1;i<=n*m;i++) p[i]=i;int t=0;for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)g[i][j]=++t;int x1,y1,x2,y2;while(~scanf("%d%d%d%d",&x1,&y1,&x2,&y2)){int a=g[x1][y1],b=g[x2][y2];a=find(a),b=find(b);p[a]=b;}int k=0;for(int z=0;z<2;z++)for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)for(int u=0;u<4;u++)if(u%2==z){int x=i+dx[u],y=j+dy[u];if(x<=0||x>n||y<=0||y>m) continue;int a=g[i][j],b=g[x][y],w=dw[u];if(a<b) e[++k]={a,b,w};}int res=0;for(int i=1;i<=k;i++){int a=e[i].a,b=e[i].b,c=e[i].c;a=find(a),b=find(b);if(a!=b) p[a]=b,res+=c;}cout<<res;
}

1146. 新的开始(活动 - AcWing)

思路:连接所有矿井,花费最小,这题乍一看就是最小生成树问题,但是实际上也不完全是,因为每个矿井通电有两种方案,既可以和已经有电的矿井相连,也就是最小生成树,也可以自己通电,这个就不能直接用最小生成树处理了。

那么怎么做呢,我们可以建立虚拟原点,然后将每个点本身通电的花费连到虚拟原点上,然后求最小生成树。

另外这题数据范围比较小,而且输入给的是矩阵,那么可以用prim算法

#include<bits/stdc++.h>
using namespace std;
const int N=310;
int g[N][N],d[N],st[N];
int n;
int res=0;
void prim()
{memset(d,0x3f,sizeof d);d[0]=0;for(int i=0;i<=n;i++){int t=-1;for(int j=0;j<=n;j++)if(!st[j]&&(t==-1||d[t]>d[j])) t=j;st[t]=1;res += d[t];for(int j=0;j<=n;j++)if(d[j]>g[t][j]) d[j]=g[t][j];}
}
int main()
{scanf("%d",&n);g[0][0]=0;for(int i=1;i<=n;i++){int c;scanf("%d",&c);g[0][i]=g[i][0]=c;}for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)scanf("%d",&g[i][j]);prim();cout<<res;
}

 1145. 北极通讯网络(活动 - AcWing)

  

思路:这题求最大值最小,我们可以用二分,那么问题的关键就变成如何求check()函数了。

我们对于二分值d,可以算出小于等于d的边能够建立出多少个连通块,然后判断这些连通块的数量是否小于等于k,如果是,那么就可以作为答案,并且可以找一找更小的可不可以,否则就要往更大的找。

二分看似正确,但是如果k等于0,那么就会二分到右边界,右边界的确定诚然可以先求出一棵最小生成树,然后去取其中最大的边,但实际上有个更简单的思路。

我们直接开始求最小生成树,并记录每次用到的边的最大值,一旦连通块的数量小于等于k,直接退出,然后输出最大值。

那么我们如何判断这些边能建立多少连通块呢?最开始连通块的数量肯定是n,每连接一次,那么数量就少1.

#include<bits/stdc++.h>
using namespace std;
#define x first
#define y second
const int N=600;
typedef pair<int,int> pii;
pii q[N];
int n,k,m;
int p[N];
struct edge{int a,b;double c;
}e[N*N];
bool cmp(edge x,edge y)
{return x.c<y.c;
}
int find(int x)
{if(p[x]!=x) p[x]=find(p[x]);return p[x];
}
double getd(pii a,pii b)
{int dx=a.x-b.x,dy=a.y-b.y;return sqrt(dx*dx+dy*dy);
}
int main()
{scanf("%d%d",&n,&k);for(int i=1;i<=n;i++) cin>>q[i].x>>q[i].y,p[i]=i;for(int i=1;i<=n;i++)for(int j=1;j<i;j++){double d=getd(q[i],q[j]);e[++m]={i,j,d};}sort(e+1,e+1+m,cmp);int cnt=n;double d;for(int i=1;i<=m;i++){if(cnt<=k) break;int a=e[i].a,b=e[i].b;double c=e[i].c;a=find(a),b=find(b);if(a!=b) p[a]=b,d=c,cnt--;}printf("%.2lf",d);
}

346. 走廊泼水节(346. 走廊泼水节 - AcWing题库)

思路:完全图就是任意两点之间都有边的图。

本题看似麻烦,但是既然涉及到最小生成树,那么我们可以想一想最小生成树的建立过程,我们从kluskal的角度来考虑,每次建边,就是将两个 不连通的集合连通,两个不连通的集合就说明除了当前这条边以外,两个集合没有任何边相连,如果要得到完全图,那么很显然两个集合之间要建立更多的边,这些边的具体数目取决于两个集合的大小。那么边权呢,其实只要是一个大于当前边的数都可以。因为当这个图建好后,我们去找最小生成树,当前边肯定在其他边之前被访问到,提前将两个集合连通,所以我们只需要多加一步将集合的大小记录一下就好。

#include<bits/stdc++.h>
using namespace std;
int n,m;
int p[6010],s[6010];
struct edge{int a,b,c;bool operator < (const edge x) const{return c<x.c;}
}e[6010];
int find(int x)
{if(x!=p[x]) p[x]=find(p[x]);return p[x];}
int main()
{int t;scanf("%d",&t);while(t--){int n;scanf("%d",&n);int a,b,c;int m=n-1;for(int i=1;i<=n-1;i++){scanf("%d%d%d",&a,&b,&c);e[i]={a,b,c};}sort(e+1,e+1+m);for(int i=1;i<=n;i++) p[i]=i,s[i]=1;int res=0;for(int i=1;i<=m;i++){int a=e[i].a,b=e[i].b,c=e[i].c;a=find(a),b=find(b);if(a!=b) {res += (s[a]*s[b]-1)*(c+1);p[a]=b;s[b]+=s[a];}}cout<<res<<endl;}
}

1148. 秘密的牛奶运输(活动 - AcWing)

思路:本题是求次小生成树,而且是严格的次小生成树。(不严格的次小生成树的边权和是可以等于最小生成树的)

可以证明最小生成树与次小生成树可以只有一条边不同。那么答案的形式应该是res+w(新)-w(原),要想使答案最小(因为是次小生成树),那么应该使w(原)尽可能的大。

求法:先求出最小生成树,然后对于每个点,求它在最小生成树中距离其他点的最大距离和次大距离。然后再遍历一遍所有在最小生成树中没用到的边,判断它连接的两个点在最小生成树中的路径的最大值,与当前这条边边权的关系,要么相等,要么当前的边更大,更大当然好说,直接替换即可,但是如果相等,那么直接替换得到的就不是严格次小生成树了,所以应该用它去替换第二大的边,这也是记录次大距离的意义。每次替换不用真的替换,计算一下替换后的结果即可,统计一个min值,最后输出即可。

#include<bits/stdc++.h>
using namespace std;
const int N=600,M=2e4+10;
struct edge{int a,b,c;bool operator < (const edge x) const{return c<x.c;}
}ed[M];
int h[N],e[M],ne[M],w[M],idx;
int st[M];
void add(int a,int b,int c)
{w[idx]=c,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int p[N];
int n,m;
int find(int x)
{if(p[x]!=x) p[x]=find(p[x]);return p[x];
}
int g1[N][N],g2[N][N];
void dfs(int u,int f,int mx1,int mx2,int d1[],int d2[])
{d1[u]=mx1,d2[u]=mx2;for(int i=h[u];i!=-1;i=ne[i]){int j=e[i];if(j==f) continue;int td1=mx1,td2=mx2;//其他点还要用if(w[i]>td1) td2=td1,td1=w[i];else if(w[i]<td1&&w[i]>td2) td2=w[i];//等于td1也是不行的,因为要求的是严格次小距离dfs(j,u,td1,td2,d1,d2);}
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=m;i++){int a,b,c;scanf("%d%d%d",&a,&b,&c);ed[i]={a,b,c};}sort(ed+1,ed+1+m);long long res=0;memset(h,-1,sizeof h);for(int i=1;i<=n;i++) p[i]=i;for(int i=1;i<=m;i++){int a=ed[i].a,b=ed[i].b,c=ed[i].c;int pa=find(a),pb=find(b);if(pa!=pb) {st[i]=1;p[pa]=pb,res+=c;add(a,b,c),add(b,a,c);}}for(int i=1;i<=n;i++) dfs(i,-1,-1e9,-1e9,g1[i],g2[i]);long long mi=1e18;for(int i=1;i<=m;i++){if(st[i]) continue;int a=ed[i].a,b=ed[i].b,c=ed[i].c;long long ans;if(c>g1[a][b]) ans=res+c-g1[a][b];else if(c>g2[a][b]) ans=res+c-g2[a][b];mi=min(mi,ans);}cout<<mi;
}

总结:最小生成树类型的题目最显著的特点就是连接所有点,边权和最小,但通常边权和最小这个要求没那么直接,同时也不是每次都要求边权和,所以当看到题目需要连通所有的点或者修改边但不影响连通性(最小生成森林)的时候就要考虑最小生成树,可以先考虑kluskal算法。