图论——最小生成树:Prim算法及优化、Kruskal算法,及时间复杂度比较

最小生成树:ios

  一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的全部 n 个结点,而且有保持图连通的最少的边。简单来讲就是有且仅有n个点n-1条边的连通图。算法

  而最小生成树就是最小权重生成树的简称,即全部边的权值之和最小的生成树。网络

  最小生成树问题通常有如下两种求解方式。优化

1、Prim算法spa

  参考了Feynman的博客 .net

  Prim算法一般以邻接矩阵做为储存结构。code

  算法思路:以顶点为主导地位,从起始顶点出发,经过选择当前可用的最小权值边把顶点加入到生成树当中来:blog

  1.从连通网络N={V,E}中的某一顶点U0出发,选择与它关联的具备最小权值的边(U0,V),将其顶点加入到生成树的顶点集合U中。排序

  2.之后每一步从一个顶点在U中,而另外一个顶点不在U中的各条边中选择权值最小的边(U,V),把它的顶点加入到集合U中。如此继续下去,直到网络中的全部顶点都加入到生成树顶点集合U中为止。 队列

  模板题连接:Prim算法求最小生成树

  朴素版时间复杂度O(n²)算法模板:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 500+10;
int n,m;
int g[N][N],dis[N],vis[N];

void prim()
{
    memset(dis,0x1f,sizeof dis);
    dis[1]=0;
    for(int j=1;j<=n;j++)
    {
        int min_len=2e+9,k;
        for(int i=1;i<=n;i++)
        {
            if(!vis[i]&&dis[i]<min_len)
            {
                min_len=dis[i];
                k=i;
            }
        }
        vis[k]=1;
        for(int i=1;i<=n;i++)
        {
            if(!vis[i]&&dis[i]>g[k][i])
                dis[i]=g[k][i];
        }
    }

}

int main()
{
    scanf("%d%d",&n,&m);
    memset(g,0x1f,sizeof g);
    for(int i=1;i<=m;i++)
    {
        int u,v,w;scanf("%d%d%d",&u,&v,&w);
        g[u][v]=g[v][u]=min(g[u][v],w);  //由于有重边,因此取min
    }
    prim();
    int ans=0;
    for(int i=1;i<=n;i++)ans+=dis[i];
    if(ans>1e7)printf("impossible\n");
    else printf("%d\n",ans);
    return 0;
}

  与Dijkstra相似,Prim算法也能够用堆优化,优先队列代替堆,优化的Prim算法时间复杂度O(mlogn)模板(图的存储方式为前向星):

void Prim_heap(int point)
{
    memset(dis,0x1f,sizeof(dis));
    priority_queue<pair<int,int> > q;
    
    dis[point]=0;
    q.push(make_pair(0,1));
    while(!q.empty())
    {
        int k=q.top().second;
        q.pop();
        v[k]=1;
        for(int i=h[k];i!=-1;i=edge[i].next)
        {
            int to=edge[i].to,w=edge[i].w;
            if(!v[to]&&dis[to]>w)
            {
                dis[to]=w;
                q.push(make_pair(-dis[to],to));  //优先队列大根堆变小根堆小骚操做:只需一个‘-’号; 
            }
        }
    }
    for(int i=1;i<=n;i++)if(dis[i]==0x1f1f1f1f)flag=false;  //判断是否不存在最小生成树 
    return ;
}

 

2、Kruskal算法

  相比于Prim算法,更经常使用的仍是Kruskal,其缘由在于Kruskal算法模板的代码量小并且思路易理解。

  算法思路:先构造一个只含 n 个顶点、而边集为空的子图,把子图中各个顶点当作各棵树上的根结点,以后,从网的边集 E 中选取一条权值最小的边,若该条边的两个顶点分属不一样的树,则将其加入子图,即把两棵树合成一棵树,反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。依次类推,直到森林中只有一棵树,也即子图中含有 n-1 条边为止。

  步骤:

  1. 新建图G,G中拥有原图中相同的节点,但没有边;
  2. 将原图中全部的边按权值从小到大排序;
  3. 从权值最小的边开始,若是这条边链接的两个节点于图G中不在同一个连通份量中,则添加这条边到图G中;
  4. 重复3,直至图G中全部的节点都在同一个连通份量中。

  简单来讲就是以边为主导地位,每次选择权值最小的边,判断该边链接的两点是否连通,若不连通,则合并两点(合并操做以并查集实现)。记录合并的次数,当次数等于n-1时结束。

  模板题连接:Kruskal算法求最小生成树

  代码以下:时间复杂度O(mlogm)

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 100000+10, M = 200000+10; 

struct Edge{
    int u,v,w;
    bool operator < (const Edge &E)const
    {
        return w<E.w;
    }
}edge[M];
int fa[N];
int n,m,cnt,ans;

int find(int x)
{
    if(fa[x]==x)return x;
    else return fa[x]=find(fa[x]);
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)fa[i]=i;
    for(int i=1;i<=m;i++)
    {
        int a,b,c;scanf("%d%d%d",&a,&b,&c);
        edge[i].u=a;edge[i].v=b;edge[i].w=c;
    }
    sort(edge+1,edge+m+1);
    for(int i=1;i<=m;i++)
    {
        int u=find(edge[i].u),v=find(edge[i].v),w=edge[i].w;
        if(u!=v)
        {
            cnt++;
            fa[u]=v;
            ans+=w;
        }
    }
    if(cnt==n-1)printf("%d\n",ans);
    else printf("impossible\n");
    return 0;
}

 

3、Prim,Prim_heap,Kruskal算法时间复杂度比较

  参考了G机器猫的博客

结论:

  1.Prim在稠密图中比Kruskal优,在稀疏图中比Kruskal劣。

  2.Prim_heap在任什么时候候都有使人满意的的时间复杂度,可是代价是空间消耗极大。(以及代码很复杂>_<)

  但值得说一下的是,时间复杂度并不能反映出一个算法的实际优劣。

  竞赛题通常给的都是稀疏图,选择Prim_heap便可;若是以为代码量太大,想要在Prim与Kruskal算法中选一个,那就选择Kruskal算法。

相关文章
相关标签/搜索