1. 程式人生 > 其它 >Bellman-Ford演算法思路詳解及SPFA演算法簡介

Bellman-Ford演算法思路詳解及SPFA演算法簡介

一、Bellman-Ford演算法:   首先科普一下,Bellman-Ford演算法是由發明者Richard Bellman(理查德.貝爾曼, 動態規劃的提出者)和Lester Ford命名的,可以處理路徑權值為負數時的單源最短路徑問題。【Dijkstra演算法的貪心思路無法處理負權邊】   演算法核心:Bellman-Ford演算法基於動態規劃,反覆利用已有的邊來更新最短距離,Bellman-Ford演算法的核心思想時鬆弛。 如果dist[u]和dist[v]滿足dist[v]<=dist[u]+map[u][v],dist[v]就應該被更新為dist[u]+map[u][v]。 反覆地利用上式對dist陣列進行鬆弛,如果沒有負權迴路的話,應當會在n-1次鬆弛之後結束。
n個頂點圖的最短路,最短路中最長的路徑上包含所有的n個頂點,即n-1條邊。所以對所有邊鬆弛操作,進行n-1輪,就可以求得最短路了。 原因在於考慮對每條邊進行鬆弛1次的時候,得到的實際上是最多經過1條邊的最短路徑, 對每條邊進行兩次鬆弛的時候得到的是至多經過2條邊的最短路徑……依次類推 如果沒有負權迴路,那麼任意兩點間的最短路徑至多經過n-1條邊,因此經過n-1次操作後應當可以得到最短路徑。 如果由負權迴路,那麼第n次鬆弛操作仍然會成功,Bellman-Ford演算法就是利用這個性質判定負環
#include<bits/stdc++.h>
using namespace std;

const int MN=1005,INF=0x3f3f3f3f;
int d[MN],ans,n,m,en,s;
struct Edge{
	int u,v,w; //u,v為頂點,w為邊權
}ed[MN]; //邊集陣列,不關心邊之間的對應關係,沒有定義nxt模擬指標欄位
/*該圖有n個結點,m條邊*/
bool Bellman(int s){
	memset(d,0x3f,sizeof(d)); //單源最短路記錄陣列初始化為INF
	d[s]=0; //源點s,最短路為0
	for(int i=1;i<n;++i) //n-1 次鬆弛即可
		for(int j=1;j<=m;++j) //每次鬆弛所有的邊
		{
			int tu=ed[j].u,tv=ed[j].v,tw=ed[j].w; 
			if(d[tv]>d[tu]+tw) //鬆弛檢測
				d[tv]=d[tu]+tw; 
		}
	for(int j=1;j<=m;++j)
	{//驗證是否有環,如果n次鬆弛之後,還可以鬆弛,就是有環
		int tu=ed[j].u,tv=ed[j].v,tw=ed[j].w;
		if(d[tv]>d[tu]+tw)
			return 0; //有環返回0
	}
	return 1;//無環返回1
}

 

 Bellman-Ford 演算法優化優化:

迴圈的提前退出:

  在實際操作中,貝爾曼-福特演算法經常會在未達到 |V| - 1 次前就出解,|V| -1 其實是最大值。於是可以在迴圈中設定判定,在某次迴圈不再進行鬆弛時,直接退出迴圈,進行負權環判定。【類似於氣泡排序的優化】

      由讀者自行完成實現,可以用bool變數每輪是否有鬆弛操作。

 

 

二、SPFA:Bellman-Ford 演算法的佇列優化實現

是一個用於求解有向帶權圖單源最短路徑的改良的貝爾曼-福特演算法(當然也可以通過將每條邊換為兩條逆向的邊來用於無向圖)。這一演算法被認為在隨機的稀疏圖上表現出色,並且極其適合帶有負邊權的圖。然而SPFA在最壞情況的時間複雜度與Bellman-Ford演算法相同,因此在非負邊權的圖中仍然最好使用Dijkstra。

原理:

  基於Bellman-Ford之外,再可以確定,鬆弛操作必定只會發生在最短路徑前導節點鬆弛成功過的節點上,用一個佇列記錄鬆弛過的節點,可以避免了冗餘計算

優化:

  SPFA演算法的效能很大程度上取決於用於鬆弛其他節點的備選節點的順序。

       我們注意到其與Dijkstra很像,

       一方面,優先佇列替換成普通的FIFO佇列,

       而另一方面,一個節可以多次進入佇列點

SPFA 程式碼模板:習題:香甜的黃油 【點選連結開啟】

//【例4-6】香甜的黃油——一本通入門篇練習題:
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
int n,p,c,a[501],hd[801],en,ds[801],tot,ans=INF;
bool ext[801];
struct Edge{
	int to,nxt,dis;
}ed[2902]; 

void addEdge(int from,int to,int dis){
	en++;
	ed[en].dis=dis;
	ed[en].to=to;
	ed[en].nxt=hd[from];
	hd[from]=en;
}

void spfa(int s){	
	queue<int> qe;
	qe.push(s);
	ext[s]=1; //起點標記在佇列中
	ds[s]=0;
	while(!qe.empty()){
		int qf=qe.front();
		qe.pop();
		ext[qf]=0; //對頭結點出隊後,標記為不在佇列中
		for(int j=hd[qf];j;j=ed[j].nxt){
			int qt=ed[j].to,qw=ed[j].dis;
			
			if(ds[qt]>ds[qf]+qw){
				ds[qt]=ds[qf]+qw;
				
				if(!ext[qt]){ //不在佇列中的結點才需要再次入隊
					ext[qt]=1;
					qe.push(qt);
				}
			}
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>p>>c;
	for(int i=1;i<=n;++i)
		cin>>a[i];		
	
	for(int i=0;i<c;++i){
		int u,v,d;
		cin>>u>>v>>d;
		addEdge(u,v,d);
		addEdge(v,u,d);
	}
	for(int i=1;i<=p;++i){
		memset(ds,0x3f,sizeof(ds));
		memset(ext,0,sizeof(ext));
		spfa(i);  //呼叫p次spfa()
		tot=0;
		for(int i=1;i<=n;++i)
			tot+=ds[a[i]];
		ans=min(ans,tot);
	}
	cout<<ans<<endl;
	return 0;
}

  事實上,如果 q 是一個優先佇列,則這個演算法將極其類似於Dijkstra演算法。然而儘管這一演算法中並沒有用到優先佇列,仍有兩種可用的技巧可以用來提升佇列的質量,並且藉此能夠提高平均效能(但仍無法提高最壞情況下的效能)。兩種技巧通過重新調整 q 中元素的順序從而使得更靠近源點的節點能夠被更早地處理。

距離小者優先(Small Lable First(SLF)):【用雙端佇列】

  將總是把v壓入佇列尾端改為比較dis[v]與dis[q.front()]的大小(為了避免出現佇列為空的操作,先將v壓入隊尾),並且在v較小時將v壓入佇列的頭端。

距離大者優先(Large Lable Last(LLL)):【用優先佇列】

  我們更新佇列以確保佇列頭端的節點的距離總小於平均,並且任何距離大於平均的節點都將被移到佇列尾端。

  

 改為DFS版:

  dfs版spfa判環根據:若一個節點出現2次及以上,則存在負環。具有天然的優勢。由於是負環,所以無需像一般的spfa一樣初始化為極大的數,只需要初始化為0就夠了(可以減少大量的搜尋,但要注意最開始時for一遍)。