什么是最小生成树

在讲最小生成树之前,我们先回顾一下什么是生成树:对于无向图G和一棵树T来说,如果T是G的子图,则称T为G的树,如果T是G的生成子图,则称T是G的生成树。而最小生成树就是对于一个有权值的图来说最小权值和的图就是最小生成树(也就是边权和最小的连通图,且只有n-1条边,其实也就是一颗树)

Kruskal算法

该算法的基本思想是从小到大加入边,是个贪心算法。Kruskal 算法是一种常见并且好写的最小生成树算法,由 Kruskal 发明。

原理虽然简单,但是需要一种数据结构维护一个森林,不能使其成环,或者说是维护多个集合,然后每次合并两个元素或者集合,这很容易和昨天学习的并查集联系起来,我们可以用并查集很轻松的维护这个森林。

如果使用 O(mlog_2m)O(mlog⁡_2m) 的排序算法,并且使用 O(mα(m,n))O(mα(m,n))O(mlog_2n)O(mlog_2⁡n) 的并查集,就可以得到时间复杂度为O(mlog_2m)O(mlog⁡_2m)的 Kruskal 算法。

证明

为了造出一棵最小生成树,我们从最小边权的边开始,按边权从小到大依次加入,如果某次加边产生了环,就扔掉这条边,直到加入了n−1条边,即形成了一棵树。

证明:使用归纳法,证明任何时候 K 算法选择的边集都被某棵 MST 所包含。

基础:对于算法刚开始时,显然成立(最小生成树存在)。

归纳:假设某时刻成立,当前边集为 F,令 T 为这棵 MST,考虑下一条加入的边 e。

如果 e 属于 T,那么成立。

否则,T+e一定存在一个环,考虑这个环上不属于 F 的另一条边 f(一定只有一条)。

首先,f的权值一定不会比e小,不然f会在 e之前被选取。

然后,f的权值一定不会比 e 大,不然T+e−f就是一棵比T还优的生成树了。

所以,T+e−f包含了F,并且也是一棵最小生成树,归纳成立。

代码实现

(以hdu1863为例)

#include<bits/stdc++.h>
using namespace std;

const int N = 1e2+10;
int fa[N];
int m,n;
struct edge {
    int u,v,w;
    bool operator < (const edge & a) const {
        return this->w < a.w;
    }
};
vector<edge> V;

void init() {
    for(int i = 1;i <= m; ++i) {
        fa[i] = i;
    }
    V.clear();
}

int find(int x) {
    while(x != fa[x]) x= fa[x];
    return x;
}

void kruskal() {
    int ans = 0;
    int cnt = m;
    for(int i = 0;i < V.size(); ++i) {
        int u = V[i].u;
        int v = V[i].v;
        int w = V[i].w;
        u = find(u);
        v = find(v);
        if(u != v) {
            fa[v] = u;
            ans += w;
            cnt--;
        }
    }
    if(cnt == 1) printf("%d\n",ans);
    else printf("?\n");
}

int main() 
{
    while(~scanf("%d%d",&n,&m) && n) {
        init();
        for(int i = 1;i <= n; ++i) {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            V.push_back({u,v,w});
        }
        sort(V.begin(),V.end());
        kruskal();
    }
    return 0;
}

Prim 算法

Prim 算法是另一种常见并且好写的最小生成树算法。该算法的基本思想是从一个结点开始,不断加点(而不是 Kruskal 算法的加边)。具体来说,每次要选择距离最小的一个结点,以及用新的边更新其他结点的距离。

其实跟 Dijkstra 算法一样,每次找到距离最小的一个点,可以暴力找也可以用堆维护。

堆优化的方式类似 Dijkstra 的堆优化,但如果使用二叉堆等不支持 O(1) decrease-key 的堆,复杂度就不优于 Kruskal,常数也比 Kruskal 大。所以,一般情况下都使用 Kruskal 算法,在稠密图尤其是完全图上,暴力 Prim 的复杂度比 Kruskal 优,但 不一定 实际跑得更快。所以一般不用prim算法

关于各种优化时间复杂度

暴力:O(n^2+m)

二叉堆:O((n+m)log_2n)

Fib 堆:O(nlog⁡_2n+m)

证明

从任意一个结点开始,将结点分成两类:已加入的,未加入的。

每次从未加入的结点中,找一个与已加入的结点之间边权最小值最小的结点。

然后将这个结点加入,并连上那条边权最小的边。

重复 n−1 次即可。

证明:还是说明在每一步,都存在一棵最小生成树包含已选边集。

基础:只有一个结点的时候,显然成立。

归纳:如果某一步成立,当前边集为 F,属于 T 这棵 MST,接下来要加入边 e。

如果 e 属于 T,那么成立。

否则考虑 T+e 中环上另一条可以加入当前边集的边 f。

首先,f 的权值一定不小于 e 的权值,否则就会选择 f 而不是 e 了。

然后,f 的权值一定不大于 e 的权值,否则 T+e−f 就是一棵更小的生成树了。

因此,e 和 f 的权值相等,T+e−f 也是一棵最小生成树,且包含了 F。

代码实现

(以hdu1863为例)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
#define P pair<int, int>
#define INF 0x3f3f3f3f

const int N = 1005;
int mp[N][N];
bool vis[N];
int dis[N];
int n,m;
int prim(int s) {
    for(int i = 1; i <=n ; ++i) {
        dis[i] = INF;
        vis[i] = false;
    }
    dis[s] = 0;
    int ans = 0;
    while(true) {
        int v = -1;
        for(int u = 1; u <= n; ++u) {
            if(!vis[u] && (v == -1 || dis[u] < dis[v])) {
                v = u;
            }
        }
        if(v == -1)
        break;
        vis[v] = true;
        ans += dis[v];
        for(int u = 1; u <= n; ++u) {
            dis[u] = min(dis[u], mp[u][v]);
        }
    }
    return ans; 
}

int main()
{
    int u,v,cost;
    while(~scanf("%d%d",&m, &n) && m) {
        memset(mp,INF,sizeof mp);
        for(int i = 0; i < m; ++i) {
            scanf("%d%d%d",&u, &v, &cost);
            if(mp[u][v] > cost)
            mp[u][v] = mp[v][u] = cost;
        }
        int ans = prim(1);
        bool is = true;
        if(ans < INF) {
            printf("%d\n",ans);
        } else {
            puts("?");
        }
    }
    return 0;
}

练习题目

题目连接 题目名
https://acm.dingbacode.com/showproblem.php?pid=1863 畅通工程
https://acm.dingbacode.com/showproblem.php?pid=1875 畅通工程再续
https://acm.dingbacode.com/showproblem.php?pid=1879 继续畅通工程
https://www.luogu.com.cn/problem/P3366 P3366 【模板】最小生成树
https://www.luogu.com.cn/problem/P2872 P2872 [USACO07DEC]Building Roads S
https://www.luogu.com.cn/problem/P1195 P1195 口袋的天空
https://www.luogu.com.cn/problem/P1194 P1194 买礼物
https://www.luogu.com.cn/problem/P2121 P2121 拆地毯
https://www.luogu.com.cn/problem/P1396 P1396 营救
https://www.luogu.com.cn/problem/P1991 P1991 无线通讯网
https://www.luogu.com.cn/problem/P4047 P4047 [JSOI2010]部落划分
https://acm.dingbacode.com/showproblem.php?pid=1598 find the most comfortable road

人均完成8题及以上吧

视频连接:https://www.bilibili.com/video/BV1wV411s7Pe


本当の声を響かせてよ