最小生成树

最小生成树

1. 最小生成树原理

  • 最小生成树算法是指:Prim算法Krusskal算法。下面给出两个算法的算法过程以及正确性证明(证明都是相同的)

最小生成树的理论基础

(1)任何一条最小生成树一定可以包含无向图中权值最小的边;

(2)给定一张无向图G=(V, E),n=|V|,m=|E|。从E中选出k(<n-1)条边构成G的一个生成森林,若再从剩余的m-k条边中选n-1-k条边添加到生成森林中,使其成为G的生成树,并且选出的边的权值之和最小,则该生成树一定可以包含m-k条边中连接生成森林的两个不连通节点的权值最小的边。

Prim原理

在这里插入图片描述

Krusskal算法

在这里插入图片描述

AcWing 858. Prim算法求最小生成树

代码模板

#include <cstring>
#include <iostream>

using namespace std;

const int N = 510, INF = 0x3f3f3f3f;

int n, m;  // 点数,边数
int g[N][N];  // 邻接矩阵
int dist[N];  // dist[j] 表示从已经求得mst的集合中到达j点的最短的一条
bool st[N];  // 已在mst集合中的点

int prim() {
    memset(dist, 0x3f, sizeof dist);

    int res = 0;  // 记录整个图的MST最小权值和
    for (int i = 0; i < n; ++i) {
        // 寻找当前与mst集合连接的最小的边对应的顶点t
        int t = -1;
        for (int j = 1; j <= n; j++)
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        if (i && dist[t] == INF) return INF;  // 如果图不连通的话,不存在MST
        if (i) res += dist[t];  // 这句话要放在更新的前面,否则,存在负权自环会更新dist[t]
        st[t] = true;  // 点t放入mst中

        for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}

int main() {

    scanf("%d%d", &n, &m);

    memset(g, 0x3f, sizeof g);

    while (m--) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = g[b][a] = min(g[a][b], c);
    }

    int t = prim();

    if (t == INF) puts("impossible");
    else printf("%d\n", t);

    return 0;
}

AcWing 859. Kruskal算法求最小生成树

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 200010;

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() {

    scanf("%d%d", &n, &m);

    for (int i = 0; i < m; i++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }

    for (int i = 0; i <= n; i++) p[i] = i;  // 并查集

    // Kruskal算法
    sort(edges, edges + m);

    int res = 0, cnt = 0;  // res存储MST权值和,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);
        if (a != b) {
            p[a] = b;
            res += w;
            cnt++;
        }
    }

    if (cnt < n - 1) puts("impossible");
    else printf("%d\n", res);

    return 0;
}

2. 最小生成树

AcWing 1140. 最短网络

问题描述

分析

  • 这个题就是让求解最小生成树,这里使用prim算法

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 110;

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

int prim() {
    
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    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;
                
        res += dist[t];
        st[t] = true;
        
        for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);
    }
    
    return res;
}

int main() {
    
    cin >> n;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            cin >> g[i][j];
    
    cout << prim() << endl;
    
    return 0;
}

AcWing 1141. 局域网

问题描述

分析

  • 整个局域网可以看成一个图,但是这个图不一定是连通图。可以看成kruskal算法只求前一部分数据。
  • 本题相当于在这个图的每个连通块内,求一棵生成树。如果要使得拔出网线的和最大,就需要使得剩余的网线的权值和最小,因此相当于求解每个连通块内的最小生成树。相当于求原图的"生成森林"。
  • 这一题使用prim算法不容易写,因为prim算法是从一个点开始向外扩散,我们需要对每个连通块单独处理。因此本题采用kruskal算法求解。
  • 虽然这个图可能不是连通的,但是直接对该图使用kruskal算法求解也是正确的。

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110, M = 210;

int n, m;
int p[N];

struct Edge {
    int a, b, w;
    
    bool operator< (const Edge &e) const {
        return w < e.w;
    }
} edges[M];

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

int main() {
    
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        edges[i] = {a, b, c};
    }
    
    for (int i = 1; i <= n; i++) p[i] = i;
    
    sort(edges, edges + m);
    
    int res = 0;
    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);
        if (a != b) p[a] = b;
        else res += w;
    }
    
    cout << res << endl;
    
    return 0;
}

AcWing 1142. 繁忙的都市

问题描述

分析

  • 相当于让我们求一棵生成树,使得这棵生成树中道路分值最大的边在所有的生成树中是最小的。
  • 对比:普通的最小生成树:所有的边权之和最小;本题中的最小生成树:最大的边权最小
  • 这一题可以使用二分,二分边权最大的边的值,然后判断边长小于等于该值的图能否使图中所有点连通即可。这里不适用这种方法求解。
  • 直接使用kruskal算法求解,在过程中记录边权的最大值即可。证明方式可以参考前面的最小生成树原理。

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 310, M = 8010;

int n, m;
int p[N];

struct Edge {
    int a, b, w;
    
    bool operator< (const Edge &e) const {
        return w < e.w;
    }
} edges[M];

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

int main() {
    
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }
    
    sort(edges, edges + m);
    
    for (int i = 1; i <= n; i++) p[i] = i;
    
    int res = 0;
    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);
        if (a != b) {
            p[a] = b;
            res = w;
        }
    }
    
    cout << n - 1 << ' ' << res << endl;
    
    return 0;
}

AcWing 1143. 联络员

问题描述

分析

  • 对于必须连接起来的点直接连接起来,并合并到一个集合中即可。
  • 对于所有选择性通信渠道,按照kruskal算法处理即可。
  • 可以看成kruskal算法只求后一部分数据。

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 2010, M = 10010;

int n, m;
int p[N];
struct Edge {
    int a, b, w;
    bool operator< (const Edge &e) const {
        return w < e.w;
    }
} edges[M];

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

int main() {
    
    cin >> n >> m;
    for (int i = 1; i <= n; i++) p[i] = i;
    
    int res = 0, k = 0;  // k: 可选边的条数
    for (int i = 0; i < m; i++) {
        int t, a, b, w;
        cin >> t >> a >> b >> w;
        if (t == 1) {
            res += w;
            p[find(a)] = find(b);
        } else edges[k++] = {a, b, w};
    }
    
    sort(edges, edges + k);
    
    for (int i = 0; i < k; i++) {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;
        
        a = find(a), b = find(b);
        if (a != b) {
            p[a] = b;
            res += w;
        }
    }
    
    cout << res << endl;
    
    return 0;
}

AcWing 1144. 连接格点

问题描述

分析

  • 首先这个题目需要抽象一下,我们将点阵中所有的点看成图中的点,点与点之间如果能直接到达,则连接一条无向边,横边权重为2,纵边权重为1。
  • 因为一共有n*m个点,我们最少需要n*m-1条边才能将所有点连通,因为边权不是1就是2,是大于0的,为了使得边权最小,我们只需要选择n*m-1条边即可。
  • 这一题相当于问:一些点已经连接起来了,求在剩余的边中添加哪些边可以使得所有点连通且花费最小。和上一题做法一样,使用kruskal算法求解即可。
  • 这一题最多有 1 0 6 10^6 106条边, 2 × 1 0 6 2\times10^6 2×106条边。因为边权只有1和2,可以在建图的时候先将纵向边加入,再把横向边加入,这样就不需要对边进行排序了。
  • 另外需要注意下面的问题不是最小生成树问题:给定n个点,m条边,边权可正可负。求将所有点连通的最小边权和是多少?

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 1010, M = N * N, E = 2 * N * N;

int n, m, k;  // n行m列,一共k个点
int ids[N][N];  // 左上角点为(1,1),将二维的点映射为一维
struct Edge {
    int a, b, w;
} edges[E];
int p[M];  // 并查集

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

void get_edges() {
    
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}, du[4] = {1, 2, 1, 2};
    
    for (int u = 0; u < 2; u++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++)
                for (int d = 0; d < 4; d++) 
                    if (d % 2 == u) {  // 保证先加入竖边
                        int x = i + dx[d], y = j + dy[d], w = du[d];
                        if (x > 0 && x <= n && y > 0 && y <= m) {
                            int a = ids[i][j], b = ids[x][y];
                            if (a < b) edges[k++] = {a, b, w};
                        }
                    }
}

int main() {
    
    cin >> n >> m;
    
    // 将二维的点映射为一维
    for (int i = 1, t = 1; i <= n; i++)
        for (int j = 1; j <= m; j++, t++)
            ids[i][j] = t;
    
    for (int i = 1; i <= n * m; i++) p[i] = i;
    
    int x1, y1, x2, y2;
    while (cin >> x1 >> y1 >> x2 >> y2) {
        int a = ids[x1][y1], b = ids[x2][y2];
        p[find(a)] = find(b);
    }
    
    get_edges();  // 建立所有的边

    int res = 0;
    for (int i = 0; i < k; i++) {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;
        
        a = find(a), b = find(b);
        if (a != b) {
            p[a] = b;
            res += w;
        }
    }
    
    cout << res << endl;
    
    return 0;
}

3. 最小生成树的扩展应用

AcWing 1146. 新的开始

问题描述

分析

  • 分析此题可知,图中的每个点要么自己有发电站(即不需要与其他点相连通,但耗费为 v i v_i vi),要么和其他已经供电的点连通。
  • 解决此题我们可以设置一个虚拟源点,不妨设为0号点(代码中也是这样实现的)从这个源点向其他所有的点连一条边,权值为 v i v_i vi,此时我们得到一张新图,我们在这张新图上求最小生成树就可以得到最小花费。如果MST中有从0号点到其他点的边,则说明在该点直接修电站比较划算。

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 310;

int n;
int g[N][N];
int dist[N];
bool st[N];  // 是否在MST中

int prim() {
    
    memset(dist, 0x3f, sizeof dist);
    dist[0] = 0;
    
    int res = 0;
    for (int i = 0; i < n + 1; i++) {
        
        int t = -1;
        for (int j = 0; j <= n; j++)
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        
        st[t] = true;
        res += dist[t];
        
        for (int j = 0; j <= n; j++)
            dist[j] = min(dist[j], g[t][j]);
    }
    
    return res;
}

int main() {
    
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &g[0][i]);
        g[i][0] = g[0][i];
    }
    
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            scanf("%d", &g[i][j]);
    
    printf("%d\n", prim());
    
    return 0;
}

AcWing 1145. 北极通讯网络

问题描述

分析

  • 分析可知本问题可以转化成:找到一个最小的d值,使得将所有权值大于d的边删去之后,整个图形的连通块的个数不超过k
  • 我们可以使用二分解决这个问题:每次二分一个mid值,大于mid代表不连通,小于等于mid代表连通,然后通过BFS或者DFS或者并查集判断连通块的数量。直到二分到答案为止。这种方法本题就不写了,下面演示使用kruskal算法解决该问题。

在这里插入图片描述

代码

  • C++
#include <iostream>
#include <algorithm>
#include <cmath>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 510, M = N * N / 2;

int n, m, k;  // 点数、边数、卫星数
struct Edge {
    int a, b;
    double w;
    
    bool operator< (const Edge &e) {
        return w < e.w;
    }
} edges[M];
PII q[N];  // 存储读入村庄的坐标
int p[N];  // 并查集

double get_dist(PII a, PII b) {
    int dx = a.x - b.x, dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
}

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

int main() {
    
    cin >> n >> k;
    for (int i = 0; i < n; i++) cin >> q[i].x >> q[i].y;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < i; j++) 
            edges[m++] = {i, j, get_dist(q[i], q[j])};
    
    for (int i = 0; i < n; i++) p[i] = i;
    sort(edges, edges + m);
    
    double res = 0;
    int cnt = n;  // 连通块个数
    for (int i = 0; i < m; i++) {
        if (cnt <= k) break;
        
        int a = edges[i].a, b = edges[i].b;
        double w = edges[i].w;
        a = find(a), b = find(b);
        if (a != b) {
            p[a] = b;
            cnt--;
            res = w;
        }
    }
    
    printf("%.2lf\n", res);
    
    return 0;
}

AcWing 346. 走廊泼水节

问题描述

分析

  • 本题的做法:从小到大依次枚举每条树边(a, b, w),让a所在的连通分量中的所有点和b所在的连通分量中的所有点连接边,边权为w+1,这样增加的边的权值总和最小。

在这里插入图片描述

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 6010;

int n;
struct Edge {
    int a, b, w;
    
    bool operator< (const Edge &e) {
        return w < e.w;
    }
} e[N];
int p[N], sz[N];  // 并查集

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

int main() {
    
    int T;
    cin >> T;
    while (T--) {
        
        cin >> n;
        for (int i = 0; i < n - 1; i++) {
            int a, b, c;
            cin >> a >> b >> c;
            e[i] = {a, b, c};
        }
        
        sort(e, e + n - 1);
        
        for (int i = 1; i <= n; i++) p[i] = i, sz[i] = 1;
        
        int res = 0;
        for (int i = 0; i < n - 1; i++) {
            int a = find(e[i].a), b = find(e[i].b), w = e[i].w;
            if (a != b) {
                res += (sz[a] * sz[b] - 1) * (w + 1);
                p[a] = b;
                sz[b] += sz[a];
            }
        }
        
        cout << res << endl;
    }
    
    return 0;
}

AcWing 1148. 秘密的牛奶运输

问题描述

分析

在这里插入图片描述

代码

  • C++
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 510, M = 10010;

int n, m;
struct Edge {
    int a, b, w;
    bool f;  // 该边是否在MST中
    
    bool operator< (const Edge &e) {
        return w < e.w;
    }
} edges[M];
int p[N];
int dist1[N][N];  // MST中任意两点所在路径中边权最大值
int dist2[N][N];  // MST中任意两点所在路径中边权严格次大值
int h[N], e[N * 2], w[N * 2], ne[N * 2], idx;  // 存储MST

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

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

// u: 当前考察的点; fa: u的父节点
// maxd1: 当前路径上的边权最大值; d1: 需要被赋值的最大值数组
// maxd2: 当前路径上的边权严格次大值; d2: 需要被赋值的严格次大值数组
void dfs(int u, int fa, int maxd1, int maxd2, int d1[], int d2[]) {
    d1[u] = maxd1, d2[u] = maxd2;
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (j != fa) {
            int td1 = maxd1, td2 = maxd2;
            if (w[i] > td1) td2 = td1, td1 = w[i];
            else if (w[i] < td1 && w[i] > td2) td2 = w[i]; 
            dfs(j, u, td1, td2, d1, d2);
        }
    }
}

int main() {
    
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }
    
    // 求解MST
    sort(edges, edges + m);
    for (int i = 1; i <= n; i++) p[i] = i;
    LL sum = 0;  // MST权值和
    for (int i = 0; i < m; i++) {
        int a =  edges[i].a, b = edges[i].b, w = edges[i].w;
        int pa = find(a), pb = find(b);
        if (pa != pb) {
            p[pa] = pb;
            sum += w;
            edges[i].f = true;
            add(a, b, w), add(b, a, w);
        }
    }

    // 求解dist1、dist2数组
    for (int i = 1; i <= n; i++) dfs(i, -1, 0, 0, dist1[i], dist2[i]);
    
    // 求解次小生成树
    LL res = 1e20;
    for (int i = 0; i < m; i++)
        if (!edges[i].f) {  // 只有是非树边才执行
            int a =  edges[i].a, b = edges[i].b, w = edges[i].w;
            LL t;
            if (w > dist1[a][b])
                t = sum + w - dist1[a][b];
            else if (w > dist2[a][b]) 
                t = sum + w - dist2[a][b];
            res = min(res, t);
        }
    
    printf("%lld\n", res);
    
    return 0;
}

4. 力扣上的最小生成树题目

  • Leetcode 1489 找到最小生成树里的关键边和伪关键边 ----> 题目链接 ----> 我的解答:JavaC++ (Kruskal)
  • Leetcode 1584 连接所有点的最小费用 ----> 题目链接 ----> 我的解答:JavaC++ (prim)
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值