代码随想录算法训练营 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 是把所有边排序,然后依次判断能不能加入。

它每次选择的是:

复制代码
当前最短且不会成环的边
相关推荐
啦啦啦_99991 小时前
案例之 逻辑回归_癌症预测
算法·机器学习·逻辑回归
StockTV1 小时前
韩国股票实时数据 KOSPI(主板)和 KOSDAQ(创业板)的实时行情、K 线及指数数据
java·开发语言·算法·php
byte轻骑兵1 小时前
【LE Audio】BASS精讲[5]: 状态特征解析,广播接收状态实时可视全流程
人工智能·算法·音视频·语音识别·le audio·低功耗音频
m0_629494731 小时前
LeetCode 热题 100-----13.最大子数组和
数据结构·算法·leetcode
0xR3lativ1ty1 小时前
大模型算法原理高频题解析
算法
故事还在继续吗2 小时前
STL 容器算法手册
开发语言·c++·算法
田梓燊2 小时前
力扣:94.二叉树的中序遍历
数据结构·算法·leetcode
啊我不会诶2 小时前
2023西安邀请赛vp补题
c++·算法
khalil10202 小时前
代码随想录算法训练营Day-38动态规划06 | 322. 零钱兑换、279.完全平方数、139.单词拆分、多重背包、总结
数据结构·c++·算法·leetcode·动态规划