1. 程式人生 > >最短路小結(三種演算法+各種常見變種)

最短路小結(三種演算法+各種常見變種)

額,博主只是做了幾(約數)道題而已,寫這篇小結純粹想留作紀念(勿噴,但是可以交流)(那啥,轉載的話註明一下來源。。打字不容易。。)
最短路呢,包括三種演算法,但是各有各的變種,其中變化有很多。
簡單記錄一下。
首先是三種演算法:
1、Dijkstra演算法。(單源點最短路徑)
雙手奉上啊哈雷演算法
然後開始敘述我所理解(如有雷同。。那就雷同吧)的:
有n個點,相互之間可能有所連線或者沒有,那麼現在,如果讓求點1到點n之間的最短路,該怎麼求?
這裡面有個很有趣的東西,叫做鬆弛,翻譯過來就是這麼個意思:
假如已知最短路1-3長度為5,最短路1->4長度為7(無論是1-3,還是1-4均是當前最短,也可以理解成當前最優(dp)),而3->4的長度為1,那麼這個時候,明顯可以知道:1->3->4的長度是小於1->4的,那麼現在把1-4更新到當前最優,長度也就是變成了6。
以上就是鬆弛操作。也就是假設在1-n中有一個x點,且用d陣列表示1~其他點的當前最優最短路,用w二維陣列表示兩點之間的距離,那麼給定一個當前最優點y,如果d[y]>d[x]+w[x][y],是不是說明d[y]可以有更小的值呢?答案顯而易見。
那麼這是一個點,題目中一共n個點,那麼我用每一個點都去更新1到其他點的距離,那麼全部更新過後,是不是d[n]表示的就是從1~n的最短路了呢?
而這一切,每一次更細都相當於在所有任意的兩個點(其中一個點是1(起點))裡插入第三個點,看是否能夠鬆弛,若能,就鬆弛,不能,就不管了唄。
還有,既然,n個點都要去更新一遍,那麼誰先去更新呢?
答案:當然是d值最小的那個點去更新,因為用最小的去更新,才可能更新的動呀。但是既然,只需要每個點更新一次,那麼要做個標記,以防更新過的點再次進行更新。
d值會越來越小,那麼初值肯定是越大越好咯。
但是呢,既然d陣列代表的是1到其餘各點的最短距離,那麼d[1]肯定是0。。
例題部落格(基本上我第一次寫的題部落格都很長。。):

Til the Cows Come Home
來啊,程式碼伺候!!!

int INF=0x3f3f3f3f
for(int i=1;i<=n;i++)
{
    d[i]=INF;
    vis[i]=0;
}
d[1]=0;
for(int i=1;i<=n;i++)//每個點都要參與更新,所以迴圈n次
{
    int m=INF,x=-1;//m是為了找出最小那一個,x記錄節點
    for(int j=1;j<=n;j++)
    {
        if(!vis[j]&&d[j]<m)
        {
            m=d[x=j];
} } if(x!=-1) { vis[x]=1;//用於更新過了,就要標記 for(int j=1;j<=n;j++)//用最小的d[x]去更新d[j] { if(d[j]>
d[x]+w[x][j]) { d[j]=d[x]+w[x][j]; } } } }

2、bellman(附加spfa)
既然已經知道什麼是鬆弛(不知道的再看一遍,因為是環環相扣的),接下來就說一下什麼是bellman。
剛才已經說了,Dijkstra演算法是用n個點去更新從1點到其餘各點(這裡的1代表起點,誰是起點看題意,只需要把起點的d變成0就可以了)的最短路,那麼也就是用的邊去更新兩點之間的距離。
然後就提出了bellman,思想是從源點逐次經過其他點,以縮短到達終點的距離,假設n個點不存在負權值迴路,那麼最多存在n-1條邊,因為假設存在超過n-1條邊,那麼肯定會重複經過一個點,那麼最短路就可以更新:
Bellman-Ford演算法構造一個最短路徑長度陣列序列:dist(1)[u],dist(2)[u],dist(3)[u],…,dist(n-1)[u]。其中:
dist(1)[u]為從源點v0到終點u的只經過一條邊的最短路徑的長度,並有dist(1)[u]=edge[v0,u]。
dist(3)[u]為從源點v0出發最多經過不構成負權值迴路的3條邊到達終點u的最短路徑長度。
……
dist(n-1)[u]為從源點v0出發最多經過不構成負權值迴路的n-1條邊到達終點u的最短路徑長度。
演算法的最終目的是計算出dist(n-1)[u],為源點v0到頂點u的最短路徑長度,也就是利用n-1條邊更新過後的最短距離。
採用遞推方式計算dist(k)[u]。
設已經求出dist(k-1)[u],u=0,1,…,n-1,此即從源點v0最多經過不構成負權值迴路的k-1條邊到達終點u的最短路徑的長度。
從圖的鄰接矩陣可以找出各個頂點j到達頂點u的(直接邊)距離edge[j,u],計算min{distk-1[j]+edge[j,u]},可得從源點v0途經各個頂點,最多經過不構成迴路的k條邊到達終點u的最短路徑的長度。 比較dist(k-1)[u]和min{dist(k-1)[j]+edge[j,u]},取較小者作為dist(k)[u]的值。
因此Bellman-Ford演算法的遞推公式(求源點v0到各頂點u的最短路徑)為: 初始:dist(1)[u]=edge[v0,u],v0是源點 遞推:dist(k)[u]=min{dist(k-1)[u],min{dist(k-1)[j]+edge[j,u]}} j=0,1,…,n-1,j<>u; k=2,3,4,…,n-1 。
所以,n-1次過後便可以得到最短路,如果n-1次後依舊可以更新,那麼說明存在負權迴路或者是正權迴路。
其次,對於無論是Dijkstra還是Bellman,均有一個選點去更新的操作,那麼有的時候,可能選出的點不能對任何點進行更新,所以造成了時間的浪費,而且,還有一句是被更新過的點一定可以去更新其他點(不知道對不對。。),然後呢,就會想到,為什麼不把每次更新過的點裝進一個容器呢?直到再沒有點可以裝進容器(代表最短路已是最優,更新完畢)。
然後想到了陣列,想到了棧和佇列,但是呢,陣列模擬需要一陣列還要一指標變數時刻維護,所以就用棧和佇列吧,相同的時間複雜度,但是呢,還有一點,選出儘量小的去更新其他點,這樣更好。所以,考慮優先佇列,那為啥不考慮優先。。棧呢?因為意義一樣呀。。。
回到正,負權的問題上,怎樣進行初始化呢、、
對於負權,自然是越來越小,所以陣列賦為極大值,正權的話,賦為0好了。
給出一篇判定負環的部落格:

Wormholes
給出一篇判定正環的部落格:Arbitrage,內含兩種判定正環的方法,一個是bellman,一個是floyed(下文有)。
給出一篇使用spfa的題解(模板):Til the Cows Come Home
然後,奉上優先佇列(經典,當然不是我的。。)部落格:優先佇列詳解
以上就是bellman和spfa用法,對了,提到spfa就不得不提到差分約束系統,自行看下(嘗試理解,因矩陣知識淺薄,所以只會最基礎的):Candies,裡面有差分約束的部落格,但是最好還是先看下題,理解這種思想用於哪個方面。

3、floyed
floyed是求任意兩點間的最短路演算法,因為三個for讓他擁有不小的侷限性,若是1s的話,n的範圍只能是100以下,若是bellman能夠理解成為利用n-1條邊去更新起點到定點的最短路,記錄的是前i條邊更新過後的狀態,那麼floyed就可以理解為利用點去更新,記錄的是前i個點更新過後的當前最優狀態,三個for,假如分別是k,i,j,那麼每次都會利用k作為中間橋樑,詢問,i->j,i->k->j這兩個哪個更短。。。保留當前最優,直到利用所有點把這對i,j更新n遍。(也就是n遍Dijkstra),這樣理解會不會容易一些。
然後呢,floyed會牽扯出一個問題,叫做傳遞閉包問題,也就是關係的一個轉換而已,給出一道例題解析:Cow Contest,這種題資料小的話(<=100)直接floyed暴力過,大了的話,就又牽扯出一個概念,叫做強連通分量,又有三種演算法(暫不解釋)。

以上呢,就是三種演算法,在這裡,補充一點Dijkstra的變種,其實也不算是變種,只是鬆弛操作的內容有點區別而已,都是共通的。
其一呢:最小生成樹之prim演算法(補了一覺繼續寫。。)。
現在我不再區別最短路的三種演算法,統稱為最短路。
那麼思考最短路與最小生成樹的區別,一樣是求的最小值,但是最短路兩點之間的最短路,而最小生成樹求得是將全圖的點連起需要的最小長度。
給出百度百科解釋(很容易理解):prim演算法百度百科,每次都找已經找過的點的最小距離,那麼利用程式碼怎麼實現呢?
找到離部分連通圖最近的d值的點,加入標記,然後用這條邊去更新所有沒有被標記的點的距離。
附程式碼:

void prim()
{
    for(int i=1;i<=n;i++)
        d[i]=w[1][i];//初始化,也可以全設為INF
    vis[1]=1;
    int sum=0;
    for(int i=2;i<=n;i++)
    {
        int minn=INF,pos=0;
        for(int j=1;j<=n;j++)//選出一個最近的點
        {
            if(!vis[j]&&d[j]<minn)
            {
                minn=d[pos=j];
            }
        }
        sum+=minn;//求最小距離
        vis[pos]=1;
        for(int j=1;j<=n;j++)//更新沒有被標記的點
            if(!vis[j]&&d[j]>w[pos][j])
                d[j]=w[pos][j];
    }
}

d[j]>w[pos][j]是指:因為只要把全圖連線起來便好,所以不用像最短路那樣嚴格控制是一條路。並且,d陣列的用法在變種裡面都代表不同的含義,因為之前在寫題解的時候寫過了,所以直接給出那道題(內含d陣列的講解)Frogger,然後給出一道模板題:Jungle Roads
其二呢,就是剛才給出的那道Frogger,這類題的基本題意是最一個有向或者無向圖裡,從一個起點(假設為st)到一個重點(假設為ed)有很多條路,那麼,每一條路呢都有一個最大長度邊和一個最小長度邊,那麼這類題就會拿這個做文章,問:從st到ed裡所有路的最小邊長的最大值是多少?或者是從st到ed裡所有路的最大邊長的最小值是多少?
而這些呢,主要就是d陣列的差異,給出兩道題,分別對應兩種問法。
Frogger
Heavy Transportation
那麼整理到這裡,也就是我花了那麼多天的做的專題的成果了。
參考:floyed演算法bellman演算法
對了對了,再補充一個鄰接表,雙手奉上啊哈雷大大的鄰接表部落格,然後,就是自己的補充了:

假設一個有向圖,存在n個點,m條邊
那麼:
我見過的有兩種方式:
①
struct djh
{
    int u,v,w;代表含義分別是:左端點,右端點,邊長
}edge[];這個陣列的範圍是按照m的範圍而定的
int first[]這個陣列的範圍是按照n的範圍而定的
int next[]這個陣列的範圍是按照m的範圍而定的
初始化:
for(int i=1;i<=n;i++)
{
    first[i]=-1;
}
for(int i=1;i<=m;i++)
{
    next[i]=-1;
}
輸入邊的時候:
for(int i=1;i<=m;i++)
{
    scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].w);
    next[i]=first[edge[i].u];
    first[edge[i].u]=i;
}
dijs或者spfa或者其他引用的寫法:
選出一個點;
res
for(int i=first[res];i!=-1;i=next[i])
{
    if(dis[edge[i].v>dis[edge[i].u]+edge[i].w)
    {
        dis[edge[i].v=dis[edge[i].u]+edge[i].w
    }
}

②
struct djh
{
    int to,w,next;分別代表右端點,邊長,next陣列
}edge[];這個陣列的範圍是按照m的範圍而定的
int first[]這個陣列的範圍是按照n的範圍而定的
初始化不變
輸入邊的時候:
tot=0;
while(m--)
{
    int u,v,w;
    scanf("%d%d%d",&u,&v,&w);
    edge[tot].nexx=first[u];
    edge[tot].v=v;
    edge[tot].w=w;
    first[u]=tot++;
}
dijs或者spfa或者其他引用的寫法:
選出一個點;
res
for(int i=first[res];i!=-1;i=edge[i].nexx)
{
    if(d[edge[i].v]>d[res]+edge[i].w)
    {
        d[edge[i].v]=d[res]+edge[i].w;
    }
}

以上便是個人用法。