圖論算法(2)


$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);
                }
            }
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM