[图+最小生成树+模板] 三大最小生成树常用模板

0. 前言

重点在于代码实现。
在这里插入图片描述
一些好的博文总结:


图论中最小生成树问题一般有三种算法最为常用,如下:

  • 朴素 P r i m Prim Prim 算法:代码简单,很短,非常相似于 d i j k s t r a dijkstra dijkstra 算法。主要用来求解稠密图的最小生成树问题常用。时间复杂度 O ( n 2 ) O(n^2) O(n2)
  • 堆优化 P r i m Prim Prim 算法:代码稍微复杂,稀疏图可用,但该算法并不常用。优化部分也是相同于 d i j k s t r a dijkstra dijkstra。时间复杂度 O ( m l o g n ) O(mlogn) O(mlogn)
  • K r u s k a l Kruskal Kruskal 算法:代码短,时间复杂度固定,会对所有边从小到大进行排序,时间复杂度也被限制在了排序上,为 O ( m l o g m ) O(mlogm) O(mlogm)稀疏图常用。

最小生成树问题,均为 无向图。有向图最小生成树问题相当相当少。 且边权正负无所谓。

1. 朴素 Prim 算法求最小生成树

Biu

在这里插入图片描述
要点: 无向图、稠密图、邻接矩阵

思路:

  • 十分类似于 d i j k s t r a dijkstra dijkstra 算法
  • 初始化距离 d i s t [ i ] = I N F dist[i] = INF dist[i]=INF
  • 迭代 n n n 次,定义集合 s s s 为当前的生成树。每次找到不在 s s s 中距离最近的点,将其加入集合。再使用该点更新其他点到集合 s s s 的距离。
  • d i j k s t r a dijkstra dijkstra 算法不同在于更新的方式
    • d i j k s t r a dijkstra dijkstra 算法是用新加入点来更新到起点的距离
    • p r i m prim prim 算法是用新加入点来更新所有点到集合的距离

关于到集合距离的定义: 一个点到已选的生成树各种路径下的最短路径的那个距离。类比,条条大道通罗马,选择最短的一条。这些最短通向罗马的路构成最小生成树的一条条边,让整个图连通。

这里需要注意一点,在累加最小生成树的边的时候,需要在更新 dist 数组之前,因为会存在负自环情况,导致 dist 数组更新后,答案的错误累加。 即当我们选中了距离集合最近的点只后,我们认为该点连接到集合的边即为最小生成树的一条边,需要将其累加到答案中,假设点为 t,即需要 res+=dist[t]同时需要用该点再更新它的出边到集合的距离,即遍历所有它的出边,用出边的权值最小值与 dist 数组取最小值。在这时,如果存在一条负自环的话,可能会导致 dist[t] = min(dist[t], g[t][t]) = g[t][t] < 0 即将本该加到答案中的 dist[t] 再次更新为负自环的权值,显然最小生成树中是不应存在自环的,故这样的更新就导致答案错误。

所以,在累加答案需要放到更新 dist 数组之前。且即便在更新后,负自环将 dist[t] 更新为一个负值,但是由于 t 已经加入了集合中,即 st[t]=true,所以下一次是不会再选择到 t 这个点的,故这个负自环一定不会被加入到答案中。

环出现,意味着 a--->a 这条边需要被两次选择,但是当 a 被选择后,st[a]=true,那么 a 就一定不会被再次选择,故最短路、最小生成树中都是不存在环的。

代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 505, INF = 0x3f3f3f3f;

int n, m;
int g[N][N];
int dist[N];
bool st[N];

int prim() {
    memset(dist, 0x3f, sizeof dist);
    
    int res = 0;
    for (int i = 0; i < n; ++i) {
        int t = -1; 
        for (int j = 1; j <= n; ++j) {
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        }
        
        // 不是第一个点且到集合距离为INF,说明该点不连通
        if (i && dist[t] == INF) return INF;    
        
        // 在此累加需要放到更新之前,数据可能会出现自负环
        // 即g[t][t] = -10,他会将dist[t]更新为-10,导致后续累加边权错误
        // 但是最小生成树中是不包括自环的,需注意
        if (i) res += dist[t];                  // 连通,作为生成树的某一条边

        for (int j = 1; j <= n; ++j)            // 更新其他点到集合外各点的距离
            dist[j] = min(dist[j], g[t][j]);    // 注意更新距离的方式,
            // dijkstra 的更新方式,dist 表示当前点到 1 号点的距离
            // prim 表示当前点到集合的部分生成树的距离,即到集合某条边的距离
            // dist[j] = min(dist[j], dist[t] + g[t][j]);	
            
        st[t] = true;                           // 加入集合
    }
    return res;
}

int main() {
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    
    
    while (m --) {
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = g[b][a] = min(g[a][b], c);
    }
    int t = prim();
    if (t == INF) puts("impossible");
    else cout << t << endl;
    
    return 0;
}

// 链式前向星写法
#include <bits/stdc++.h>

using namespace std;

const int N = 1e5+5;

int n, m;
int h[N], e[N * 2], ne[N * 2], w[N * 2], idx;
int d[N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int prim() {
    memset(d, 0x3f, sizeof d);
    
    // 其实最小生成树从任意一点作为起点都可以,因为必然连通,作为起点到集合距离为 0 也行
    // d[1] = 0;		// 不应该再做初始化,1 号点不一定在最小生成树中,不应该将其加入集合
    
    int res = 0;
    for (int i = 0; i < n; i ++ ) {
        int t = -1;
        for (int j = 1; j <= n; j ++ ) 
            if (!st[j] && (t == -1 || d[j] < d[t]))
                t = j;
        
        if (i && d[t] == 0x3f3f3f3f) return -1;
        
        if (i) res += d[t];
        st[t] = true;
        
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            d[j] = min(d[j], w[i]);                 // 遍历 t 点的出边,到集合的距离,t 已经在集合中
        }
    }
    return res;
}

int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i ++ ) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    
    int res = prim();
    if (res == -1) puts("impossible");
    else printf("%d\n", res);
    
    return 0;
}

2. 堆优化 Prim 算法求最小生成树

同上题。这个写法不必掌握,有 K r u s k a l Kruskal Kruskal 算法完美解决稀疏图最小生成树问题。

代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 2e5+5;            // 数据范围需要开到 2e5+5,仅 1e5 是不够用的

int n, m;
int e[N], w[N], ne[N], h[N], idx;
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int prim() {
    priority_queue<PII, vector<PII>, greater<PII> > heap;   // 小顶堆
    heap.push({0, 1});          // 到集合距离---编号
    // 为了保持代码统一性,没在下面将 1 号点的 st[1] 直接置为 true
    
    int res = 0, cnt = 0;
    while (heap.size()) {
        auto t = heap.top(); heap.pop();
        
        int d = t.first, idx = t.second;
        
        // 这就是在外面,没将 st[1]=true 的原因,后续还有一大波更新需要做
        if (st[idx]) continue;      // 若该点已经被选中,那么直接continue即可,不需要更新它的信息,不需要更新它的出边
        st[idx] = true, res += d, cnt ++ ;      // 选中该点进入最小生成树,答案累加边权距离,cnt 为最小生成树中的点的个数
        
        for (int i = h[idx]; i != -1; i = ne[i]) {      // 遍历所有出边
            int j = e[i];
            if (!st[j])             // 如果其未被选中,则直接入堆即可,会自动挑选出来距离最小的一个
                heap.push({w[i], j});
        }
    }
    if (cnt != n) return 0x3f3f3f3f;    // 在此不要返回 -1,因为答案也可能是 -1,负权边也可以计算最小生成树
    return res;
}

int main() {
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i ++ ) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);     // 不需要针对重边做特殊处理,算法保证会选最小的边
    }
    
    int res = prim();
    if (res == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n", res);
    
    return 0;
}

3. Kruskal 算法求最小生成树

859. Kruskal算法求最小生成树

在这里插入图片描述
要点: 无向图、稠密图、邻接矩阵

思路:

  • 将所有边按权重从小到大排序, s o r t sort sort 函数搞定,这就是该算法的算法瓶颈 O ( m l o g m ) O(mlogm) O(mlogm)。但是排序算法的算法常数很小,所以还是很优秀的。
  • 从小到大枚举每条边,如果不连通,则将该边加入到集合。总共 m m m 条边,并查集时间复杂度 O ( 1 ) O(1) O(1)。所以该步骤总的时间复杂度为 O ( m ) O(m) O(m)

关于第二步加入集合,就是个并查集的应用。一开始每个点都是单独的一部分,然后从小到大枚举每条边

[并查集] 连通块中点的数量(模板+维护集合元素个数)

代码:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 2e5+5;

int n, m;
int p[N];

struct Edge {
    int a, b, w;
    
    bool operator< (const Edge &W) const {
        return w < W.w;                     // 按权重从小到大排序
    }
}edges[N];

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


int main() {
    cin >> n >> m;
    
    // 虽然是无向图,但是也只存单向边。当然存双向边也是可以的
    // 但是权值一样,排序后必然相邻,find(a)==find(b) 与 find(b)==find(a) 意义一样
    // 故双向边另一个方向不做操作,存双向边没有意义,故简化到单向边也行
    for (int i = 0; i < m; ++i) {
        int a, b, w;
        cin >> a >> b >> w;
        edges[i] = {a, b, w};
    }
    
    sort(edges, edges + m);
    
    for (int i = 1; i <= n; ++i) p[i] = i;
    
    // 与堆优化版prim算法相比,这个cnt记录的是加入边的数量,故不能写成 if (cnt!=n) return -1;
    // prim 记录的是加入点的数量,故写成 if (cnt != n) return -1;
    // 在此边应该有 n-1 条,故写成 if (cnt!=n-1) return -1; 也是正确的
    int res = 0, cnt = 0;           // res存最小生成树的权重和,cnt当前加入边的数量
    for (int i = 0; i < m; ++i) {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;
        
        a = find(a), b = find(b);   // 找到起点a,终点b的祖先
        
        // 不在一个集合内,才连边,连 n-1 条边,必然能将 n 个点连通,即最小生成树
        if (a != b) {               // 如果不在一颗子树内,那么就将其合并,合并集合
            p[a] = b;               
            res += w;
            cnt ++;
        }
                                    // 如果在一颗子树内,说明已经连通了,再将该边选入最小生成树则会成环,所以不做操作
    }
    
    // 将其放入循环内判断也行
    if (cnt < n - 1) puts("impossible");    // n个点的最小生成树应该有n-1条边,如果小于n-1条边则无法构成最小生成树,即无法覆盖所有点
    else cout << res << endl;
    
    return 0;
}

4. 总结

最小生成树是由连通带权无向图中一个权值最小的生成树。我们一般采用 2 个算法来解决稀疏图、稠密图的问题。堆优化 P r i m Prim Prim 用的很少很少。

总结来讲:

  • p r i m prim prim 算法十分相似于 d i j k s t r a dijkstra dijkstra 算法,仅更新方式不同。
    • d i j k s t r a dijkstra dijkstra 是用来求解单源最短路问题, d i s t [ j ] dist[j] dist[j] 是 1 号点到 j j j 号点的最短距离。 d i s t [ j ] = m i n ( d i s t [ j ] , d i s t [ t ] + g [ t ] [ j ] ) ; dist[j] = min(dist[j], dist[t] + g[t][j]); dist[j]=min(dist[j],dist[t]+g[t][j]);
    • p r i m prim prim 算法 d i s t [ j ] dist[j] dist[j] 指的是 j j j 号点到生成树集合的最短距离,更新方式为 d i s t [ j ] = m i n ( d i s t [ j ] , g [ t ] [ j ] ) ; dist[j] = min(dist[j], g[t][j]); dist[j]=min(dist[j],g[t][j]); 通过点+权的方法求得最小生成树。在寻找最小生成树的过程中始终保证了它是一颗树,然后逐步覆盖到所有的点形成最小生成树,每次遍历确定了最小生成树的一个点,将其加入到集合中,然后下次再从集合外的点选个距离最小的加进来。
  • K r u s k a l Kruskal Kruskal 算法基于边+权的方法。直接按边权排序,从小到大挑边将其加入到集合中,在寻找最小生成树的过程中可能在图内存在很多棵树,但使用并查集保证最小生成树中不存在环,并进行了树的合并操作,遍历完所有的边就得到了最小生成树。

还是那句话,图论问题,背好模板,多刷题,多掌握建图、抽象的方法和技巧。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值