最小生成树、最短路 笔记

原理

Part1. 建图

实现方法 邻接矩阵 邻接表 链式前向星 边集数组
空间 O(n2) 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(n2) O((n+m)logn) O(mlogm)

暴力 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(n2) O(mlogn)(严格上是O((n+m)logn) O(m)O(nm) O(n3) O(nmlogn)

单源

暴力 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

......

image

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 买礼物

思路 Solution

这道题我调了两年半, 01735100pts ......

一眼:这不就是个板子吗?一点不需要动,跑最小生成树,只需要在答案累加上根节点的价值 c 就可以了......然而抱灵

再仔细看数据范围,原来它并没有保证 c>wi,j不贴合实际,蒸乌鱼

也就是说,全图跑最小生成树并不一定是最优解,

那么显然,对于两个互相有关联的物品,要比较单买 2c 和 捆绑买 c+w 哪个费用更少

易得思路:通过比较 2cc+w,跑部分图的最小生成树 + 单点集合

O(n2logn2)  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, 连自己用 n2 输入时都没注意;

点击查看代码
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;

总结 Summary

  1. 题意一定要反复看,不可主观臆断,特别是数据范围和特殊条件;

  2. 做题思路一定要清晰,觉得模棱两可的想法不要急着实现,先保证正确性,再考虑优化时效;


最短路题单

二、1. P1629 邮递员送信

P1342 请柬(双倍经验)

思路 Solution

50pts

在有向图中,有 viV,求 i=2nwmin(1vi)+i=2nwmin(vi1) .

易想到以 1 为起点跑一遍 dijkstra,再暴力枚举其他点每次再跑一遍 dijkstra。

但这样做复杂度为 O(nmlogn),在 T 的边缘反复横跳


考虑对暴力枚举进行优化,可以发现,每次跑 dijkstra 可以知道到所有点的最短路,而答案只需要累加该点到 1 的最短路,其他的都没有用。

问了下 syz 大佬怎么优化,有一种办法可以不用枚举 tql %%%

就是在反图上跑。而 反图就是将原有向图的所有边方向倒转

在反图上从 1 向其他点跑最短图就等价于在原图上其他点向 1 跑最短路。

O(mlogn)  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;
}

总结 Summary

  1. 跑最短路时可以考虑 建反图 来转化思路,优化时效;
posted @   Zhang_Wenjie  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示