53. 寻宝(第七期模拟笔试)
题目描述
在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。
不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将 所有岛屿联通起来(注意:这是一个无向图)。
给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。以最小化公路建设长度,确保可以链接到所有岛屿。
输入描述
第一行包含两个整数V 和 E,V代表顶点数,E代表边数 。顶点编号是从1到V。例如:V=2,一个有两个顶点,分别是1和2。
接下来共有 E 行,每行三个整数 v1,v2 和 val,v1 和 v2 为边的起点和终点,val代表边的权值。
输出描述
输出联通所有岛屿的最小路径总距离
输入示例
7 11 1 2 1 1 3 1 1 5 2 2 6 1 2 4 2 2 3 2 3 4 1 4 5 1 5 6 2 5 7 1 6 7 1输出示例
6
一、Prim 算法
cpp
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// 邻接矩阵存图
// edges[i][j] 表示 i 到 j 的边权
// 10005 表示两个点之间没有直接相连的边
vector<vector<int>> edges(n + 1, vector<int>(n + 1, 10005));
// minDist[i] 表示节点 i 到当前最小生成树的最短距离
vector<int> minDist(n + 1, 10005);
// visited[i] 表示节点 i 是否已经加入最小生成树
vector<bool> visited(n + 1, false);
// 读入无向图
for (int i = 0; i < m; i++) {
int s, t, v;
cin >> s >> t >> v;
edges[s][t] = v;
edges[t][s] = v;
}
// 从节点 1 开始构建最小生成树
// 这里不显式设置 minDist[1] = 0 也能运行
// 因为第一次会选中节点 1
for (int j = 0; j < n - 1; j++) {
int cur = -1;
int minv = 10006;
// 1. 找到距离当前生成树最近的节点
for (int i = 1; i <= n; i++) {
if (!visited[i] && minDist[i] < minv) {
minv = minDist[i];
cur = i;
}
}
// 将该节点加入最小生成树
visited[cur] = true;
// 2. 用当前节点更新其他节点到生成树的最短距离
for (int i = 1; i <= n; i++) {
if (!visited[i] && minDist[i] > edges[cur][i]) {
minDist[i] = edges[cur][i];
}
}
}
// 统计最小生成树的总权值
// 节点 1 作为起点,不需要加入答案
int res = 0;
for (int i = 2; i <= n; i++) {
res += minDist[i];
}
cout << res << endl;
return 0;
}
总结
1. 核心思路
Prim 算法是从"点"的角度来构造最小生成树。
每次都从还没有加入生成树的节点中,选择一个距离当前生成树最近的点。
也就是:
每次选一个离当前生成树最近的点加入
2. 关键数组
minDist[i]
表示节点 i 到当前最小生成树的最短距离。
visited[i]
表示节点 i 是否已经加入最小生成树。
3. 代码流程
从节点 1 开始
↓
找到距离生成树最近的节点
↓
加入生成树
↓
更新其他节点到生成树的距离
↓
重复 n - 1 次
↓
统计答案
4. 本质理解
Prim 更像是"扩张"。
一开始只有一个点,然后每次找一条最短边,把新的点拉进来。
它关注的是:
哪个点离当前生成树最近
5. 复杂度
使用邻接矩阵时:
时间复杂度:O(n²)
空间复杂度:O(n²)
适合点数不太大、图比较稠密的情况。
二、Kruskal 算法
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int n, m;
// father[i] 表示节点 i 的父节点
// 用并查集维护节点之间的连通关系
vector<int> father(10005, -1);
// 初始化并查集
// 一开始每个节点的父节点都是自己
void init() {
for (int i = 1; i <= n; i++) {
father[i] = i;
}
}
// 查找节点 u 所在集合的根节点
// 使用路径压缩优化
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
// 合并两个节点所在的集合
// 如果两个节点已经在同一个集合中,说明加入这条边会形成环
bool join(int u, int v) {
u = find(u);
v = find(v);
// 已经连通,不能再加入这条边
if (u == v) return false;
// 合并两个集合
father[v] = u;
return true;
}
// 边结构体
struct Edge {
int s; // 起点
int t; // 终点
int v; // 边权
};
int main() {
cin >> n >> m;
vector<Edge> edges;
// 读入所有边
for (int i = 0; i < m; i++) {
int s, t, v;
cin >> s >> t >> v;
edges.push_back({s, t, v});
}
// 按照边权从小到大排序
// Kruskal 的核心:优先选择最小的边
sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
return a.v < b.v;
});
// 初始化并查集
init();
int res = 0; // 最小生成树的总权值
// 遍历所有边
for (auto edge : edges) {
// 如果这条边连接的两个点已经连通
// 说明加入后会形成环,不能选
if (!join(edge.s, edge.t)) continue;
// 否则加入最小生成树
res += edge.v;
}
cout << res << endl;
return 0;
}
总结
1. 核心思路
Kruskal 算法是从"边"的角度来构造最小生成树。
先把所有边按照权值从小到大排序,然后从小到大依次尝试加入。
如果加入这条边不会形成环,就加入最小生成树。
如果会形成环,就跳过。
2. 关键点
Kruskal 最重要的是两个部分:
sort(edges.begin(), edges.end(), ...)
用于按照边权排序。
join(s, t)
用于判断两个点是否已经连通。
如果 join 返回 false,说明这条边会形成环,不能加入。
3. 代码流程
读入所有边
↓
按边权从小到大排序
↓
初始化并查集
↓
从小到大遍历边
↓
不成环就加入
↓
成环就跳过
↓
输出答案
4. 本质理解
Kruskal 更像是"选边"。
它不关心从哪个点开始,只关心当前最小的边能不能加入。
它关注的是:
当前这条最小边会不会形成环
5. 复杂度
主要耗时在排序:
时间复杂度:O(m log m)
空间复杂度:O(m)
其中 m 是边的数量。
并查集使用路径压缩后,查询和合并的速度接近常数。
三、Prim 和 Kruskal 对比
| 算法 | 思路 | 适合场景 | 主要数据结构 |
|---|---|---|---|
| Prim | 选点 | 稠密图 | 邻接矩阵 / 邻接表 |
| Kruskal | 选边 | 稀疏图 | 边集数组 + 并查集 |
Prim
Prim 是从一个点开始,不断扩展生成树。
它每次选择的是:
离当前生成树最近的点
Kruskal
Kruskal 是把所有边排序,然后依次判断能不能加入。
它每次选择的是:
当前最短且不会成环的边