最短路复习
记搜写到NOIP2017逛公园的时候 发现之前一直说复习最短路
但是单源最短路的模板题提交记录上次还是2019年..
存图
直接存边
找边 O(m) | 找一个点的所有出边 O(m) | 遍历整个图 O(nm) | 空间复杂度O(m)
应用: Kruskal
#include<bits/stdc++.h>
const int N=100010; // 点数
struct Edge {
int u, v, w;
bool operator<(const Edge &a) const {
return w<a.w;
}
};
int main() {
int n, m;
std::vector<Edge>e;
std::bitset<N>vis; vis.reset();
auto find_edge=[&](int u, int v)->bool {
for (auto it:e) if (it.u==u&&it.v==v) return true;
return false;
};
auto dfs=[&](auto self, int u)->void {
if (vis[u]==1) return; vis[u]=1;
for (auto it:e) if (it.u==u) self(self, it.v);
};
return 0;
}
邻接矩阵
不能考虑重边的影响
别的没啥了
邻接表
查询是否存在u到v的边: O((u)) 如果排过序 那么可以二分 降到log
遍历点u所有出边: O((u)) | 遍历整张图 O(n+m) | 空间复杂度 O(m)
存各种图都很好 尤其是需要对一个点的出边进行排序的时候 加个权值 排序的时候写个lambda 也很方便
int main() {
int n, m;
cin >> n >> m;
std::vector<std::vector<int>>adj(n + 1);
std::bitset<100010>vis; vis.reset();
for (int i=1; i<=m; i++) {
int u, v; std::cin>>u>>v;
adj[u].push_back(v);
}
auto find_edge=[&](int u, int v)->bool {
for (auto it:adj[u])
if (it==v) return true;
return false;
};
auto dfs=[&](auto self, int u)->void {
vis[u]==1;
for (auto it:adj[u]) if (vis[it]==0) self(self, it);
};
return 0;
}
链式前向星
本质上是用链表实现的链式前向星 但是不依赖指针实现
不能对一个点的出边进行排序
优点是边是带编号的 奇数^1 = 成对的偶数 双向边可以以此来找反向边 常用于网络流
const int N=1e5+5;
const int M=1e6+5;
int tot, nxt[M], to[M], head[N];
void add(int u, int v) { to[++tot]=v, nxt[tot]=head[u], head[u]=tot; }
int main() {
int u;
for (int i=head[u]; i; i=nxt[i]) {
int y=to[i];
/* code */
}
return 0;
}
单源最短路径
Bellman-Ford
对于边,松弛操作对应的式子: {}
每次循环O(m) 一次松弛操作最多使一条最短路的边数+1 最长的最短路至多长为n-1
因而算法是O(nm)的
下面这份代码跑单源最短路径只有84分 会T一个点
const int N = 1e5 + 5;
int main() {
std::vector<std::pair<int, int>>e[N];
int dis[N];
memset(dis, 0x7f, sizeof(dis));
int n, m, s;
std::cin >> n >> m >> s;
for (int i = 1; i <= m; i++) {
int u, v, w;
std::cin >> u >> v >> w;
e[u].push_back({v, w});
}
dis[s] = 0;
bool flag = false;
for (int i = 1; i < n; i++) { // 数据保证的情况下做n-1轮就可以了
flag = false;
for (int j = 1; j <= n; j++) {
for (auto it : e[j]) {
int to = it.first, val = it.second;
if (dis[to] > dis[j] + val)
dis[to] = dis[j] + val, flag = true;
}
}
if (!flag)
break;
}
for (int i = 1; i <= n; i++)
std::cout << dis[i] << ' ';
return 0;
}
判负环
如果松弛次数 那么就会有一个负环包含在图里面
到n轮还没停下来那么可以判断从当前源点出发是达到一个负环的
#include <bits/stdc++.h>
int main() {
int T, n, m; std::cin >> T;
int dis[2005];
std::vector <std::pair<int, int>> e[2010];
auto Bellman_Ford = [&]() -> bool {
dis[1] = 0;
bool flag;
for (int i = 1; i <= n; i++) {
flag = false;
for (int u = 1; u <= n; u++) {
if (dis[u] == 0x7fffffff) continue; // 如果当前这个点不能到 也就不能松弛
for (auto eg : e[u]) {
int v = eg.first, w = eg.second;
if (dis[v] > dis[u] + w)
dis[v] = dis[u] + w, flag = true;
}
}
if (!flag) break;
}
return flag;
};
while (T--) {
std::cin >> n >> m;
for (int i=1;i<=n;i++) dis[i]=0x7fffffff;
for (int i = 1; i <= n; i++)
e[i].clear();
for (int i = 1; i <= m; i++) {
int u, v, w;
std::cin >> u >> v >> w;
if (w >= 0)
e[u].push_back({v, w}), e[v].push_back({u, w});
else
e[u].push_back({v, w});
}
if (Bellman_Ford())
puts("YES");
else
puts("NO");
}
return 0;
}
SPFA
流程
还是依赖松弛操作 有一源点s s入队
取出队头u 找出所有的() 尝试松弛 能被松弛的点才能入队
这样就只访问了必要的边
int main() {
int n, m, s;
std::cin >> n >> m >> s;
std::bitset<100010>vis;
std::vector dis(n + 1, 0x7fffffff);
std::vector <std::vector<std::pair<int, int>>> e(n + 1);
dis[s] = 0; vis.reset();
std::queue <int> q;
q.push(s);
for (int i = 1; i <= m; i++) {
int u, v, w;
std::cin >> u >> v >> w;
e[u].push_back({v, w});
}
while (!q.empty()) {
int u = q.front(); vis[u] = 0; q.pop();
for (auto eg : e[u]) {
int v = eg.first, w = eg.second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (vis[v] == 0) {
vis[v] = true;
q.push(v);
}
}
}
}
for (int i = 1; i <= n; i++)
std::cout << dis[i] << ' ';
return 0;
}
像单源最短路径[标准版] SPFA被卡到32分 而 Bellman-Ford 还有84分
[弱化版]的随机数据 SPFA 表现也不一定比 Bellman-Ford 好
洛谷 P3385[模板]负环
判断从点1出发能否到达一个负环
SPFA可以实现从指定源点出发到达一个负环的功能
struct Edge {
int v, w;
};
std::bitset <2010> vis;
std::vector <Edge> e[2010];
std::queue <int> q;
int T, n, m, dis[2010], cnt[2010];
int main() {
std::cin >> T;
auto SPFA = [&]() -> bool {
for (int i = 1; i <= n; i++)
dis[i] = 0x7fffffff, cnt[i] = 0;
dis[1] = 0; vis[1] = true;
q.push(1);
while (!q.empty()) {
int u = q.front();
q.pop(), vis[u] = 0;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n) return false;
if (!vis[v]) vis[v] = 1, q.push(v);
}
}
}
return true;
};
while (T--) {
while (!q.empty()) q.pop();
vis.reset();
std::cin >> n >> m;
for (int i = 1; i <= n; i++) e[i].clear();
for (int i = 1; i <= m; i++) {
int u, v, w; std::cin >> u >> v >> w;
if (w >= 0) e[u].push_back({v, w}), e[v].push_back({u, w});
else e[u].push_back({v, w});
}
if (SPFA())
puts("NO");
else
puts("YES");
}
return 0;
}
Bellman-Ford的其他优化
这个直接看OI-wiki上面的吧
每种优化都有对应的Hack 看fstqwq的知乎回答就好了
题外话
我百度正确性 找hack数据的时候看到了这一篇博客 写的非常全面 包括一些历史..
Dijkstra
条件是非负权图
流程
节点分为两个集合 一个是确定了最短路径的{}和未确定的{}
初始化dis(s)=0 其余的dis均为
然后重复以下操作直到为空:
- 从{}中,选取最距离源点最近的结点,移到{}中
- 对刚刚加入{}的结点的出边进行松弛
照这个思路 暴力.. O() = O()
无法处理带负权的图
(图片缺失:DijkstraCannot.png)
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m, s;
std::cin >> n >> m >> s;
std::vector <std::vector<std::pair <int, int>> >adj(n + 1);
std::vector <int> dis(n + 1, 0x7fffffff);
std::bitset <10010> vis;
for (int i = 0; i < m; i++) {
int u, v, w;
std::cin >> u >> v >> w;
adj[u].push_back({v, w});
}
dis[s] = 0;
int curr = s, Min = 0x7fffffff;
while (vis[curr] == 0) {
vis[curr] = 1;
for (auto it : adj[curr]) {
int to = it.first, val = it.second;
if (vis[to] == 0 && dis[to] > dis[curr] + val)
dis[to] = dis[curr] + val;
}
Min = 0x7fffffff;
for (int i = 1; i <= n; i++)
if (dis[i] < Min && vis[i] == 0)
Min = dis[i], curr = i;
}
for (int i = 1; i <= n; i++)
std::cout << dis[i] << ' ';
return 0;
}
加速的关键在于维护操作1的最近{}中节点
二叉堆优化
用二叉堆维护 每成功松弛一条边 加入下一个节点{v, dis[v]} 如果已经有了v 我们只要进行更新就可以
操作1直接取堆顶即可 因此一共m次试图更新\插入操作 n次删除堆顶操作
复杂度 =
优先队列优化
使用STL的优先队列的时候 由于我们不能修改或者删除在松弛时新插入的节点 所以元素的个数是的
总体时间复杂度在
也是用的最多的模版之一了 网上一堆堆优化Dijkstra的 写出来的实际内容都还是STL priority_queue优化 包括一期洛谷日报 成功的带偏了复杂度分析 相比之下OI-wiki写的就很好
struct Node {
int u, w;
bool operator<(const Node &a) const {
return w > a.w;
}
};
int main() {
int n, m, s;
std::cin >> n >> m >> s;
std::priority_queue <Node> q;
std::vector <std::vector<std::pair<int, int>>> e(n + 1);
std::vector dis(n + 1, 0x7fffffff);
for (int i = 1; i <= m; i++) {
int u, v, w; std::cin >> u >> v >> w;
e[u].push_back({v, w});
}
q.push({s, 0}); dis[s] = 0;
while (!q.empty()) {
int u = q.top().u, w = q.top().w; q.pop();
if (w != dis[u]) continue;
for (auto eg : e[u]) {
int v = eg.first, val = eg.second;
if (dis[v] > val + dis[u])
dis[v] = dis[u] + val, q.push({v, dis[v]});
}
}
for (int i = 1; i <= n; i++)
std::cout << dis[i] << ' ';
return 0;
}
Fibonacci堆优化
我不会 以下内容来自OI-wiki 什么时候回来再看情况填坑吧
和前面二者类似 但Fibonacci堆插入的时间复杂度为 故时间复杂度为= 时间复杂度最优
但因为Fibonacci堆较二叉堆不易实现 效率优势也不够大 算法竞赛中较少使用
线段树优化
考虑Dijkstra过程 要求动态维护每个点的dis值 查询最小dis值的点u 要求能够更新指定点u的dis值
要能够更新指定节点u的话 感觉应该是用类似权值线段树的操作去维护 每个点存了当前区间dis值最小的{u, dis[u]}
如果要查询最近点u 拿出第一个就好了 如果要更新指定点u的dis值 就是权值树的写法
这东西调试给我调麻了
#define N 100005
#define ls p << 1
#define rs p << 1 | 1
#define mid (l + r >> 1)
struct Node {
int u, w;
} seg[N << 2];
void build(int p, int l, int r) { // 建树
if (l == r) {
seg[p] = (Node){l, INT_MAX};
return;
}
build(ls, l, mid); build(rs, mid + 1, r);
seg[p] = (Node){seg[ls].u, INT_MAX};
}
void mark(int p, int l, int r, int u, int w) { // 点修 和权值树的部分一样
if (l == r) {
seg[p].w = w;
return;
}
if (u <= mid) mark(ls, l, mid, u, w);
else mark(rs, mid + 1, r, u, w);
if (seg[ls].w < seg[rs].w) seg[p] = seg[ls];
else seg[p] = seg[rs];
}
int main() {
int n, m, s, u, v, w;
std::cin >> n >> m >> s;
build(1, 1, n);
std::vector <std::vector<std::pair<int, int>>> e(n + 1);
std::vector dis(n + 1, INT_MAX);
for (int i = 1; i <= m; i++) {
std::cin >> u >> v >> w;
e[u].push_back({v, w});
}
dis[s] = 0; mark(1, 1, n, s, 0);
while (seg[1].w != INT_MAX) { // 最近的节点没有被更新过
int u = seg[1].u;
mark(1, 1, n, u, INT_MAX); // 等价于队列里面pop这个节点
for (auto eg : e[u]) {
int v = eg.first, w = eg.second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
mark(1, 1, n, v, dis[v]);
}
}
}
for (int i = 1; i <= n; i++) {
std::cout << dis[i] << ' ';
}
return 0;
}
emmm 不难发现这个点修区间查的RMQ还有树状数组 zkw(好像会更快一点 但是我不会写)都能维护
我走了 我不打扰 感兴趣的大佬慢慢写
水题
P1608 路径统计
这个东西等同于最短路计数 只不过要考虑重边的影响 那么就用邻接矩阵来处理
写的时候堆优不知道那里写错了 着急吃饭 直接把最短路计数那份线段树优化的拿过来用了
反正这个调了也是浪费时间了..
/* P1144 最短路计数 */
#include <bits/stdc++.h>
#define ls p << 1
#define rs p << 1 | 1
#define fi first
#define se second
#define mid (l + r >> 1)
#define mod 100003
#define N 2010
using namespace std;
int mp[N][N];
struct Node {
int u, w;
bool operator<(const Node &a) const {
return w < a.w;
}
} seg[N << 2];
void build(int p, int l, int r) {
if (l == r) {
seg[p] = {l, INT_MAX};
return;
}
build(ls, l, mid); build(rs, mid + 1, r);
seg[p] = seg[ls];
}
void mark(int p, int l, int r, int u, int w) {
if (l == r) {
seg[p].w = w;
return;
}
if (u <= mid) mark(ls, l, mid, u, w);
else mark(rs, mid + 1, r, u, w);
if (seg[ls].w < seg[rs].w) seg[p] = seg[ls];
else seg[p] = seg[rs];
}
int main() {
int n, m;
cin >> n >> m;
vector dp(n + 1, 0);
vector dis(n + 1, INT_MAX);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
mp[i][j] = INT_MAX;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
mp[u][v] = min(mp[u][v], w);
}
build(1, 1, n);
dis[1] = 0, dp[1] = 1;
mark(1, 1, n, 1, 0);
while (seg[1].w != INT_MAX) {
int u = seg[1].u;
mark(1, 1, n, u, INT_MAX);
for (int v = 1; v <= n; v++) {
int w = mp[u][v];
if (w == INT_MAX) continue;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w, dp[v] = dp[u];
mark(1, 1, n, v, dis[v]);
} else if (dis[v] == dis[u] + w) {
dp[v] += dp[u];
}
}
}
if (dis[n] == INT_MAX) puts("No answer");
else cout << dis[n] << ' ' << dp[n] << '\n';
return 0;
}