最小生成树是无向图中的算法,最基础的是在一张无向图中求一棵树,该树包含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算法。