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

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

  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
prim算法是一种用于解决最小生成树(Minimum Spanning Tree,MST)问题的算法最小生成树是指在一个连通加权图中找到一棵包含所有顶点并且边权值之和最小的树。 下面以一个例题图来解释prim算法的过程。假设我们有一个加权图,顶点分别为A、B、C、D、E,边的权值为: AB: 2 AC: 3 AD: 7 BC: 8 BE: 4 CE: 5 DE: 6 首先选择一个任意顶点作为起始点,我们选择A点作为起始点。将A点标记为已访问,然后找到与A点相邻的边中权值最小的边,即AB,将B点标记为已访问。此时A—B这条边就成为了最小生成树的一部分。 接下来,我们需要找到与A、B点相邻的边中权值最小的边。分别是AC和BE,我们选择AC这条边,将C点标记为已访问。此时A—B和A—C这两条边就成为了最小生成树的一部分。 然后,我们找到与A、B、C点相邻的边中权值最小的边。分别是AD和CE,我们选择CE这条边,将E点标记为已访问。此时A—B、A—C和C—E这三条边就成为了最小生成树的一部分。 最后,我们找到与A、B、C、E点相邻的边中权值最小的边,即DE。将D点标记为已访问。此时A—B、A—C、C—E和D—E这四条边就组成了最小生成树。 通过上述过程,我们得到了最小生成树,其包含了ABCED这5个顶点,使得边的权值之和最小。这就是prim算法的过程,通过不断选择最小的边来构建最小生成树

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值