最小生成树、最短路 笔记
原理
Part1. 建图
| 实现方法 | 邻接矩阵 | 邻接表 | 链式前向星 | 边集数组 |
|---|---|---|---|---|
| 空间 | \(O(n^2)\) | \(O(n+m)\) | \(O(n+m)\) | \(O(m)\) |
| 优点 | 适合于稠密图,方便得到出入度、一条边是否存在 | 各种图,对一个点的出边排序时十分常用 | 各种图,边带有编号 | 关注边的信息,常用于 kruskal |
| 缺点 | 复杂度高,不能处理重边 | 不好计算节点入度、删除节点,不能处理反向边 | 无法快速查询一条边的存在,无法对一个点的出边排序 | 搜索整张图时要用 \(O(nm)\) 对每个点遍历所有边,复杂度高 |
邻接矩阵
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 |
|---|---|---|---|
| 实现方法 | 邻接表、链式前向星 | 邻接表、链式前向星 | 边集数组 |
| 时间 | \(O(n^2)\) | \(O((n+m)\log n)\) | \(O(m\log m)\) |
暴力 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 |
|---|---|---|---|---|---|
| 类型 | 单源 | 单源 | 单源 | 多源 | 多源 |
| 实现方法 | 邻接表、链式前向星 | ~ | ~ | 领接矩阵 | ~ |
| 适用于 | 非负权图 | 非负权图、负权图跑最长路 | 任意图、判负环、跑最长路 | 任意图 | 非负权图 |
| 时间 | \(O(n^2)\) | \(O(m\log n)\)(严格上是\(O((n+m)\log n)\)) | \(O(m)\sim O(nm)\) | \(O(n^3)\) | \(O(nm\log n)\) |
单源
暴力 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 买礼物
思路 \(\scr{Solution}\)
这道题我调了两年半, \(0\to17\to35\to100pts\) ......
一眼:这不就是个板子吗?一点不需要动,跑最小生成树,只需要在答案累加上根节点的价值 \(c\) 就可以了......然而抱灵
再仔细看数据范围,原来它并没有保证 \(c > w_{i,j}\) ,不贴合实际,蒸乌鱼
也就是说,全图跑最小生成树并不一定是最优解,
那么显然,对于两个互相有关联的物品,要比较单买 \(2c\) 和 捆绑买 \(c+w\) 哪个费用更少
易得思路:通过比较 \(2c\),\(c+w\),跑部分图的最小生成树 + 单点集合
\(O(n^2\log n^2)~\) done.
#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, 连自己用 \(n^2\) 输入时都没注意;
点击查看代码
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;
总结 \(\scr{Summary}\)
-
题意一定要反复看,不可主观臆断,特别是数据范围和特殊条件;
-
做题思路一定要清晰,觉得模棱两可的想法不要急着实现,先保证正确性,再考虑优化时效;
二、1. P1629 邮递员送信
思路 \(\scr{Solution}\)
50pts
在有向图中,有 \(v_i\in V\),求 \(\sum\limits_{i=2}^{n}w_{min}(1\to v_i)+\sum\limits_{i=2}^{n}w_{min}(v_i\to 1)\) .
易想到以 1 为起点跑一遍 dijkstra,再暴力枚举其他点每次再跑一遍 dijkstra。
但这样做复杂度为 \(O(nm\log n)\),在 T 的边缘反复横跳
考虑对暴力枚举进行优化,可以发现,每次跑 dijkstra 可以知道到所有点的最短路,而答案只需要累加该点到 1 的最短路,其他的都没有用。
问了下 syz 大佬怎么优化,有一种办法可以不用枚举 tql %%%
就是在反图上跑。而 反图就是将原有向图的所有边方向倒转
在反图上从 1 向其他点跑最短图就等价于在原图上其他点向 1 跑最短路。
\(O(m\log n)~\) done.
#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;
}
总结 \(\scr{Summary}\)
- 跑最短路时可以考虑 建反图 来转化思路,优化时效;

浙公网安备 33010602011771号