最短路徑演算法總結(Floyd,bellmen-ford,dijkstra,Spfa)
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必相鄰,所以這個長度一定已知(不然我們得到的也不叫一個完整的圖);這種操作叫做鬆弛。
(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];
}