一、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)。
完整执行流程
-
初始化
dis[]全部设无穷大,起点dis[start]=0;- 把
(0, start)压入小根堆; vis[]标记点是否已加入最小生成树。
-
循环取出堆顶:
-
弹出堆顶
(w, u),这是当前距离最小的未处理点; -
如果 u 已经
vis[u]=true,直接跳过(堆里存在旧的、失效的松弛记录); -
否则标记
vis[u]=true,把 w 计入总 MST 权值; -
遍历所有其他点 v,计算 (u- v) 的边权 d; 若 (d < disv):
- 更新
dis[v] = d; - 把新状态
(d, v)推入堆;
不删除堆里原来旧的、更大的
(dis[v],v),留在堆里,后续弹出时直接跳过。 - 更新
-
-
直到生成树包含全部 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 | 并查集提前合并极其方便 |
四、高频变形套路
- 坐标求距离:两点((x_1,y_1),(x_2,y_2)) (dist=sqrt{(x_1-x_2)^2+(y_1-y_2)^2}) 三维再加 z 维度;
- 卫星 / 多连通块:MST 边排序后,取倒数第 S 条边作为答案/限制最短点间距;
- 已有道路:Kruskal 先合并,不计代价;
- 浮点输出 :
printf("%.2lf", ans)/%.3lf; - 无解判断 :最终选中边数 \(cnt < n-1\),输出
oh!; - 单点 (n=1):总花费直接为 0。
五、通用注意点
- 多组数据:
fa、e、dis、vis必须每组重新初始化,不能用全局静态数组不重置; - 点编号:题目字母 A~Z 转数字
c-'A';1~N 直接使用; - 距离平方用 long long 防止 int 溢出;
- Kruskal 排序是从小到大,保证贪心选最小边。
MST 所有变式题型汇总 + 板子调用操作 + 选型使用指南
一、全部题型概括
题型 1:基础无向图最小生成树(布线题)
题意 :给若干点、无向带权边,保留最少总权值的边连通所有点,输出总权。 输入 :点数量、多条边(两点 + 权);0 结束多组。 板子选用 :Kruskal 调用操作
- 建 Edge 数组存所有边;
- 并查集每组数据重新初始化;
- 边从小到大排序;
- 依次合并两点,累加边权,凑够 \(n-1\) 条边终止;
- 输出总权。
题型 2:邻接矩阵输入(已有道路)
题意 :给出 (n*n) 两两距离矩阵,部分村庄已经通路,只新建最少总长度道路连通全图。 输入 :n + n 行矩阵 + 已有道路数量 Q + Q 组连通点对。 板子选用 :Kruskal(优先)/ 朴素邻接矩阵 Prim 调用操作
- 遍历矩阵 (i<j) 生成所有无向边;
- 并查集先把题目给出的已连通点合并(不计代价);
- 排序所有边,正常跑 Kruskal,累加新增道路长度;
- 输出累加和。
题型 3:坐标点求距离(百岛湖、空间站球体)
题意 :给出二维 / 三维坐标,两点距离为边权;存在距离限制(仅区间内可建边),求最小总造价;无法连通输出 oh!。 输入 :多组数据,点数 C,每组坐标 ((x,y)/(x,y,z))。 板子选用
- (n<100):Kruskal;
- (n>500) 稠密点集:堆优化 Prim。 调用操作
- 枚举所有两点,计算欧几里得距离;
- 过滤不满足距离限制的边直接丢弃;
- 正常跑 MST;
- 统计成功合并边数,不足 (n-1) 输出无解,否则总距离 × 单价输出(浮点保留小数)。
题型 4:稠密海量点集(卡车编码汉明距离,(n=2000))
题意 :n 个 7 位字符串,两点边权为汉明距离,求 MST 总权。 输入 :多组,n 个字符串,0 结束。 板子选用 :堆优化 Prim(强制,Kruskal 存 200 万条边会超时 / 爆内存) 调用操作
- 不用预存全部边,松弛时实时计算两点汉明距离;
- 小根堆维护最小松弛距离;
- 跳过堆内过期记录;
- 累加总权,按固定格式输出。
题型 5:卫星信道 MST 变形(多连通块)
题意 :S 个卫星可让任意 S 个连通块互通,求最小无线电距离 D(MST 中第 \(P-S\) 长边)。 输入 :多组用例,卫星数 S、哨站数 P,所有哨站坐标。 板子选用 :Kruskal / 堆 Prim 均可 调用操作
- 求出完整 MST,把 MST 所有边权存入数组升序排序;
- 一共需要 (P-1) 条边,卫星抵消 (S-1) 条最长边;
- 取排序后第 (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(并查集 + 边排序)适用场景 & 修改点
适用场景
- 稀疏图、边总数 ≤ 1e4;
- 需要提前合并已有连通点(已修路);
- 需要过滤合法边(距离区间限制);
- 需要收集 MST 所有边后续处理(卫星题取第 k 长边);
- 点数小 \(n\le 200\)。
通用修改模板(应对变式)
- 边权为浮点:
Edge中w改为double; - 坐标生成边:双重循环枚举 i<j,计算距离存入边;
- 已有道路:读入点对后先
find合并; - 距离过滤:判断距离条件,不合法不加入边数组;
- 无解判断:统计合并成功的边数
cnt,\(cnt<n-1\) 输出无解; - 多组数据:
vector<Edge>、fa每组循环内重新定义,自动清空。
优缺点
- 优点:变形修改逻辑简单,预处理连通块极其方便;
- 缺点:稠密大点集((n>1000))存边过多,排序超时。
2. 堆优化 Prim(小根堆)适用场景 & 修改点
适用场景
- 稠密图、点数量大 (n>500),两两生成距离;
- 无法存储全部边(百万条边内存超限);
- 字符串汉明距离、坐标实时算权,无需预存边。
通用修改模板(应对变式)
- 边权浮点:
dis数组改为double,堆存pair<double, int>; - 自定义距离:松弛循环内调用
getDist(u,v)实时计算; - 距离限制:计算距离后不符合条件直接跳过更新;
- 无解判断:最后统计访问点数量,不足 n 代表不连通;
- 多组数据:
dis、vis数组每组memset重置无穷大 / 0。
优缺点
- 优点:无需存储全部边,稠密图省内存;
- 缺点:无法提前合并已连通点,处理 "已有道路" 不如 Kruskal 方便。
3. 朴素邻接矩阵 Prim
适用场景
仅 (n<100),题目直接给邻接矩阵输入(畅通工程类)。
局限
点超过 100 空间浪费,不适合坐标、字符串类题目。