代码随想录算法训练营 Day49 | 图论 part07

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 是把所有边排序,然后依次判断能不能加入。

它每次选择的是:

复制代码
当前最短且不会成环的边
相关推荐
MartinYeung528 分钟前
[论文学习]DP2Unlearning:高效且具保证的大型语言模型遗忘框架(基于差分隐私的 LLM Unlearning 方法)
学习·算法·语言模型
Tian_Hang41 分钟前
C++原型模式(Protype)
开发语言·c++·算法
bIo7lyA8v41 分钟前
算法复杂度的渐进分析与实际运行时间的差异的技术8
算法
yuan199972 小时前
欧拉梁静力与屈曲计算的 MATLAB 实现(有限差分法 + 解析解)
开发语言·算法·matlab
汉克老师3 小时前
GESP7级C++考试语法知识(二、指数函数(3、综合练习)
c++·算法·数学建模·指数函数·gesp7级·复利
林间码客3 小时前
04 ROC曲线与AUC:从零开始手动计算
大数据·人工智能·算法
Irissgwe3 小时前
map/set/multimap/multiset 的底层逻辑与实现
数据结构·c++·算法·二叉树·stl·c·红黑树
IronMurphy3 小时前
【算法五十八】23. 合并 K 个升序链表
数据结构·算法·链表
思茂信息3 小时前
CST软件基于液态金属开关的方向图可重构天线
服务器·算法·重构·cst·仿真软件·电磁仿真
月疯4 小时前
PPG研究中暑的算法记录
算法