我眼中的最小生成树

前言

鄙人不才,读代码能力比较差,实现代码能力也比较差,作为一个之前对这个算法一无所知的蒟蒻来说,对于网上的讲解大多不甚明了,而且大家也都有各自不同的实现方法。各位大佬可能觉得最小生成树比较简单,可以一笑而过。这篇博客主要是写给未来的我,让我以后忘记这个算法时,看到这篇博客,还能找回当年的思路。
图论中,如何储存图是一个关键。邻接矩阵简单,但使用起来效率低下;邻接表效率高,但写起来比较麻烦。关于邻接表,网上使用的大多是前向星式邻接表,而我自认为我所使用的vector式邻接表理解起来更简单,具体实现都写在代码里。

什么是最小生成树

最小生成树,英文名是minimum spanning tree(MST)。通俗地讲,最小生成树就是在一个图里,删去一些边,使得剩下的图连通,且边的权值和最小,剩下的图就被称为最小生成树。(显然,最小生成树是刚好连通的,每删去一条边,都会使它不连通,即边数=点数-1,这同时也是树的性质,所以这样的图是一棵树。这就是为什么这样的图不叫最小生成图,而叫最小生成树。)
如果还不理解,看看图吧。

本文以洛谷p3366为例。介绍一下最小生成树算法。

prim+优先队列

网上一般的prim有朴素prim和prim+heap。我所使用的prim+优先队列和prim+heap差不多,但自认为代码编写复杂度和理解难以程度和朴素prim差不多。优先队列是一种C++自带的数据结构,非常好用,如果不会,一定要去学一下。
以下我们不考虑非连通图。思路是这样的:做到中间时,可以看成有两个不连通的图,一个是已经选入最小生成树的一些点和几条边,此时这个图和外界是独立的,然后要把外界与这个图相连的最小的一条边和这条边所连的点加入这个图,不断循环直到所有点都入队,就得到了一棵最小生成树。prim算法需要先给出一个起点(如果题目没说,也可以随便找一个起点),然后不断循环:找出所有未标记的点与已标记的点所连成的边中权值最小的,并把那个未标记的点标记掉。是不是有点难懂。代码的实现方法是这样的:先用邻接表存图,把起点所能扩展出来的边进队,每次用优先队列找到队里最短的边所对应的节点(如果这条边的节点已被选过,就continue),把这个节点所连的边也入队,并标记掉(防止下次再被选)。这样,所有点就都能被扩展进来了。至于非连通图,只要看是否所有点都被标记过了。
代码如下,结合着注释好好理解一下。

#include<iostream>
#include<queue>
#include<vector>
using namespace std;
struct node
{
    int to,dis;
    node(){}//没有这一行就无法定义node类型的变量,如node a;是会使编译错误的 
    node(int x,int y):to(x),dis(y){}//构造函数,可以把变量转换为node类型,使以下语句合法:node a=node(x,y); 
    bool operator <(node x) const//为优先队列定义node函数的大小比较(优先级) 
    {
        return dis>x.dis;
    }
};
int n,m,b[5005],ans;
vector<node> a[5005];
priority_queue<node> p;
void add(int x,int y,int z)//用a[i]储存i这个点的信息,a[i][j].to表示i这个点第j条边是从i到a[i][j].to这个点,a[i][j].dis表示i到a[i][j].to的距离 
{
    a[x].push_back(node(y,z));
}
void prim(int v)
{
    for (int i=0;i<a[v].size();i++)//先把起点所连的边进队 
    {
        p.push(a[v][i]);
    }
    b[v]=1;
    while (!p.empty())//一直循环到队为空 
    {
        node x=p.top();//每次把所有边中离已选点最近的边弹出 
        p.pop();
        if (b[x.to]==1) continue;//如果这个点已被选过就就不能再被选了 
        int u=x.to;
        b[u]=1;//把u这个点选进来 
        ans+=x.dis;//ans表示最小生成树的边的权值和 
        for (int i=0;i<a[u].size();i++)//把u能扩展出来的边进队 
        {
            if (b[a[u][i].to]==0) 
            {
                p.push(a[u][i]);
            }
        }
    }
}
int main()
{
    cin>>n>>m;
    int x,y,z;
    for (int i=1;i<=m;i++)
    {
        cin>>x>>y>>z;
        add(x,y,z);//需要add两次,因为这是无向图 
        add(y,x,z);
    }
    prim(1);
    for (int i=1;i<=n;i++)
    if (b[i]==0) ans=-1;
    if (ans!=-1) cout<<ans;
    else cout<<"orz";
}

至于复杂度,朴素prim是O(V^2),网上的prim+heap是O(ElogV),而由于我的程序是把边入队,而不是点入队,所以我也不知道我的程序复杂度是多少(应该约等于O(ElogV)。(V表示点数,E表示边数)

kruskal+并查集

这个算法需要并查集,如果不会请自学。如果说prim是基于点来做的,kruskal就是基于边来做的。这个不用邻接表,但也要优先队列。读入的时候把每一条边存入优先队列里。优先队列的好处在于,可以自动排序(复杂度为O(logn))。思路如下:边按权值从小到大自动排完序之后,如果不构成环就选进来,直到所有点都被选进来。是否构成环只要用并查集看这条边的两个端点是否在一个集合里。
具体代码如下:

#include<iostream>
#include<queue>
#include<vector>
using namespace std;
struct node
{
    int x1,x2,dis;
    node(){}
    node(int x,int y,int z):x1(x),x2(y),dis(z){}
    bool operator <(node x) const//const不能漏写,这是语法问题 
    {
        return dis>x.dis;
    }
};
int n,m,ans,fa[5005],b[5005];
priority_queue<node> p;
int find(int x)//找一个点属于哪个集合 
{
    if (fa[x]==x) return x;
    fa[x]=find(fa[x]);
    return fa[x];
}
void merge(int x,int y)//合并两个集合 
{
    fa[x]=y;
}
void kruskal()
{
    while (!p.empty())
    {
        node z=p.top();//每次把最短的边弹出来 
        int x=z.x1,y=z.x2;
        x=find(x);
        y=find(y);
        p.pop();
        if (x==y) continue;//判断是否构成环 
        merge(x,y);//用边连接这两个集合 
        ans+=z.dis;//最小生成树权值和要加上这条边的权值 
    }
}
int main()
{
    cin>>n>>m;
    int x,y,z;
    for (int i=1;i<=m;i++)
    {
        cin>>x>>y>>z;
        p.push(node(x,y,z));//把每条边入队 
    }
    for (int i=1;i<=n;i++) fa[i]=i;//一开始,每个点都是独立的集合,所以后面要用边去连接着一个个集合 
    kruskal();//不需要像prim一样需要起点 
    x=find(1);
    for (int i=2;i<=n;i++)
    {
        if (x!=find(i)) ans=-1;//看是否所有点都在一个集合里,如果没有,说明此图不连通 
    }
    if (ans!=-1) cout<<ans;
    else cout<<"orz";
}

至于复杂度,一开始将边加入优先队列,并自动排序,复杂度就是堆排序的复杂度,及O(ElogE),后面对每条边进行处理(并查集的复杂度可以看成一个小常数(一般小于等于4),忽略不计),复杂度为O(logE)。所以kruskal的总复杂度为O(ElogE)。(不考虑任何优化的前提下)

总结

这两种算法的思想都很妙,不过由于我水平有限,也无法证明这两种算法的正确性,有兴趣的同学可以自行百度。
从复杂度上,可以看出,prim+优先队列效率高于kruskal(因为一般认为边数大于点数),而在我的程序里,编程复杂度也差不多,所以推荐大家写prim+优先队列。但一般,对于稀疏图,用kruskal比较多;对于稠密图,用prim+优先队列比较多。至于具体题目,还请大家自己斟酌。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值