最小生成树、最短路 笔记
原理
Part1. 建图
实现方法 | 邻接矩阵 | 邻接表 | 链式前向星 | 边集数组 |
---|---|---|---|---|
空间 | ||||
优点 | 适合于稠密图,方便得到出入度、一条边是否存在 | 各种图,对一个点的出边排序时十分常用 | 各种图,边带有编号 | 关注边的信息,常用于 kruskal |
缺点 | 复杂度高,不能处理重边 | 不好计算节点入度、删除节点,不能处理反向边 | 无法快速查询一条边的存在,无法对一个点的出边排序 | 搜索整张图时要用 |
邻接矩阵
int n, m, e[N][N]; int main() { cin >> n >> m; for (int i = 1; i <= m; i ++) { int x, y, w; cin >> x >> y >> w; e[x][y] = w; e[y][x] = w; } for (int i = 1; i <= n; i ++) { for (int j = 1; j <= n; j ++) cout << e[x][y] << ' '; cout << '\n'; } }
邻接表(vector)
struct edge { int y, w; }; vector<edge> e[N]; int n, m; int main() { cin >> n >> m; for (int i = 1; i <= m; i ++) { int x, y, w; cin >> x >> y >> w; e[x].push_back({y, w}); e[y].push_back({x, w}); } for (int i = 1; i <= n; i ++) for (int j = 0; j < e[i].size(); j ++) printf("%d %d %d\n", i, e[i][j].y, e[i][j].w); }
链式前向星
struct edge { int to, w, next; }e[M]; int n, m, top, h[N]; void add(int x, int y, int w) { e[++top] = {y, w, h[x]}; h[x] = top; } int main() { cin >> n >> m; for (int i = 1; i <= m; i ++) { int x, y, w; cin >> x >> y >> w; add(x, y, w); add(y, x, w); } for (int i = 1;i <= n; i ++) for (int j = h[i]; j; j = e[j].next) printf("%d %d %d\n", i, e[j].to, e[j].w); }
边集数组
struct edge { int x, y, w; }e[2*M]; int n, m; int main() { cin >> n >> m; for (int i = 1; i <= m; i ++) { int x, y, w; cin >> x >> y >> w; e[i] = {x, y, w}; e[i+m] = {y, x, w}; } for (int i = 1; i <= n; i ++) for (int j = 1; j <= m << 1; j ++) if (e[j].x == i) printf("%d %d %d\n", i, e[j].y, e[j].w); }
Part2. 最小生成树
算法 | 暴力 prim | heap(小根堆) - prim | kruskal |
---|---|---|---|
实现方法 | 邻接表、链式前向星 | 邻接表、链式前向星 | 边集数组 |
时间 |
暴力 prim
bool prim(int s) { for (int i = 0; i <= n; i ++) dist[i] = inf; dist[s] = 0; for (int i = 1; i <= n; i ++) { int x = 0; for (int j = 1; j <= n; j ++) if (!vis[j] && dist[j] < dist[x]) x = j; vis[x] = true; ans += dist[x]; if (dist[x] != inf) cnt ++; for (int j = h[x]; j ; j = e[j].next) { int y = e[j].to, w = e[j].w; if (w < dist[y]) dist[y] = w; } } return (cnt == n ? 1 : 0); }
heap(小根堆) - prim
bool prim(int s) { for (int i = 1; i <= n; i ++) dist[i] = inf; dist[s] = 0; q.push({0, s}); while(!q.empty()) { int x = q.top().second; q.pop(); if (vis[x]) continue; vis[x] = true; ans += dist[x]; cnt ++; for (int j = h[x]; j ; j = e[j].next) { int y = e[j].to, w = e[j].w; if (w < dist[y]) { dist[y] = w; q.push({dist[y], y}); } } } return (cnt == n ? 1 : 0); }
kruskal
bool kruskal() { int cnt = 0; for (int i = 1; i <= n; i ++) fa[i] = i; sort(e + 1, e + m + 1, cmp); for (int i = 1; i <= m; i ++) { int x = e[i].x, y = e[i].y, w = e[i].w; if (find(x) != find(y)) { merge(x, y); ans += w; cnt ++; } } return (cnt == n - 1 ? 1 : 0); }
Part3. 最短路
算法 | 暴力 dijkstra | heap(小根堆) - dijkstra | SPFA | floyd | 跑 n 次 heap - dijkstra |
---|---|---|---|---|---|
类型 | 单源 | 单源 | 单源 | 多源 | 多源 |
实现方法 | 邻接表、链式前向星 | ~ | ~ | 领接矩阵 | ~ |
适用于 | 非负权图 | 非负权图、负权图跑最长路 | 任意图、判负环、跑最长路 | 任意图 | 非负权图 |
时间 |
单源
暴力 dijkstra
void dijkstra() { for (int i = 0; i <= n; i ++) dist[i] = inf; dist[s] = 0; for (int i = 1; i <= n; i ++) { int x = 0; for (int j = 1; j <= n; j ++) if (!vis[j] && dist[j] < dist[x]) x = j; vis[x] = true; for (int j = 0; j < e[x].size(); j ++) { int y = e[x][j].y, w = e[x][j].w; if (dist[x] + w < dist[y]) dist[y] = dist[x] + w; } } }
heap(小根堆) - dijkstra
void dijkstra() { for (int i = 0; i <= n; i ++) dist[i] = inf; dist[s] = 0; q.push({0, s}); while (!q.empty()) { pii t = q.top(); q.pop(); int x = t.second; if (vis[x]) continue; vis[x] = true; for (int i = h[x]; i ; i = e[i].next) { int y = e[i].to, w = e[i].w; if (dist[x] + w < dist[y]) { dist[y] = dist[x] + w; q.push({dist[y], y}); } } } }
SPFA
......
void spfa(int s) { for (int i = 1; i <= n; i ++) dist[i] = inf; dist[s] = 0; q.push(s); vis[s] = true; while (!q.empty()) { int x = q.front(); q.pop(); vis[x] = false; for (int j = h[x]; j ; j = e[j].next) { int y = e[j].to, w = e[j].w; if (dist[x] + w < dist[y]) { dist[y] = dist[x] + w; // cnt[y] = cnt[x] + 1; // if (cnt[y] >= n) return false; 有负环 if (!vis[y]) { q.push(y); vis[y] = true; } } } } // return true; }
全源
floyd
void init() { memset(f, inf, sizeof(f)); for (int i = 1; i <= n; i ++) f[i][i] = 0; } void floyd() { for (int k = 1; k <= n; k ++) for (int i = 1; i <= n; i ++) for (int j = 1; j <= n; j ++) f[i][j] = min(f[i][j], f[i][k] + f[k][j]); }
运用
一、1. P1194 买礼物
思路
这道题我调了两年半,
一眼:这不就是个板子吗?一点不需要动,跑最小生成树,只需要在答案累加上根节点的价值
再仔细看数据范围,原来它并没有保证 不贴合实际,蒸乌鱼
也就是说,全图跑最小生成树并不一定是最优解,
那么显然,对于两个互相有关联的物品,要比较单买
易得思路:通过比较
#include <bits/stdc++.h> using namespace std; const int N = 510, M = 1e3 + 10, inf = 0x3f3f3f3f; struct edge { int x, y, w; }e[N*N]; int top, n, c, fa[N], ans; bool cmp(edge a, edge b) { return a.w < b.w; } int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); } void merge(int x, int y) { fa[find(x)] = find(y); } void kruskal() { for (int i = 1; i <= n; i ++) fa[i] = i; sort(e + 1, e + top + 1, cmp); for (int i = 1; i <= top; i ++) { int x = e[i].x, y = e[i].y, w = e[i].w; if (find(x) != find(y) && c + w < 2 * c) { merge(x, y); ans += w; } } } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> c >> n; for (int i = 1; i <= n; i ++) for (int j = 1; j <= n; j ++) { int x; cin >> x; if (i == j) continue; e[++top] = {i, j, (!x ? inf : x)}; } kruskal(); int res = ans; for (int i = 1; i <= n; i ++) { if (find(i) == i) res += c; } cout << res; return 0; }
粗心1. 本题没直接给出边数范围,我就惯性打 N, 连自己用
点击查看代码
struct edge { int x, y, w; }e[N];
粗心2. 震惊,一开始我甚至默认以为最小生成树集合的点是连续的,没过脑子吗?
点击查看代码
int last = 0, res = 0; for (int i = 1; i <= n ; i ++) { if (find(last) != find(i)) { if (find(i) != i) res += ans + w; else res += w; } last = i; } cout << res;
总结
-
题意一定要反复看,不可主观臆断,特别是数据范围和特殊条件;
-
做题思路一定要清晰,觉得模棱两可的想法不要急着实现,先保证正确性,再考虑优化时效;
二、1. P1629 邮递员送信
思路
50pts
在有向图中,有
易想到以 1 为起点跑一遍 dijkstra,再暴力枚举其他点每次再跑一遍 dijkstra。
但这样做复杂度为
考虑对暴力枚举进行优化,可以发现,每次跑 dijkstra 可以知道到所有点的最短路,而答案只需要累加该点到 1 的最短路,其他的都没有用。
问了下 syz 大佬怎么优化,有一种办法可以不用枚举 tql %%%
就是在反图上跑。而 反图就是将原有向图的所有边方向倒转
在反图上从 1 向其他点跑最短图就等价于在原图上其他点向 1 跑最短路。
#include <bits/stdc++.h> #define re register #define int long long using namespace std; typedef pair<int, int> pii; const int N = 1e6 + 10, M = 1e6 + 10, inf = 0x3f3f3f3f; struct edge1 { int to, w, next; }e1[M]; struct edge2 { int to, w, next; }e2[M]; int n, m, ans; int top1, top2, h1[N], h2[N], dist[N]; bool vis[N]; priority_queue< pii, vector<pii>, greater<pii> > q; void add1(int x, int y, int w) { e1[++top1] = (edge1){y, w, h1[x]}; h1[x] = top1; } void add2(int x, int y, int w) { e2[++top2] = (edge2){y, w, h2[x]}; h2[x] = top2; } void kruskal1(int s) { memset(vis, false, sizeof(vis)); for (re int i = 1; i <= n; i ++) dist[i] = inf; dist[s] = 0; q.push({0, s}); while (!q.empty()) { int x = q.top().second; q.pop(); if (vis[x]) continue; vis[x] = true; for (re int i = h1[x]; i ; i = e1[i].next) { int y = e1[i].to, w = e1[i].w; if (dist[x] + w < dist[y]) { dist[y] = dist[x] + w; q.push({dist[y], y}); } } } } void kruskal2(int s) { memset(vis, false, sizeof(vis)); for (re int i = 1; i <= n; i ++) dist[i] = inf; dist[s] = 0; q.push({0, s}); while (!q.empty()) { int x = q.top().second; q.pop(); if (vis[x]) continue; vis[x] = true; for (re int i = h2[x]; i ; i = e2[i].next) { int y = e2[i].to, w = e2[i].w; if (dist[x] + w < dist[y]) { dist[y] = dist[x] + w; q.push({dist[y], y}); } } } } signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n >> m; for (re int i = 1; i <= m; i ++) { int x, y, w; cin >> x >> y >> w; add1(x, y, w); add2(y, x, w); } kruskal1(1); for (int i = 1; i <= n; i ++) ans += dist[i]; kruskal2(1); for (int i = 1; i <= n; i ++) ans += dist[i]; cout << ans; return 0; }
总结
- 跑最短路时可以考虑 建反图 来转化思路,优化时效;
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析