1. 程式人生 > >最短路徑演算法總結(Floyd,bellmen-ford,dijkstra,Spfa)

最短路徑演算法總結(Floyd,bellmen-ford,dijkstra,Spfa)

Dijkstra:適用於權值為非負的圖的單源最短路徑,用斐波那契堆的複雜度O(E+VlgV) BellmanFord:適用於權值有負值的圖的單源最短路徑,並且能夠檢測負圈,複雜度O(VE) SPFA:適用於權值有負值,且沒有負圈的圖的單源最短路徑,論文中的複雜度O(kE),k為每個節點進入Queue的次數,且k一般<=2,但此處的複雜度證明是有問題的,其實SPFA的最壞情況應該是O(VE). Floyd:每對節點之間的最短路徑。

Bellman-Ford演算法

Bellman-Ford演算法能在更普遍的情況下(存在負權邊)解決單源點最短路徑問題。對於給定的帶權(有向或無向)圖 G=(V,E),其源點為s,加權函式 w 是邊集 E 的對映。對圖G執行Bellman-Ford演算法的結果是一個布林值,表明圖中是否存在著一個從源點s可達的負權迴路。若不存在這樣的迴路,演算法將給出從源點s到圖G的任意頂點v的最短路徑d[v]。

Bellman-Ford演算法流程分為三個階段:

(1)初始化:將除源點外的所有頂點的最短距離估計值 d[v] ←+∞, d[s] ←0;
(2)迭代求解:反覆對邊集E中的每條邊進行鬆弛操作,使得頂點集V中的每個頂點v的最短距離估計值逐步逼近其最短距離;(執行|v|-1次)

(3)檢驗負權迴路:判斷邊集E中的每一條邊的兩個端點是否收斂。如果存在未收斂的頂點,則演算法返回false,表明問題無解;否則演算法返回true,並且從源點可達的頂點v的最短距離儲存在 d[v]中。

適用條件和範圍:
  1.單源最短路徑(從源點s到其它所有頂點v); 
  2.有向圖&無向圖(無向圖可以看作(u,v),(v,u)同屬於邊集E的有向圖); 


  3.邊權可正可負(如有負權迴路輸出錯誤提示); 
  4.差分約束系統;

bool bellman()  
{  
    bool flag ;  
    for(int i=0;i<n-1;i++)  
    {  
        flag=false;  
        for(int j=0;j<all;j++) //窮舉每條邊 
            if(dis[t[j].to]>dis[t[j].from]+t[j].vis) //鬆弛判斷 
            {  
                dis[t[j].to]=dis[t[j].from]+t[j].vis;  //鬆弛操作 
                flag=true;  
            }  
            if(!flag)  
            break;  
    }  
    for(int k=0;k<all;k++) //對所有邊進行一次遍歷,判斷是否有負迴路 
    if(dis[t[k].to]>dis[t[k].from]+t[k].vis)  
    return true;  
    return false;  
}  

Dijkstra演算法

演算法流程:
(a) 初始化:用起點v到該頂點w的直接邊(弧)初始化最短路徑,否則設為∞;
(b) 從未求得最短路徑的終點中選擇路徑長度最小的終點u:即求得v到u的最短路徑;
(c) 修改最短路徑:計算u的鄰接點的最短路徑,若(v,…,u)+(u,w)<(v,…,w),則以(v,…,u,w)代替。
(d) 重複(b)-(c),直到求得v到其餘所有頂點的最短路徑。
特點:總是按照從小到大的順序求得最短路徑。

假設一共有N個節點,出發結點為s,需要一個一維陣列vis[N]來記錄前一個節點序號,一個一維陣列dis[N]來記錄從原點到當前節點最短路徑(初始值為s到Vi的邊的權值,沒有則為+∞),一個二維陣列map[N][N]來記錄各點之間邊的權重,按以上流程更新map[N]和dis[N]。

void dijs(int v)//v為原點   
{  
    int i,j,k;  
    for(i=1;i<=n;i++)  
    dis[i]=map[v][i];//初始化   
    memset(vis,0,sizeof(vis));  
    vis[v]=1;  
    for(i=2;i<=n;i++)  
    {  
        int min=INF;  
        k=v;  
        for(j=1;j<=n;j++)  
        {  
            if(!vis[j]&&min>dis[j])  
            {  
                k=j;  
                min=dis[j];//在dis中找出最小值   
             }   
        }  
        vis[k]=1;//使k為已生成終點   
        for(j=1;j<=n;j++)//修改dis   
        {  
            if(dis[j]>dis[k]+map[k][j])  
            dis[j]=dis[k]+map[k][j];  
        }  
    }  
}  
SPFA演算法

求最短路徑的演算法有許多種,除了排序外,恐怕是OI界中解決同一類問題演算法最多的了。最熟悉的無疑是Dijkstra,接著是Bellman-Ford,它們都可以求出由一個源點向其他各點的最短路徑;如果我們想要求出每一對頂點之間的最短路徑的話,還可以用Floyd-Warshall。

SPFA是這篇日誌要寫的一種演算法,它的效能非常好,程式碼實現也並不複雜。特別是當圖的規模大,用鄰接矩陣存不下的時候,用SPFA則可以很方便地面對臨接表。每個人都寫過廣搜,SPFA的實現和廣搜非常相似。

如何求得最短路徑的長度值?

首先說明,SPFA是一種單源最短路徑演算法,所以以下所說的“某點的最短路徑長度”,指的是“某點到源點的最短路徑長度”。

我們記源點為S,由源點到達點i的“當前最短路徑”為D[i],開始時將所有D[i]初始化為無窮大,D[S]則初始化為0。演算法所要做的,就是在執行過程中,不斷嘗試減小D[]陣列的元素,最終將其中每一個元素減小到實際的最短路徑。

過程中,我們要維護一個佇列,開始時將源點置於隊首,然後反覆進行這樣的操作,直到佇列為空:

(1)從隊首取出一個結點u,掃描所有由u結點可以一步到達的結點,具體的掃描過程,隨儲存方式的不同而不同;

(2)一旦發現有這樣一個結點,記為v,滿足D[v] > D[u] + w(u, v),則將D[v]的值減小,減小到和D[u] + w(u, v)相等。其中,w(u, v)為圖中的邊u-v的長度,由於u-v必相鄰,所以這個長度一定已知(不然我們得到的也不叫一個完整的圖);這種操作叫做鬆弛。

引用內容 引用內容 鬆弛操作的原理是著名的定理:“三角形兩邊之和大於第三邊”,在資訊學中我們叫它三角不等式。所謂對i,j進行鬆弛,就是判定是否d[j]>d[i]+w[i,j],如果該式成立則將d[j]減小到d[i]+w[i,j],否則不動。


(3)上一步中,我們認為我們“改進了”結點v的最短路徑,結點v的當前路徑長度D[v]相比於以前減小了一些,於是,與v相連的一些結點的路徑長度可能會相應地減小。注意,是可能,而不是一定。但即使如此,我們仍然要將v加入到佇列中等待處理,以保證這些結點的路徑值在演算法結束時被降至最優。當然,如果連線至v的邊較多,演算法執行中,結點v的路徑長度可能會多次被改進,如果我們因此而將v加入佇列多次,後續的工作無疑是冗餘的。這樣,就需要我們維護一個bool陣列Inqueue[],來記錄每一個結點是否已經在佇列中。我們僅將尚未加入佇列的點加入佇列。

void spfa()
{
	int i,k;
	memset(vis,0,sizeof(vis));
	for(i=1;i<=n;i++)
		dis[i]=INF;//初始化 
	dis[1]=0;
	queue<int>q;//建立佇列 
	vis[1]=1;
	q.push(1);//源點放入隊尾 
	 
	while(!q.empty())
	{
		k=q.front();//從隊首取出一個節點,掃描所有從該節點可以到達的終點 
		q.pop();
		vis[k]=0;
		for(i=1;i<=n;i++)
		{
			if(dis[i]>dis[k]+map[k][i])//鬆弛判斷 
			{
				dis[i]=dis[k]+map[k][i];//鬆弛操作 
			    if(vis[i]==0)//判斷這個點是否在佇列裡面,如果不在加入佇列 
			    {
				    q.push(i);
				    vis[i]=1;
			    }
			}
			
		}
	}	
}
Floyd演算法


這裡需要用到動態規劃的思想,對於任何一個城市而言,i 到 j 的最短距離不外乎存在經過 i 與 j 之間的k和不經過k兩種可能,所以可以令k=1,2,3,...,n(n是城市的數目),再檢查d(ij)與d(ik)+d(kj)的值;在此d(ik)與d(kj)分別是目前為止所知道的 i 到 k 與 k 到 j 的最短距離,因此d(ik)+d(kj)就是 i 到 j 經過k的最短距離。所以,若有d(ij)>d(ik)+d(kj),就表示從 i 出發經過 k 再到j的距離要比原來的 i 到 j 距離短,自然把i到j的d(ij)重寫為d(ik)+d(kj)<這裡就是動態規劃中的決策>,每當一個k查完了,d(ij)就是目前的 i 到 j 的最短距離。重複這一過程,最後當查完所有的k時,d(ij)裡面存放的就是 i 到 j 之間的最短距離了<這就是動態規劃中的記憶化搜尋>。利用一個三重迴圈產生一個儲存每個結點最短距離的矩陣. 
用三個for迴圈把問題解決了,但是有一個問題需要注意,那就是for迴圈的巢狀的順序:我們可能隨手就會寫出這樣的列舉程式,但是仔細考慮的話,會發現是有問題的:
for i:=1 to n do
for j:=1 to n do
for k:=1 to n do
if.....
問題出在我們太早的把i-k-j的距離確定下來了,假設一旦找到了i-p-j最短的距離後,i到j就相當處理完了,以後不會在改變了,一旦以後有使i到j的更短的距離時也不能再去更新了,所以結果一定是不對的。所以應當象下面一樣來寫程式:
for k:=1 to n do
for i:=1 to n do
for j:=1 to n do
if .....

這樣作的意義在於固定了k,把所有i到j而經過k的距離找出來,然後象開頭所提到的那樣進行比較和重寫,因為k是在最外層的,所以會把所有的i到j都處理完後,才會移動到下一個K。

        for(i=0;i<=m;i++)
        for(j=0;j<=n;j++)
        map[i][j]=INF;//初始化 
		for(k=1;k<=n;k++)//動態規劃的思想  
        for(i=1;i<=n;i++)  
        for(j=1;j<=n;j++)  
        {  
            if(i==j)  
            continue;  
            if(map[i][j]>map[i][k]+map[k][j])  
            map[i][j]=map[i][k]+map[k][j];  
        }