浅谈最小生成树

· · 个人记录

引言:该文章由kkkac01,Eric_hoo,selina联合出版,感谢“hello luogu”群友对我们的鼓励

这几天在看洛谷日报,结果发现竟然没有最小生成树,赶紧过来水一发。

最小生成树(Minimum Spanning Tree,MST)

定义: 给定一张边带权的无向图 G=(V,E) ,n=|V| ,m=|E|

由V中全部n个顶点和E中n-1条边构成的无向连通子图

被称为G的一颗生成树

边权之和最小的的生成树被称为无向图G的最小生成树

实现最小生成树的方法有两种,那么我们就先讨论第一种,也是最常见的一种:

Kruskal算法

首先,我们先来看一个定理:

任意一颗最小生成树一定包含无向图中权值最小的边

证明:

反证法。假设无向图G=(V,E)存在一颗最小生成树不包含权值最小的边。 设e=(x,y,z)是无向图中权值最小的边。e添加到树中,会和树上的x到y的路径一起构成一个环,并且环上其它边的权值都比z大。因此,用e代替环上的其它任意一条边,会形成一颗权值和更小的生成树,与假设矛盾。故假设不成立.

是不是感觉看了一堆废话。。。。。。。。。。

好的,进入正题:

Kruskal算法就是建立在这个结论上的

详细的介绍一下Kruskal的算法流程

  1. 建立并查集,每个点各自构成一个集合。

  2. 把所有的边按照权值从大到小排序,一次扫描每条边(x,y,z)。

  3. 若x,y属于同一集合(连通),则忽略。

  4. 否则合并xy,并将z累加进答案中。

复杂度O(m log m)

我们图解一下:

让我们把它变成代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

int father[100001],n,m,tot,k;

int read(){
    int r=0,f=1;
    char ch;
    do ch=getchar();while(!isdigit(ch) && ch!='-');
    if (ch=='-')f=-1,ch=getchar();
    do r=r*10+ch-48,ch=getchar();while(isdigit(ch));
    return r*f;
}

int find(int x){
    if (father[x]!=x)
        father[x]=find(father[x]);
    return father[x];
}

void unionn(int r1,int r2){
    father[find(r2)]=find(r1);
}

struct node{
    int s,e,w;
}edge[200001];

bool operator < (node a,node b){
    return a.w<b.w;
}

void Kruskal(){
    for (int i=1;i<=m;i++)
        if (find(edge[i].s)!=find(edge[i].e)){
            unionn(edge[i].s,edge[i].e);
            tot+=edge[i].w;
            k++;
            if (k==n-1)
                break;
        }
    k==n-1?printf("%d\n",tot):printf("orz\n");
}

int main(){
    n=read();m=read();
    for (int i=1;i<=n;i++)father[i]=i;
    for (int i=1;i<=m;i++){
        edge[i].s=read();
        edge[i].e=read();
        edge[i].w=read();
    }
    sort(edge+1,edge+m+1);
    Kruskal();
    return 0;
}

休息一下,马上回来

好的,接下来我们开始讨论另一个算法:

Prim算法

   Prim算法的思路略有改变。设图G顶点集合为U,首先任意选择图G中的一点作为起始点a,将该点加入集合V,再从集合U-V中找到另一点b使得点b到V中任意一点的权值最小,此时将b点也加入集合V;以此类推,现在的集合V={a,b},再从集合U-V中找到另一点c使得点c到V中任意一点的权值最小,此时将c点加入集合V,直至所有顶点全部被加入V,此时就构建出了一颗MST。因为有N个顶点,所以该MST就有N-1条边,每一次向集合V中加入一个点,就意味着找到一条MST的边。

还是图解吧。。。。。

那么将它变成程序: PS:加了点神奇的优化,思路没变

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <map>
#include <queue>
using namespace std;
int head[10010],to[500010],ww[500010],nxt[500010];
int dis[10010];
bool mark[10010];
int R=0,n,m,ans=0;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
pair <int,int> pa;

void MEMSET_INF()
{
    for (int i=1;i<=n;++i)
        dis[i]=0x7fffffff;
}

void Prim()
{
    dis[1]=0;
    pa.first=0,pa.second=1,q.push(pa);
    while (!q.empty())
    {
        pair <int,int> p;
        do{
            p=q.top(),q.pop();
        }while (mark[p.second]&&!q.empty());
        if (mark[p.second]&&q.empty()) continue;
        mark[p.second]=1;
        ans+=p.first;
        for (int e=head[p.second];e;e=nxt[e])
            if (ww[e]<dis[to[e]])
                dis[to[e]]=ww[e],q.push(make_pair(dis[to[e]],to[e]));
    }
}

void AddEdge(int u,int v,int w){
    to[++R]=v,ww[R]=w,nxt[R]=head[u],head[u]=R;
}

int main()
{
    scanf("%d%d",&n,&m);
    for (int i=1,u,v,w;i<=m;AddEdge(u,v,w),AddEdge(v,u,w),++i)
        scanf("%d%d%d",&u,&v,&w);
    MEMSET_INF();
    Prim();
    cout << ans << endl;
    return 0;
}

好的,这两个算法今天就介绍到这里,祝大家愉快,也请大佬们不要。。那啥。。,鼓励一下蒟弱。

尾声:这两个算法各有使用之处,Prim适合用于稠密图,Kruskal适用于稀疏图,所以不要一味的去写Kruskal,要按着题目分析,才能真正发挥这两个算法的作用!