笔记性质,记录一下单源最短路径的SPFA,Dijkstra,Bellman-Ford算法.
先来做个对比:
SPFA实质是Bellman-Ford的队列优化版本,但这个优化并不改变最坏情况的复杂度,所以他死了.
Dijkstra的复杂度很好,但是根本不能碰负权.
那么负权最短路算法有复杂度更低的算法吗?暂时没有接触到.
1.Bellman-Ford&SPFA
Bellman-Ford算法不停枚举并尝试松弛每一条边以达到求最短路的目的,SPFA是其优化版本.
这是一道用来测试SPFA和Bellman-Ford的题目,不过没有负权(当然也就没有负环).
如果可以写SPFA,似乎没有必要写Bellman-Ford,写后者的理由不过是容易写而已(不论是求最短路还是判负环).
Bellman-Ford若要判断是否有负环,只需要在进行n-1次对每条边松弛操作后检查是否仍有可松弛的边,有则存在负环.
注意此时的边有独特的存储方式.
(当时码风还没形成,建议看这篇文章里的实现)
#include <algorithm> #include <cstdio> #include <cstring> #include <iostream> using namespace std; #define MAX_E 500010 #define MAX_V 10010 struct edge { int from, to, cost; }; edge es[MAX_E]; int d[MAX_V], V, E, s; const int INF = 2147483648 - 1; void solve() { while (1) { bool bad = true; for (int i = 1; i <= E; i++) { edge e = es[i]; if (d[e.from] != INF && d[e.to] > d[e.from] + e.cost) { d[e.to] = d[e.from] + e.cost; bad = false; } } if (bad) break; } } int main() { // freopen("in.txt", "r", stdin); // freopen("out.txt", "w", stdout); cin >> V >> E >> s; for (int i = 1; i <= E; i++) cin >> es[i].from >> es[i].to >> es[i].cost; for (int i = 0; i <= V; i++) d[i] = INF; d[s] = 0; solve(); for (int i = 1; i <= V; i++) cout << d[i] << ' '; cout << endl; return 0; }
现在用SPFA求最短路,顺便判一下负环.
基于这样的一个事实:每当一条边被从队列中取出,则说明其在入队时进行了一次松弛操作.
因此可以记录每一条边的出队次数即松弛次数,一旦发现某条边被松弛了第n次,则存在负环.
相比Bellman-Ford,多了个队列,多了个used数组,又多了个计数数组,不过只需要使用普通的vector邻接表.
#include <algorithm> #include <cstdio> #include <cstring> #include <iostream> #include <queue> #include <vector> using namespace std; struct E { int to, wei; }; vector<E> e[2010]; int n, m, dist[2010], ct[2010]; bool used[2010]; bool solve() { memset(used, 0, sizeof(used)); memset(ct, 0, sizeof(ct)); memset(dist, 0x3F, sizeof(dist)); dist[1] = 0; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) e[i].clear(); while (m--) { int u, v, w; scanf("%d%d%d", &u, &v, &w); e[u].push_back({v, w}); if (w >= 0) e[v].push_back({u, w}); } queue<int> q; q.push(1); used[1] = true; while(!q.empty()){ int cur = q.front(); q.pop(); used[cur] = false, ++ct[cur]; if(ct[cur] >= n) return 1; for(auto i : e[cur]) if(dist[i.to] > dist[cur] + i.wei){ dist[i.to] = dist[cur] + i.wei; if(!used[i.to]){ used[i.to] = true; q.push(i.to); } } } return 0; } int main() { int t; scanf("%d", &t); while (t--) puts(solve() ? "YES" : "NO"); return 0; }
相比于Dijkstra,SPFA有时候具有更强的灵活性.
本题中对于节点入队有了限制条件:新入队节点与先前遍历的点的等级必须保持在一定范围内.
由于SPFA与BFS的相似性质,可以很方便地将已有等级的信息设计到队列元素中来处理新节点.
#include <algorithm> #include <cstdio> #include <cstring> #include <iostream> #include <vector> #include <queue> using namespace std; struct E{ int to, wei; }; struct Q{ int to, l, r; }; vector<E> e[110]; queue<Q> q; int n, m, lv[110], dist[110]; bool used[110]; int main(){ scanf("%d%d", &m, &n); for(int i = 1; i <= n; i++){ int a, b, c; scanf("%d%d%d", &a, &b, &c); e[0].push_back({i, a}); lv[i] = b; while(c--){ int u, v; scanf("%d%d", &u, &v); e[u].push_back({i, v}); } } lv[0] = lv[1]; memset(dist, 0x3F, sizeof(dist)); q.push({0, lv[0], lv[0]}); dist[0] = 0; while(!q.empty()){ Q cur = q.front(); q.pop(); used[cur.to] = false; for(vector<E>::iterator i = e[cur.to].begin(); i != e[cur.to].end(); i++) if(dist[cur.to] + i->wei < dist[i->to] && cur.r - lv[i->to] <= m && lv[i->to] - cur.l <= m){ dist[i->to] = dist[cur.to] + i->wei; if(!used[i->to]){ used[i->to] = true; q.push({i->to, min(cur.l, lv[i->to]), max(cur.r, lv[i->to])}); } } } printf("%d ", dist[1]); return 0; }
2.Dijkstra
Dijkstra算法通过维护集合并寻找集合外最近节点以计算最短路.
由于其贪心思想,不能处理负权图.但是复杂度很好.
如果可以的话就用Dijkstra吧,因为
#include <algorithm> #include <cstdio> #include <cstring> #include <iostream> #include <queue> #include <vector> using namespace std; struct E { int to, wei; bool operator<(const E &other) const { return wei > other.wei; } }; vector<E> e[100010]; int n, m, s, dist[100010]; bool used[100010]; int main() { scanf("%d%d%d", &n, &m, &s); while(m--){ int u, v, w; scanf("%d%d%d", &u, &v, &w); e[u].push_back({v, w}); } priority_queue<E> q; q.push({s, 0}); while(!q.empty()){ E cur = q.top(); q.pop(); if(used[cur.to]) continue; used[cur.to] = true; dist[cur.to] = cur.wei; for(auto i : e[cur.to]) q.push({i.to, i.wei + cur.wei}); } for(int i = 1; i <= n; i++) printf("%d ", dist[i]); puts(""); return 0; }
(可以删掉used,把dist初始化为-1作为used判断)