【板子】最小生成树MST

一、Kruskal

适用:稀疏图、边少、需要自定义边过滤、多组数据通用,代码极简 核心:边排序 + 并查集判环 复杂度:(O(mlog m))

1. 通用模板(整数权,无向图)

复制代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;

// 边结构体
struct Edge {
    int u, v, w;//w适配带权
    bool operator<(const Edge& other) const {
        return w < other.w;
    }
};

// 并查集
vector<int> fa;
int find(int x) {
    if (fa[x] != x) fa[x] = find(fa[x]);
    return fa[x];
}

int main() {
    int n, m;
    while (scanf("%d%d", &n, &m) == 2) {
        vector<Edge> e;
        fa.resize(n + 1);
        for (int i = 1; i <= n; ++i) fa[i] = i;//并查集预处理

        // 读入所有边
        for (int i = 0; i < m; ++i) {
            int u, v, w;
            scanf("%d%d%d", &u, &v, &w);
            e.push_back({u, v, w});
        }

        sort(e.begin(), e.end());
        int sum = 0, cnt = 0;
        for (auto& ed : e) {
            int fu = find(ed.u), fv = find(ed.v);
            if (fu != fv) {
                fa[fu] = fv;
                sum += ed.w;
                cnt++;
                if (cnt == n - 1) break;
            }
        }
        // cnt<n-1 代表图不连通,保证连通的题目不需要判断
        printf("%d\n", sum);
    }
    return 0;
}

2. 浮点权改版(球体 / 坐标距离题)

仅修改 Edge 权值为 double,输入输出适配浮点

复制代码
struct Edge {
    int u, v;
    double w;
    bool operator<(const Edge& other) const {
        return w < other.w;
    }
};

3. 已有道路预合并变形(畅通工程类)

读完所有边后,先执行若干次 find+合并(并已有边),再跑 Kruskal 累加新增道路花费。


二、Prim

适用:稠密图(n<2000)、点多边极多,无需存储全部边

版本 1:堆优化 Prim(推荐,(O(mlog n))

小根堆优化核心思路

把「找全局最小 dis 的点」这个操作交给小根堆(优先队列),堆内部自动维护最小值,取出最小值仅需 O(log n)。

完整执行流程

  1. 初始化

    • dis[] 全部设无穷大,起点 dis[start]=0
    • (0, start) 压入小根堆;
    • vis[] 标记点是否已加入最小生成树。
  2. 循环取出堆顶:

    1. 弹出堆顶 (w, u),这是当前距离最小的未处理点;

    2. 如果 u 已经 vis[u]=true,直接跳过(堆里存在旧的、失效的松弛记录);

    3. 否则标记 vis[u]=true,把 w 计入总 MST 权值;

    4. 遍历所有其他点 v,计算 (u- v) 的边权 d; 若 (d < disv):

      • 更新 dis[v] = d
      • 把新状态 (d, v) 推入堆;

      不删除堆里原来旧的、更大的 (dis[v],v),留在堆里,后续弹出时直接跳过。

  3. 直到生成树包含全部 n 个点,结束。

    #include
    #include
    #include
    #include
    #include
    #include
    using namespace std;

    const int MAXN = 2005;
    const int INF = 0x3f3f3f3f;

    int dis[MAXN];
    bool vis[MAXN];

    int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n;
    while (cin >> n && n) {
    // 初始化
    memset(dis, 0x3f, sizeof dis);
    memset(vis, 0, sizeof vis);
    dis[1] = 0;
    // 小根堆 pair<权值,点>
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
    q.push({0, 1});

    复制代码
         int total = 0, num = 0;
         while (!q.empty()) {
             auto cur = q.top(); q.pop();
             int w = cur.first;
             int u = cur.second;
             if (vis[u]) continue;
             vis[u] = true;
             total += w;
             num++;
             if (num == n) break;
    
             // 遍历所有点计算边权(稠密图核心)
             for (int v = 1; v <= n; ++v) {
                 if (!vis[v]) {
                     int d = getDist(u, v); // 自定义求两点距离函数
                     if (d < dis[v]) {
                         dis[v] = d;
                         q.push({d, v});
                     }
                 }
             }
         }
         cout << total << endl;
     }
     return 0;

    }

版本 2:邻接矩阵朴素 Prim((O(n^2)),n≤100)

适合村庄邻接矩阵输入,代码最简单

复制代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 105;
const int INF = 0x3f3f3f3f;

int g[MAXN][MAXN];
int dist[MAXN];
bool vis[MAXN];

int main() {
    int n;
    while (scanf("%d", &n) == 1) {
        memset(dist, 0x3f, sizeof dist);
        memset(vis, 0, sizeof vis);
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                scanf("%d", &g[i][j]);//一般这个会让你自己求距离加入,给出的是坐标

        dist[1] = 0;
        int ans = 0;
        for (int i = 1; i <= n; ++i) {//代表一共循环找n次点
            // 遍历n个点找未访问最小点
            int u = -1, minv = INF;
            for (int j = 1; j <= n; ++j) {
                if (!vis[j] && dist[j] < minv) {
                    minv = dist[j];
                    u = j;
                }
            }
            vis[u] = true;
            ans += minv;
            // 更新邻点,把新加入的点到邻近点的距离更新以用于下一次找最近未加入点
            for (int v = 1; v <= n; ++v) {
                if (!vis[v] && g[u][v] < dist[v])
                    dist[v] = g[u][v];
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

三、两种算法选型对比

场景 推荐算法 理由
边少、稀疏图、需要过滤合法边 Kruskal 只存有效边,逻辑直观
(n> 500) 稠密图、两两生成距离 堆优化 Prim 不用存百万条边,省内存
(n<100),输入邻接矩阵 朴素 Prim 代码最短,无需建边数组
存在预先连通的点(已修路) Kruskal 并查集提前合并极其方便

四、高频变形套路

  1. 坐标求距离:两点((x_1,y_1),(x_2,y_2)) (dist=sqrt{(x_1-x_2)^2+(y_1-y_2)^2}) 三维再加 z 维度;
  2. 卫星 / 多连通块:MST 边排序后,取倒数第 S 条边作为答案/限制最短点间距;
  3. 已有道路:Kruskal 先合并,不计代价;
  4. 浮点输出printf("%.2lf", ans) / %.3lf
  5. 无解判断 :最终选中边数 \(cnt < n-1\),输出 oh!
  6. 单点 (n=1):总花费直接为 0。

五、通用注意点

  1. 多组数据:faedisvis 必须每组重新初始化,不能用全局静态数组不重置;
  2. 点编号:题目字母 A~Z 转数字 c-'A';1~N 直接使用;
  3. 距离平方用 long long 防止 int 溢出;
  4. Kruskal 排序是从小到大,保证贪心选最小边。

MST 所有变式题型汇总 + 板子调用操作 + 选型使用指南

一、全部题型概括

题型 1:基础无向图最小生成树(布线题)

题意 :给若干点、无向带权边,保留最少总权值的边连通所有点,输出总权。 输入 :点数量、多条边(两点 + 权);0 结束多组。 板子选用 :Kruskal 调用操作

  1. 建 Edge 数组存所有边;
  2. 并查集每组数据重新初始化;
  3. 边从小到大排序;
  4. 依次合并两点,累加边权,凑够 \(n-1\) 条边终止;
  5. 输出总权。

题型 2:邻接矩阵输入(已有道路)

题意 :给出 (n*n) 两两距离矩阵,部分村庄已经通路,只新建最少总长度道路连通全图。 输入 :n + n 行矩阵 + 已有道路数量 Q + Q 组连通点对。 板子选用 :Kruskal(优先)/ 朴素邻接矩阵 Prim 调用操作

  1. 遍历矩阵 (i<j) 生成所有无向边;
  2. 并查集先把题目给出的已连通点合并(不计代价);
  3. 排序所有边,正常跑 Kruskal,累加新增道路长度;
  4. 输出累加和。

题型 3:坐标点求距离(百岛湖、空间站球体)

题意 :给出二维 / 三维坐标,两点距离为边权;存在距离限制(仅区间内可建边),求最小总造价;无法连通输出 oh!输入 :多组数据,点数 C,每组坐标 ((x,y)/(x,y,z))。 板子选用

  • (n<100):Kruskal;
  • (n>500) 稠密点集:堆优化 Prim。 调用操作
  1. 枚举所有两点,计算欧几里得距离;
  2. 过滤不满足距离限制的边直接丢弃;
  3. 正常跑 MST;
  4. 统计成功合并边数,不足 (n-1) 输出无解,否则总距离 × 单价输出(浮点保留小数)。

题型 4:稠密海量点集(卡车编码汉明距离,(n=2000))

题意 :n 个 7 位字符串,两点边权为汉明距离,求 MST 总权。 输入 :多组,n 个字符串,0 结束。 板子选用 :堆优化 Prim(强制,Kruskal 存 200 万条边会超时 / 爆内存) 调用操作

  1. 不用预存全部边,松弛时实时计算两点汉明距离;
  2. 小根堆维护最小松弛距离;
  3. 跳过堆内过期记录;
  4. 累加总权,按固定格式输出。

题型 5:卫星信道 MST 变形(多连通块)

题意 :S 个卫星可让任意 S 个连通块互通,求最小无线电距离 D(MST 中第 \(P-S\) 长边)。 输入 :多组用例,卫星数 S、哨站数 P,所有哨站坐标。 板子选用 :Kruskal / 堆 Prim 均可 调用操作

  1. 求出完整 MST,把 MST 所有边权存入数组升序排序;
  2. 一共需要 (P-1) 条边,卫星抵消 (S-1) 条最长边;
  3. 取排序后第 (P-S) 条边的权值,保留两位小数输出。

题型 6:特殊边界:单点 (n=1)

题意 :只有一个村庄,无需任何道路,总花费为 0。 调用操作:输入读取点数量后直接输出 0,跳过建图、MST 流程。

题型 7:球体天然连通(空间站)

题意 :两球球心距离 ≤ 半径和,天然连通,边权置 0。 调用操作:计算距离后判断,满足则边权设 0,正常跑 MST。

题型 8:一条路可置零

最小树性质,可以求出直接删最长一个节点,最短路需要分层

特殊辩题:秦始皇

生成树时维护两点间最大一条路,两点归零后会断掉最长连通

需要:prim,记录更新节点时距离是连接的哪个点,更新一遍到前面每个点

Matlab 复制代码
#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
#define endl '\n'
#define ll long long
#define debug(x) cout<< #x << ' ' << x << endl;

const int MAXX = 1e7 + 10;
const int inf = 1e9 + 10;
const ll INF = 1e18 + 10;

struct City{int x,y,p;};
double prim(int n, const vector<vector<pair<int,double>>>& g,vector<vector<double>>& j) {
    const long long INF = 1e18;
    vector<double> min_e(n, INF);
    vector<bool> in_mst(n, false);
    min_e[0] = 0;
    priority_queue<pair<double,int>, vector<pair<double,int>>, greater<>> pq;
    pq.push({0, 0});
    double total = 0;
    int cnt = 0;
    while (!pq.empty()) {
        auto [w, u] = pq.top(); pq.pop();
        if (in_mst[u]) continue;
        in_mst[u] = true;
        total += w;
        cnt++;
        for (auto [v, wt] : g[u]) {
            if (!in_mst[v] && wt < min_e[v]) {
                min_e[v] = wt;
                for(int i = 0 ; i < n;i++){
                    if(!in_mst[i]) continue;
                    j[i][v]=max(j[i][u],wt);
                    j[v][i]=max(j[u][i],wt);
                }
                pq.push({wt, v});
            }
        }
    }
    return cnt == n ? total : -1;
}

void test(){
    int n;cin>>n;
    vector<City> cs(n);
    for(int i = 0 ; i < n;i++){
        cin>>cs[i].x>>cs[i].y>>cs[i].p;
    }
    vector<vector<pair<int, double>>> g(n);
    vector<vector<double>> len(n,vector<double>(n,0));
    for(int i = 0 ; i < n;i++){
        for(int j = i+1 ; j < n;j++){
            double dis = sqrt((
                (cs[i].x-cs[j].x)*(cs[i].x-cs[j].x)+
                (cs[i].y-cs[j].y)*(cs[i].y-cs[j].y)
            ));
            g[i].push_back({j,dis});
            g[j].push_back({i,dis});
        }
    }
    double total = prim (n,g,len);
    double ans=0;
    for(int i = 0; i < n;i++){
        for(int j = i+1 ; j < n;j++){
            double res = (cs[i].p+cs[j].p)/(total - len[i][j]);
            ans = max(res,ans);
        }
    }
    cout<<fixed<<setprecision(2)<<ans<<endl;

}

int main(){
    IOS
    int t ;cin >> t;
    while(t--){
        test();
    }
    return 0;
}

二、两套板子适配所有变式的统一使用说明

1. Kruskal(并查集 + 边排序)适用场景 & 修改点

适用场景
  1. 稀疏图、边总数 ≤ 1e4;
  2. 需要提前合并已有连通点(已修路);
  3. 需要过滤合法边(距离区间限制);
  4. 需要收集 MST 所有边后续处理(卫星题取第 k 长边);
  5. 点数小 \(n\le 200\)。
通用修改模板(应对变式)
  1. 边权为浮点:Edgew 改为 double
  2. 坐标生成边:双重循环枚举 i<j,计算距离存入边;
  3. 已有道路:读入点对后先 find 合并;
  4. 距离过滤:判断距离条件,不合法不加入边数组;
  5. 无解判断:统计合并成功的边数 cnt,\(cnt<n-1\) 输出无解;
  6. 多组数据:vector<Edge>fa 每组循环内重新定义,自动清空。
优缺点
  • 优点:变形修改逻辑简单,预处理连通块极其方便;
  • 缺点:稠密大点集((n>1000))存边过多,排序超时。

2. 堆优化 Prim(小根堆)适用场景 & 修改点

适用场景
  1. 稠密图、点数量大 (n>500),两两生成距离;
  2. 无法存储全部边(百万条边内存超限);
  3. 字符串汉明距离、坐标实时算权,无需预存边。
通用修改模板(应对变式)
  1. 边权浮点:dis 数组改为 double,堆存 pair<double, int>
  2. 自定义距离:松弛循环内调用 getDist(u,v) 实时计算;
  3. 距离限制:计算距离后不符合条件直接跳过更新;
  4. 无解判断:最后统计访问点数量,不足 n 代表不连通;
  5. 多组数据:disvis 数组每组 memset 重置无穷大 / 0。
优缺点
  • 优点:无需存储全部边,稠密图省内存;
  • 缺点:无法提前合并已连通点,处理 "已有道路" 不如 Kruskal 方便。

3. 朴素邻接矩阵 Prim

适用场景

仅 (n<100),题目直接给邻接矩阵输入(畅通工程类)。

局限

点超过 100 空间浪费,不适合坐标、字符串类题目。