$Floyed-Warshall$算法
定義:
簡稱$Floyed$(弗洛伊德)算法,是最簡單的最短路徑算法,可以計算圖中任意兩點間的最短路徑。$Floyed$的時間復雜度是$O (N^3)$,適用於出現負邊權的情況。
算法描述:
$ps$:以下沒有特別說明的話:$dis[u][v] $表示從 $u$ 到$ v $最短路徑長度。$w[u][v]$ 表示連接 $u$,$v$ 的邊的長度。
初始化:點$u$、$v$如果有邊相連,則$dis[u][v]=w[u][v]$。
如果不相連則$dis[u][v]=INF$
for (k = 1; k <= n; k++)
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
if (dis[i][j] >dis[i][k] + dis[k][j])
dis[i][j] = dis[i][k] + dis[k][j];
算法&思想:
三層循環,第一層循環中間點k,第二第三層循環起點終點$i$、$j$,算法的思想很容易理解:如果點i到點k的距離加上點k到點j的距離小於原先點i到點j的距離,那么就用這個更短的路徑長度來更新原先點$i$到點$j$的距離。
在上圖中,因為$dis[1][3]+dis[3][2]<dis[1][2]$,所以就用$dis[1][3]+dis[3][2$]來更新原先1到2的距離。
我們在初始化時,把不相連的點之間的距離設為一個很大的數,不妨可以看作這兩點相隔很遠很遠,如果兩者之間有最短路徑的話,就會更新成最短路徑的長度。Floyed算法的時間復雜度是O(N3)。
$Dijkstra$算法
定義:
用來計算從一個點到其他所有點的最短路徑的算法,是一種單源最短路徑算法。也就是說,只能計算起點只有一個的情況。
($ps$:有了負權值$dij$這種算法就不能用了,為什么呢?
因為這種算法是貪心的思想,每次松弛的的前提是用來松弛這條邊的最短路肯定是最短的。
然而有負權值的時候這個前提不能得到保證,所以$dij$這種算法不能成立。)
思路:
算法分析&思想講解:
從起點到一個點的最短路徑一定會經過至少一個“中轉點”(例如下圖1到5的最短路徑,中轉點是2,特殊地,我們認為起點1也是一個“中轉點”)。
顯而易見,如果我們想求出起點到一個點的最短路徑,那我們必然要先求出中轉點的最短路徑(例如我們必須先求出點2 的最短路徑后,才能求出從起點到5的最短路徑)。
我們把點分為兩類,一類是已確定最短路徑的點,稱為“白點”,另一類是未確定最短路徑的點,稱為“藍點”。如果我們要求出一個點的最短路徑,就是把這個點由藍點變為白點。從起點到藍點的最短路徑上的中轉點在這個時刻只能是白點。
Dijkstra的算法思想,就是一開始將起點到起點的距離標記為0,而后進行$n$次循環,每次找出一個到起點距離$dis[u]$最短的點$u$,將它從藍點變為白點。隨后枚舉所有的藍點$v[i]$,如果以此白點為中轉到達藍點$v[i]$的路徑$dis[u]+w[u][vi]$更短的話,這將它作為$v[i]$的“更短路徑”$dis[v[i]]$(此時還不確定是不是$v[i]$的最短路徑)。
就這樣,我們每找到一個白點,就嘗試着用它修改其他所有的藍點。中轉點先於終點變成白點,故每一個終點一定能夠被它的最后一個中轉點所修改,而求得最短路徑。
實現
(1)朴素算法
給出代碼:
void dij(int st){
memset(dis,INF,sizeof(dis));
for(int i=head[st];i;i=e[i].nxt) dis[e[i].v]=e[i].w;
dis[st]=0,now=st;
while(!vis[now]){
vis[now]=1,minn=INF;
for(int i=head[now],w,v;i;i=e[i].nxt){
v=e[i].v;w=e[i].w;
if(!vis[v]&&dis[v]>dis[now]+w)dis[v]=dis[now]+w;
}
for(int i=1;i<=n;i++){
if(!vis[i]&&dis[i]<minn){
minn=dis[i],now=i;
}
}
}
}
(2)堆優化
我們通過學習朴素$Dij$算法,明白$Dij$算法的實現需要從頭到尾掃一遍點找出最小的點然后進行松弛。這個掃描操作就是坑害朴素$DIJ$算法時間復雜度的罪魁禍首。
所以我們使用小根堆,用優先隊列來維護這個“最小的點”。從而大大減少$Dij$算法的時間復雜度。
前置芝士:
1.pair
$pair$是$C++$自帶的二元組。我們可以把它理解成一個有兩個元素的結構體。
更刺激的是,這個二元組有自帶的排序方式:以第一關鍵字為關鍵字,再以第二關鍵字為關鍵字進行排序。
所以,我們用二元組的$first$位存距離,$second$位存編號即可。
typedef pair<int,int> p;
priority_queue<p,vector<p>,greater<p> >q;
定義一個按pair排好的小根堆;
2.怎么往pair類型的優先隊列里加元素
q.push(make_pair(first,second))
實現:
我們需要往優先隊列中$push$最短路長度,但是它一旦入隊,就會被優先隊列自動維護離開原來的位置,換言之,我們無法再把它與它原來的點對應上,也就是說沒有辦法形成點的編號到點權的映射。
我們用$pair$解決這個問題,參考前置芝士。
代碼:
void dijkstra(){
for(int i=1;i<=n;i++)dis[i]=INF;
dis[s]=0;q.push(make_pair(0,s));
while(!q.empty()){
int u=q.top().second;q.pop();
if(vis[u]) continue; vis[u]=1;
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w){
dis[v]=dis[u]+e[i].w;
q.push(make_pair(dis[v],v));
}
}
}
}
$Bellman-Ford$算法
不會……
$SPFA$算法
關於:
$SPFA$是$Bellman-Ford$算法的一種隊列實現,減少了不必要的冗余計算。
主要思想:
初始時將起點加入隊列。每次從隊列中取出一個元素,並對所有與它相鄰的點進行修改,若某個相鄰的點修改成功,則將其入隊。直到隊列為空時算法結束。
這個算法,簡單的說就是隊列優化的$bellman-ford$,利用了每個點不會更新次數太多的特點發明的此算法。
$SPFA$ 在形式上和廣度優先搜索非常類似,不同的是廣度優先搜索中一個點出了隊列就不可能重新進入隊列,但是$SPFA$中一個點可能在出隊列之后再次被放入隊列,也就是說一個點修改過其它的點之后,過了一段時間可能會獲得更短的路徑,於是再次用來修改其它的點,這樣反復進行下去。
($ps$:為什么$SPFA$可以處理負邊:
因為在SPFA中每一個點松弛過后說明這個點距離更近了,所以有可能通過這個點會再次優化其他點,所以將這個點入隊再判斷一次,而$Dijkstra$中是貪心的策略,每個點選擇之后就不再更新,如果碰到了負邊的存在就會破壞這個貪心的策略就無法處理了。)
代碼:
void spfa() {
for(int i=1; i<=n; i++) dis[i]=INF;
q.push(s); vis[s]=1;dis[s]=0;int u,v;
while(!q.empty()) {
u=q.front();q.pop();vis[u]=0;
for(int i=head[u]; i; i=e[i].nxt) {
v=e[i].v;
if(dis[v]>dis[u]+e[i].w) {
dis[v]=dis[u]+e[i].w;
if(!vis[v]) {
vis[v]=1;q.push(v);
}
}
}
}
}