Coded by Jelly_Goat on 2019/4/13. All rights reserved.
來自一個被智能推薦了一萬年\(\text{【模板】單源最短路徑}\)的蒟蒻小金羊。html
說實話這個算法是用來求多源最短路徑的算法。node
可是這個時候咱們能夠引用一下來先作一個鋪墊。ios
動態規劃。git
typedef long long int lli; lli map[5001][5001]; int n; void floyd() { for (register int i=1;i<=n;i++) for (register int j=1;j<=n;j++) for (register int k=1;k<=n;k++) if (map[i][j]>map[i][k]+map[k][j]) map[i][j]=map[i][k]+map[k][j]; }
最後程序中的map[i][j]
就是i->j
的最短路徑長度。算法
時間複雜度:\(O(n^3)\),空間複雜度:\(S(n^2)\)(用的是鄰接表)。數組
以及若是題目中數據範圍\(n\leq5000\),通常就是Floyd沒跑了。學習
最經典的最短路算法之一,衍生品(其實主要就是SPFA)簡直不計其數。測試
可是咱們如今仍是先回到這個算法自己。優化
咱們用一個結構體存一下邊。注意,這個時候鏈式前向星尚未用到。ui
struct edge{ int from,to; lli dis; edge(){ from=to=dis=0; } }Edge[500001];
通俗易懂的變量名字
而後就能夠進行比較寬泛的「鬆弛」操做。
「鬆弛」是什麼?就是咱們利用三角形不等式的方法逼近更新最短路。
這就是這個算法的核心。
不行,我不證實
咱們從一個點出發,一定會更新到其餘節點。再從其餘節點更新其餘節點,一定會獲得其餘節點的鬆弛操做。
由於咱們每個節點都鬆弛了邊數遍,因此鬆弛一定會獲得其餘節點的最短路(距離源點)。
因此這個算法是正確的。
可是這個不是嚴謹的證實,證實仍是要上網右轉百度。
struct edge { int from, to; lli dis; edge() { from = to = dis = 0; } } Edge[500001]; lli dis[10001]; int nodenum, edgenum, origin_node, querynum; bool Bellman_Ford() { dis[origin_node] = 0; //核心:鬆弛操做 for (int i = 1; i <= nodenum - 1; i++) { for (int j = 1; j <= edgenum; j++) { if (dis[Edge[j].to] > dis[Edge[j].from] + Edge[j].dis) dis[Edge[j].to] = dis[Edge[j].from] + Edge[j].dis; } } for (int i = 1; i <= edgenum; i++) if (dis[Edge[i].to] > dis[Edge[i].from] + Edge[i].dis) return false; return true; }
這就是上文所說的衍生物。
可是在這以前咱們還得學習一下鏈式前向星——一種存儲圖的方式。
咱們會發現,bf之因此跑得慢,就是由於對於邊的更新次數太多了!因此咱們想要像搜索那樣更新每個節點。
可是咱們會發現,咱們只須要一個節點的全部出邊就夠了。
對於空間上:對比鄰接矩陣,咱們會發現咱們若是存儲有向邊的話,矩陣的空間損耗太大,並且二位數組容易爆。
對於時間上:O(1)訪問某一個節點的第一個出邊。
首先咱們須要開一個節點個數大小的數組head[10001]
,用來存儲每個節點的「第一條」出邊,而後加上一個變量cnt
用來轉移下標。
而後咱們須要在結構體內部作修改——改爲一個「出邊」:出邊的終點、出邊的長度、下一條出邊這三項就能夠了。
struct edge{ int to,next; lli dis; edge(){ to=next=dis=0; } }Edge[500001]; int cnt=1;//存儲的其實是下一條邊的下標
而後咱們進行添加邊的操做。
inline void add_edge(int from,int to,lli dis) { Edge[cnt].to=to;//先給要添加的邊進行一個賦值 Edge[cnt].dis=dis; //重點理解:鏈接到上一條from的出邊 Edge[cnt].next=head[from]; //將head[from]——from的最後一條出邊改爲這個邊,而後到下一條邊的下標(下標++) head[from]=cnt++; }
看起來就很簡單
上面說:head[10001]`,用來存儲每個節點的「第一條」出邊
其實是最後一條出邊,可是咱們遍歷全部出邊的時候哪管那個是頭上的邊......
咱們對這個算法進行一個測試,看看到底正確性如何。剪貼板子
而後進入正題——
引自GGBeng大佬的blog:原文(P.S:解釋的我以爲還不錯)
咱們學過了Bellman-Ford算法,如今又要提出這個SPFA算法,爲何呢?
考慮一個隨機圖(點和邊隨機生成),除了已肯定最短路的頂點與還沒有肯定最短路的頂點之間的邊,其它的邊所作的都是無用的,大體描述爲下圖(分割線以左爲已肯定最短路的頂點):
其中紅色部分爲所作無用的邊,藍色部分爲實際有用的邊。既然只需用到中間藍色部分的邊,那就是SPFA算法的優點之處了。
就是由於咱們要捨棄沒有用的邊的遍歷,咱們引進了SPFA的思想。
首先先給一個全局定義:
#include <iostream> #include <cstdio> #include <bitset> #include <queue> #include <cstring> using namespace std; typedef long long int lli; const lli oo = 2147483647LL, INF = oo; struct edge { int to, next; lli dis; edge() { to = next = 0, dis = oo; } } Edge[500001]; int nodenum, edgenum, origin_node, end_node, cnt = 1; int dis[10001], head[400001]; bitset<10005> vst; //bool vst[10005]; queue<int> q;
而後咱們須要進行SPFA!
BFS?聽起來挺熟悉......
然而還真的是借用了這個思想。
隊列裏面要放須要擴展的節點,而後還要用vst[]
表示是否在隊列中。
對於每個取出來的節點,咱們擴展全部的出邊的終點,咱們發現能夠進行判斷&鬆弛——沒錯,仍是那個鬆弛。
若是在隊列中,就不須要重複加入隊列。
可是若是每個節點重複加入了n次,就說明這個地方有一個能夠無限鬆弛的負環。
#include <iostream> #include <cstdio> #include <cctype> #include <bitset> #include <queue> #include <cstring> using namespace std; typedef long long int lli; const lli oo = 2147483647LL, INF = oo; struct edge { int to, next; lli dis; edge() { to = next = 0, dis = oo; } } Edge[500001]; int nodenum, edgenum, origin_node, end_node, cnt = 1; int dis[10001], head[400001], cnt2[10001]; bitset<10005> vst; //bool vst[10005]; queue<int> q; //quick input of num template <typename T_> inline T_ getnum(); inline void add_edge(int from, int to, lli dis) { Edge[cnt].dis = dis; Edge[cnt].to = to; Edge[cnt].next = head[from]; head[from] = cnt++; } bool spfa() { q.push(origin_node); vst[origin_node] = true; dis[origin_node] = 0; while (!q.empty()) { int t = q.front(); q.pop(); vst[t] = false; for (register int i = head[t]; i != 0; i = Edge[i].next) { int to = Edge[i].to; if (dis[to] > dis[t] + Edge[i].dis) { dis[to] = dis[t] + Edge[i].dis; if (!vst[to]) { q.push(to); vst[to] = true; if (++cnt2[to] > nodenum) return false; } } } } return true; } int main() { #ifdef WIN32 freopen("in.txt", "r", stdin); freopen("out.txt", "w+", stdout); #endif nodenum = getnum<int>(), edgenum = getnum<int>(), origin_node = getnum<int>(); for (register int i = 1; i <= nodenum; i++) { dis[i] = oo; } for (register int i = 1; i <= edgenum; i++) { int from = getnum<int>(), to = getnum<int>(); lli dis = getnum<lli>(); add_edge(from, to, dis); } if (!spfa()) { cout << "Exist a minus circle." << endl; return 0; } else { for (register int i = 1; i <= nodenum; i++) { printf("%d ", dis[i]); } } return 0; } //quick input of num template <typename T_> inline T_ getnum() { T_ res = 0; bool flag = false; char ch = getchar(); while (!isdigit(ch)) { flag = flag ? flag : ch == '-'; ch = getchar(); } while (isdigit(ch)) { res = (res << 3) + (res << 1) + ch - '0'; ch = getchar(); } return flag ? -res : res; }
代碼如上。