当前位置: 首页 > news >正文

算法:图的存储与遍历,最小生成树(Prim算法,kruskal算法)

一、图的存储和遍历

图的存储有两种:邻接矩阵和邻接表:

  • 其中,邻接表的存储方式与树的孩子表示法完全一样。因此,用 vector 数组以及链式前向星就能实现。
  • 邻接矩阵就是用一个二维数组,其中edges[i][j]存储顶点i与顶点j之间边的信息。

图的遍历分两种:DFS 和 BFS

1.邻接矩阵

邻接矩阵,是指用一个矩阵 (即二维数组) 存储图中边的信息 (即各个顶点之间的邻接关系),存储顶点之间邻接关系的矩阵称为邻接矩阵。

对于带权图而言,若顶点 v_i 和 v_j 之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶点 v_i 和 v_j 不相连,则用 无穷 来代表这两个顶点之间不存在边。

对于不带权的图,可以创建一个二维的bool类型的数组,来标记顶点 v_i 和 v_j 之间有边相连。

【注意】 矩阵中元素个数为 n * n,即空间复杂度为 O(n^2),n 为顶点个数,和实际边的条数无关,适合存储稠密图。

#include <iostream> #include <cstring> using namespace std; const int N = 1010; int n, m; int edges[N][N]; int main() { memset(edges, -1, sizeof edges); cin >> n >> m; // 读入结点个数以及边的个数 for(int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; // a - b 有一条边,权值为 c edges[a][b] = c; // 如果是无向边,需要反过来再存一下 edges[b][a] = c; } return 0; }

2.vector 数组

和树的存储一模一样,只不过如果存在边权的话,我们的 vector 数组里面放一个结构体或者是 pair 即可。

#include <iostream> #include <vector> using namespace std; typedef pair<int, int> PII; const int N = 1e5 + 10; int n, m; vector<PII> edges[N]; int main() { cin >> n >> m; // 读入结点个数以及边的个数 for(int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; // a 和 b 之间有一条边,权值为 c edges[a].push_back({b, c}); // 如果是无向边,需要反过来再存一下 edges[b].push_back({a, c}); } return 0; }

3.用链式前向星的方式存储

#include <iostream> #include <queue> using namespace std; const int N = 1e5 + 10; // 链式前向星 int h[N], e[N * 2], ne[N * 2], w[N * 2], id; int n, m; // 其实就是把 b 头插到 a 所在的链表后面 void add(int a, int b, int c) { id++; e[id] = b; w[id] = c; // 多存一个权值信息 ne[id] = h[a]; h[a] = id; } bool st[N]; void dfs(int u) { cout << u << endl; st[u] = true; // 遍历所有的孩子 for(int i = h[u]; i; i = ne[i]) { // u->v 的一条边 int v = e[i]; if(!st[v]) { dfs(v); } } } int main() { cin >> n >> m; // 读入结点个数以及边的个数 for(int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; // a 和 b 之间有一条边,权值为 c add(a, b, c); add(b, a, c); } return 0; }

4.DFS

4.1 用邻接矩阵的方式存储

代码实现:

#include <iostream> #include <cstring> #include <queue> using namespace std; const int N = 1010; int n, m; int edges[N][N]; bool st[N]; // 标记哪些点已经访问过 void dfs(int u) { cout << u << endl; st[u] = true; // 遍历所有孩子 for(int v = 1; v <= n; v++) { // 如果存在 u->v 的边,并且没有遍历过 if(edges[u][v] != -1 && !st[v]) { dfs(v); } } } int main() { memset(edges, -1, sizeof edges); cin >> n >> m; // 读入结点个数以及边的个数 for(int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; // a - b 有一条边,权值为 c edges[a][b] = c; // 如果是无向边,需要反过来再存一下 edges[b][a] = c; } return 0; }

4.2 用 vector 数组的方式存储

代码实现:

#include <iostream> #include <vector> #include <queue> using namespace std; typedef pair<int, int> PII; const int N = 1e5 + 10; int n, m; vector<PII> edges[N]; bool st[N]; // 标记哪些点已经访问过 void dfs(int u) { cout << u << endl; st[u] = true; // 遍历所有孩子 for(auto& t : edges[u]) { // u->v 的一条边,权值为 w int v = t.first, w = t.second; if(!st[v]) { dfs(v); } } } int main() { cin >> n >> m; // 读入结点个数以及边的个数 for(int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; // a 和 b 之间有一条边,权值为 c edges[a].push_back({b, c}); // 如果是无向边,需要反过来再存一下 edges[b].push_back({a, c}); } return 0; }

4.3 用链式前向星的方式存储

代码实现:

#include <iostream> #include <queue> using namespace std; const int N = 1e5 + 10; // 链式前向星 int h[N], e[N * 2], ne[N * 2], w[N * 2], id; int n, m; // 其实就是把 b 头插到 a 所在的链表后面 void add(int a, int b, int c) { id++; e[id] = b; w[id] = c; // 多存一个权值信息 ne[id] = h[a]; h[a] = id; } bool st[N]; void dfs(int u) { cout << u << endl; st[u] = true; // 遍历所有的孩子 for(int i = h[u]; i; i = ne[i]) { // u->v 的一条边 int v = e[i]; if(!st[v]) { dfs(v); } } } int main() { cin >> n >> m; // 读入结点个数以及边的个数 for(int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; // a 和 b 之间有一条边,权值为 c add(a, b, c); add(b, a, c); } return 0; }

5.BFS

5.1 用邻接矩阵的方式存储

代码实现:

#include <iostream> #include <cstring> #include <queue> using namespace std; const int N = 1010; int n, m; int edges[N][N]; bool st[N]; // 标记哪些点已经访问过 void bfs(int u) { queue<int> q; q.push(u); st[u] = true; while(q.size()) { auto a = q.front(); q.pop(); cout << a << endl; for(int b = 1; b <= n; b++) { if(edges[a][b] != -1 && !st[b]) { q.push(b); st[b] = true; } } } } int main() { memset(edges, -1, sizeof edges); cin >> n >> m; // 读入结点个数以及边的个数 for(int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; // a - b 有一条边,权值为 c edges[a][b] = c; // 如果是无向边,需要反过来再存一下 edges[b][a] = c; } return 0; }

5.2 用 vector 数组的方式存储

代码实现:

#include <iostream> #include <vector> #include <queue> using namespace std; typedef pair<int, int> PII; const int N = 1e5 + 10; int n, m; vector<PII> edges[N]; bool st[N]; // 标记哪些点已经访问过 void bfs(int u) { queue<int> q; q.push(u); st[u] = true; while(q.size()) { auto a = q.front(); q.pop(); cout << a << endl; for(auto& t : edges[a]) { int b = t.first, c = t.second; if(!st[b]) { q.push(b); st[b] = true; } } } } int main() { cin >> n >> m; // 读入结点个数以及边的个数 for(int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; // a 和 b 之间有一条边,权值为 c edges[a].push_back({b, c}); // 如果是无向边,需要反过来再存一下 edges[b].push_back({a, c}); } return 0; }

5.3 用链式前向星的方式存储

代码实现:

#include <iostream> #include <queue> using namespace std; const int N = 1e5 + 10; // 链式前向星 int h[N], e[N * 2], ne[N * 2], w[N * 2], id; int n, m; // 其实就是把 b 头插到 a 所在的链表后面 void add(int a, int b, int c) { id++; e[id] = b; w[id] = c; // 多存一个权值信息 ne[id] = h[a]; h[a] = id; } bool st[N]; void bfs(int u) { queue<int> q; q.push(u); st[u] = true; while(q.size()) { auto a = q.front(); q.pop(); cout << a << endl; for(int i = h[a]; i; i = ne[i]) { int b = e[i], c = w[i]; if(!st[b]) { q.push(b); st[b] = true; } } } } int main() { cin >> n >> m; // 读入结点个数以及边的个数 for(int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; // a 和 b 之间有一条边,权值为 c add(a, b, c); add(b, a, c); } return 0; }

二、最小生成树

一个具有 n 个顶点的连通图,其生成树为包含n-1 条边和所有顶点的极小连通子图对于生成树来说,若砍去一条边就会使图不连通;若增加一条边就会形成回路。

一个图的生成树可能有多个,将所有生成树中权值之和最小的树称为最小生成树。

构造最小生成树有多种算法,典型的有普利姆(Prim)算法和克鲁斯卡尔(Kruskal)算法两种,它们都是基于贪心的策略。下面讲解算法的具体流程,关于正确性的证明(看《算法导论》)。

模板题

P3366 【模板】最小生成树 - 洛谷

1.Prim 算法

核心:不断加点。

Prim 算法构造最小生成树的基本思想:

  1. 从任意一个点开始构造最小生成树;
  2. 将距离该树权值最小且不在树中的顶点,加入到生成树中。然后更新与该点相连的点到生成树的最短距离;
  3. 重复 2 操作 n 次,直到所有顶点都加入为止。

具体流程:

代码实现 - 邻接矩阵:

#include <iostream> #include <cstring> using namespace std; const int N = 5010, INF = 0x3f3f3f3f; int n, m; int edges[N][N]; // 邻接矩阵存储图 int dist[N]; // 某个点距离生成树的最短距离 bool st[N]; // 标记哪些点已经加入到生成树 int prim() { // 初始化 memset(dist, 0x3f, sizeof dist); dist[1] = 0; int ret = 0; for(int i = 1; i <= n; i++) // 循环加入 n 个点 { // 1. 找最近点 int t = 0; for(int j = 1; j <= n; j++) if(!st[j] && dist[j] < dist[t]) t = j; // 判断是否联通 if(dist[t] == INF) return INF; st[t] = true; ret += dist[t]; // 2. 更新距离 for(int j = 1; j <= n; j++) // 枚举 t 能走到哪 dist[j] = min(dist[j], edges[t][j]); } return ret; } int main() { cin >> n >> m; // 初始化邻接矩阵 memset(edges, 0x3f, sizeof edges); for(int i = 1; i <= m; i++) { int x, y, z; cin >> x >> y >> z; // 注意有重边的情况 edges[x][y] = edges[y][x] = min(edges[x][y], z); } int ret = prim(); if(ret == INF) cout << "orz" << endl; else cout << ret << endl; return 0; }

代码实现 - 邻接表 - vector 数组:

#include <iostream> #include <vector> #include <cstring> using namespace std; typedef pair<int, int> PII; const int N = 5010, INF = 0x3f3f3f3f; int n, m; vector<PII> edges[N]; int dist[N]; bool st[N]; int prim() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; int ret = 0; for(int i = 1; i <= n; i++) { // 1. 找最近点 int t = 0; for(int j = 1; j <= n; j++) if(!st[j] && dist[j] < dist[t]) t = j; // 判断是否联通 if(dist[t] == INF) return INF; st[t] = true; ret += dist[t]; // 2. 更新距离 for(auto& p : edges[t]) { int a = p.first, b = p.second; dist[a] = min(dist[a], b); } } return ret; } int main() { cin >> n >> m; for(int i = 1; i <= m; i++) { int x, y, z; cin >> x >> y >> z; // 如果有重边,怎么办? // 不影响 edges[x].push_back({y, z}); edges[y].push_back({x, z}); } int ret = prim(); if(ret == INF) cout << "orz" << endl; else cout << ret << endl; return 0; }

2.Kruskal 算法

核心:不断加边。

Kruskal 算法构造最小生成树的基本思想:

  1. 所有边按照权值排序;
  2. 每次选出权值最小且两端顶点不连通的一条边,直到所有顶点都联通。

代码实现:

#include <iostream> #include <algorithm> using namespace std; const int N = 5010, M = 2e5 + 10, INF = 0x3f3f3f3f; int n, m; struct node { int x, y, z; }a[M]; int fa[N]; // 并查集 bool cmp(node &a, node &b) { return a.z < b.z; } int find(int x) { return x == fa[x] ? fa[x] : fa[x] = find(fa[x]); } int kk() { sort(a + 1, a + 1 + m, cmp); int cnt = 0; int ret = 0; for(int i = 1; i <= m; i++) { int x = a[i].x, y = a[i].y, z = a[i].z; int fx = find(x), fy = find(y); if(fx != fy) { cnt++; ret += z; fa[fx] = fy; } } return cnt == n - 1 ? ret : INF; } int main() { cin >> n >> m; for(int i = 1; i <= m; i++) cin >> a[i].x >> a[i].y >> a[i].z; // 初始化并查集 for(int i = 1; i <= n; i++) fa[i] = i; int ret = kk(); if(ret == INF) cout << "orz" << endl; else cout << ret << endl; return 0; }
http://www.jsqmd.com/news/900759/

相关文章:

  • 别再傻傻分不清!一文搞懂CPU、GPU、NPU、MCU、DSP、FPGA、SOC,嵌入式选型不踩坑
  • 别只让LED闪了!基于STM32CubeMX的HAL库,教你玩转GPIO输入输出与硬件抽象层设计
  • 推荐题目:洛谷 P5730 【深基5.例10】显示屏
  • 别再找第三方工具了!用Windows自带的DISM命令,5分钟给Win10家庭版装上组策略编辑器
  • 在OpenClaw中配置Taotoken作为后端AI供应商的详细步骤
  • Cortex-M3/M4调试系统设计:TPIU与CoreSight Funnel应用
  • ROCK5B新手避坑指南:用BalenaEtcher给NVMe刷Debian11,从驱动安装到首次登录的完整流程
  • 从彩虹猫到MBR:一次MEMZ病毒‘事故’后,我搞懂了Windows引导修复的几种方法
  • [智能体-119]:LangChain 生态工具详解
  • 2026年4月花灯供货商怎么选,景区灯会/大型户外花灯/天幕花灯/春节国潮花灯/春节花灯/巡游花灯,花灯定做厂家推荐分析 - 品牌推荐师
  • 2026支持百度AI优化的GEO服务商测评:服务优质响应高效
  • 2026年4月市场优秀的混合机直销厂家哪家可靠,链盘管链输送机/吨袋无尘拆包机/双锥混合机,混合机企业哪家靠谱 - 品牌推荐师
  • SARscape版本升级实战:5.3到5.6.2,那些官方没细说的数据导入与DEM处理变化
  • 别再死磕梯度下降了!用Python手把手教你实现遗传算法解决旅行商问题
  • 深入浅出 LoongSuite Python Agent:让你的 AI 应用「透明化」(上篇)
  • 数据分析入门:手把手教你用Python爬取直播数据并做简单可视化
  • 从编译到出结果:SPEC CPU 2017在CentOS 7上的完整避坑指南(含gcc/g++/gfortran配置)
  • 别再死记硬背公式了!用这个在线仿真工具,5分钟搞懂正激变换器(Forward Converter)工作原理
  • 别再找第三方工具了!用Windows自带的DISM命令,5分钟搞定Win10家庭版组策略(gpedit.msc)安装
  • 量子纠错码与被动解码技术解析
  • 2026年 宝钢HC900/1180DP吉帕钢厂家推荐榜:高强汽车板/先进高强钢/冷轧双相钢/轻量化选材解决方案 - 品牌企业推荐师(官方)
  • 2026指南:东莞老化房专业品牌厂家甄选 - 品牌企业推荐师(官方)
  • Agent技术大变革:从魔法提示词到系统工程,未来已来!
  • 别再死记硬背公式了!用LTspice仿真带你直观理解Buck、Boost、Buck-Boost三大基础拓扑
  • LAMMPS转Material Studio数据流打通:从Perl脚本到MS建模的完整避坑实践
  • 别再傻傻分不清!用Python实战解析SLA与SSHA数据(附Jupyter Notebook代码)
  • 别再被配置单搞晕了!理光喷头UV打印机,从4色到6色+白墨光油,到底怎么选才不浪费钱?
  • CTF新手必看:用Python脚本暴力破解PNG图片的CRC校验,修复被篡改的宽高信息
  • Halcon DLT V22.06新功能尝鲜:深度OCR标注与训练效率提升实战
  • OpenMV串口数据收发的那些坑:解码错误、数据丢失?手把手教你调试与避雷