题解:洛谷 P3366 最小生成树
【题目来源】
【题目描述】
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz。
【输入】
第一行包含两个整数 \(N,M\),表示该图共有 \(N\) 个结点和 \(M\) 条无向边。
接下来 \(M\) 行每行包含三个整数 \(X_i,Y_i,Z_i\),表示有一条长度为 \(Z_i\) 的无向边连接结点 \(X_i,Y_i\)。
【输出】
如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 orz。
【输入样例】
4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3
【输出样例】
7
【核心思想】
-
问题分析:给定一个 \(N\) 个节点、\(M\) 条边的无向图,每条边有边权。需要找到一棵连接所有节点的生成树,使得树中所有边的权值之和最小。若图不连通,则不存在生成树,输出
orz。这是经典的最小生成树(MST)问题,可以通过 Prim 或 Kruskal 算法解决。 -
算法选择:
- Prim 算法:从任意节点开始,逐步将离当前生成树最近的节点加入,适合稠密图。堆优化版本可达到 \(O(M \log N)\)。
- Kruskal 算法:将所有边按权值从小到大排序,依次加入不形成环的边,直到树中包含 \(N-1\) 条边。适合稀疏图,复杂度 \(O(M \log M)\)。
- 并查集:Kruskal 中用于快速判断是否形成环。
-
关键步骤(以 Kruskal 为例):
- 读入与存储:读取 \(N, M\),存储所有边 \((u, v, w)\)。
- 排序:按边权 \(w\) 从小到大排序。
- 初始化并查集:每个节点自成一个集合。
- 贪心选边:遍历排序后的边,若边的两端点不在同一集合中,则加入该边(合并集合),累加权值,计数加 1。
- 终止条件:当选边数达到 \(N-1\) 时,生成树构建完成;若遍历完所有边仍不足 \(N-1\) 条,则图不连通。
- 输出:连通时输出总权值,否则输出
orz。
-
时间/空间复杂度:
- 时间复杂度:\(O(M \log M)\)(Kruskal)或 \(O(M \log N)\)(堆优化 Prim)。
- 空间复杂度:\(O(N + M)\)。
-
最小生成树的核心思想:
- 贪心策略:每次选择当前权值最小的边,只要不形成环,就加入生成树。这种局部最优选择能保证全局最优(由割性质和回路性质保证)。
- 并查集的高效性:用于动态维护连通分量,快速判断边是否连接两个不同集合。
- 适用场景:网络设计、电路布线、公路建设等最小成本连通问题。
【算法标签】
普及 #生成树
【代码详解】
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#define inf 1e9
using namespace std;
const int N = 5010; // 最大节点数
int n, m, a, b, c; // n:节点数, m:边数, a,b,c:临时变量
int ans; // 最小生成树的总权重
int cnt; // 已加入生成树的节点数
// 边结构体
struct edge
{
int v; // 目标节点
int w; // 边权重
};
vector<edge> e[N]; // 邻接表存储图
int d[N]; // 存储当前生成树到各节点的最小距离
int vis[N]; // 标记节点是否已加入最小生成树
// 使用大根堆模拟小根堆:存储{-距离, 节点}
priority_queue<pair<int, int>> q;
/**
* Prim算法(堆优化版):求解最小生成树
* @param s 起始节点
* @return true: 图连通,存在最小生成树;false: 图不连通
*/
bool prim(int s)
{
// 初始化距离数组为无穷大
for (int i = 0; i <= n; i++)
{
d[i] = inf;
}
// 起始节点距离设为0,并加入优先队列
d[s] = 0;
q.push({0, s});
// 当队列不为空时继续处理
while (q.size())
{
int u = q.top().second; // 获取当前距离最小的节点
q.pop();
// 如果节点已经处理过,跳过(延迟删除)
if (vis[u])
{
continue;
}
// 标记节点u已加入最小生成树
vis[u] = 1;
// 累加当前边的权重到总结果
ans += d[u];
// 已加入生成树的节点数加1
cnt++;
// 遍历节点u的所有邻接边
for (auto ed : e[u])
{
int v = ed.v; // 邻接节点
int w = ed.w; // 边权重
// 如果通过u到v的距离更小,更新距离
if (d[v] > w)
{
d[v] = w; // 更新到v的最小距离
// 将新距离加入优先队列(使用负数模拟小根堆)
q.push({-d[v], v});
}
}
}
// 检查是否所有节点都加入了生成树(图是否连通)
return cnt == n;
}
int main()
{
// 输入节点数和边数
cin >> n >> m;
// 输入所有边信息,构建无向图
for (int i = 0; i < m; i++)
{
cin >> a >> b >> c;
// 无向图,双向添加边
e[a].push_back({b, c});
e[b].push_back({a, c});
}
// 从节点1开始执行Prim算法
if (!prim(1))
{
// 图不连通,无法生成最小生成树
puts("orz");
}
else
{
// 输出最小生成树的总权重
printf("%d\n", ans);
}
return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
const int N = 5005; // 最大节点数
const int INF = 0x3f3f3f3f; // 无穷大值
int n; // 节点数量
int m; // 边数量
int g[N][N]; // 邻接矩阵存储图
int dist[N]; // 存储当前生成树到各节点的最小距离
bool st[N]; // 标记节点是否已加入最小生成树
/**
* Prim算法:求解无向连通图的最小生成树
* @return 最小生成树的总权重,INF表示图不连通
*/
int prim()
{
// 初始化距离数组为无穷大
memset(dist, 0x3f, sizeof(dist));
int res = 0; // 存储最小生成树的总权重
// 循环n次,每次加入一个节点到生成树
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;
}
}
// 如果不是第一个节点(i>0)且最小距离为无穷大,说明图不连通
if (i && dist[t] == INF)
{
return INF; // 图不连通,无法生成最小生成树
}
// 如果不是第一个节点,将当前边的权重加入总结果
if (i)
{
res += dist[t];
}
// 用新加入的节点t更新其他节点到生成树的最小距离
for (int j = 1; j <= n; j++)
{
dist[j] = min(dist[j], g[t][j]);
}
// 标记节点t已加入最小生成树
st[t] = true;
}
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);
// 如果数据中没有重边,可以使用下面这行:
// g[a][b] = g[b][a] = c;
}
// 调用Prim算法计算最小生成树
int t = prim();
// 输出结果
if (t == INF)
{
// 图不连通,无法生成最小生成树
puts("orz");
}
else
{
// 输出最小生成树的总权重
printf("%d\n", t);
}
return 0;
}
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 200006; // 最大边数
int n; // 节点数量
int m; // 边数量
int ans; // 最小生成树的总权重
int cnt; // 已选择的边数
// 边结构体
struct edge
{
int u, v, w; // u:起点, v:终点, w:权重
// 重载小于运算符,用于按权重排序
bool operator<(const edge &t) const
{
return w < t.w;
}
} e[N]; // 边数组
int fa[N]; // 并查集数组,用于记录节点的父节点
/**
* 并查集查找操作(带路径压缩)
* @param x 要查找的节点
* @return 节点x的根节点
*/
int find(int x)
{
if (fa[x] == x)
{
return x;
}
return fa[x] = find(fa[x]); // 路径压缩
}
/**
* Kruskal算法:求解最小生成树
* @return true: 图连通,存在最小生成树;false: 图不连通
*/
bool kruskal()
{
// 将边按权重从小到大排序
sort(e, e + m);
// 初始化并查集,每个节点独立成集合
for (int i = 1; i <= n; i++)
{
fa[i] = i;
}
// 遍历所有边(按权重从小到大)
for (int i = 0; i < m; i++)
{
// 查找两个端点的根节点
int x = find(e[i].u);
int y = find(e[i].v);
// 如果两个端点不在同一个连通分量中
if (x != y)
{
fa[x] = y; // 合并两个连通分量
ans += e[i].w; // 累加当前边的权重
cnt++; // 已选边数加1
}
}
// 最小生成树应有n-1条边,检查图是否连通
return cnt == n - 1;
}
int main()
{
// 输入节点数和边数
cin >> n >> m;
// 输入所有边的信息
for (int i = 0; i < m; i++)
{
cin >> e[i].u >> e[i].v >> e[i].w;
}
// 调用Kruskal算法计算最小生成树
if (!kruskal())
{
// 图不连通,无法生成最小生成树
puts("orz");
}
else
{
// 输出最小生成树的总权重
printf("%d\n", ans);
}
return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
const int N = 5005; // 最大节点数
const int M = 200005; // 最大边数
const int INF = 1e9; // 无穷大值(实际未使用)
int n; // 节点数量
int m; // 边数量
int ans; // 最小生成树的总权重
int cnt; // 已选择的边数
int p[N]; // 并查集数组,用于记录节点的父节点
// 边结构体
struct Edge
{
int a, b, w; // a:起点, b:终点, w:权重
// 重载小于运算符,用于按权重排序
bool operator < (const Edge &E) const
{
return w < E.w;
}
} edges[M]; // 边数组
/**
* 并查集查找操作(带路径压缩)
* @param x 要查找的节点
* @return 节点x的根节点
*/
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 <= m; i++)
{
int a, b, w;
cin >> a >> b >> w;
// 确保起点编号小于终点编号(可选操作,便于调试)
if (a > b)
{
swap(a, b);
}
// 存储边信息
edges[i] = {a, b, w};
}
// 将边按权重从小到大排序
sort(edges + 1, edges + m + 1);
// 初始化并查集,每个节点独立成集合
for (int i = 1; i <= n; i++)
{
p[i] = i;
}
// 使用Kruskal算法构建最小生成树
for (int i = 1; i <= m; i++)
{
int s = edges[i].a; // 当前边的起点
int e = edges[i].b; // 当前边的终点
int w = edges[i].w; // 当前边的权重
// 查找两个端点的根节点
s = find(s);
e = find(e);
// 如果两个端点不在同一个连通分量中
if (s != e)
{
p[e] = s; // 合并两个连通分量
ans += w; // 累加当前边的权重
cnt++; // 已选边数加1
}
}
// 检查图是否连通:最小生成树应有n-1条边
if (cnt < n - 1)
{
// 图不连通,无法生成最小生成树
cout << "orz" << endl;
}
else
{
// 输出最小生成树的总权重
cout << ans << endl;
}
return 0;
}
【运行结果】
4 5
1 2 2
1 3 2
1 4 3
2 3 4
3 4 3
7
浙公网安备 33010602011771号