B-F
適用條件&範圍
- 單源最短路徑(從源點s到其它所有頂點v);
- 有向圖&無向圖(無向圖可以看作(u,v),(v,u)同屬於邊集E的有向圖);
- 邊權可正可負(如有負權迴路輸出錯誤提示);
- 差分約束系統;
演算法描述
- 對每條邊進行|V|-1次Relax ( 就是鬆弛操作 )操作;
- 如果存在(u,v)∈E使得dis[u]+w<dis[v],則存在負權迴路;否則dis[v]即為s到v的最短距離,pre[v]為前驅。
For i:=1 to |V|-1 do //v為頂點數For 每條邊(u,v)∈E do //對每條邊進行遍曆 Relax(u,v,w);For每條邊(u,v)∈E do If dis[u]+w<dis[v] Then Exit(False)
時空複雜度
演算法時間複雜度O(VE)。因為演算法簡單,適用範圍又廣,雖然複雜度稍高,仍不失為一個很實用的演算法。
演算法的改進---> SPFA 複雜度O(KE),k ≈ 2
演算法簡介
SPFA(Shortest Path Faster Algorithm)是Bellman-Ford演算法的一種隊列實現,減少了不必要的冗餘計算。也有人說SPFA本來就是Bellman-Ford演算法,現在廣為流傳的Bellman-Ford演算法實際上是山寨版。
演算法流程
演算法大致流程是用一個隊列來進行維護。 初始時將源排入佇列。 每次從隊列中取出一個元素,並對所有與他相鄰的點進行鬆弛(見下文最後),若某個相鄰的點鬆弛成功,則將其入隊。
直到隊列為空白時演算法結束。
這個演算法,簡單的說就是隊列最佳化的bellman-ford,利用了每個點不會更新次數太多的特點發明的此演算法
SPFA——Shortest Path Faster Algorithm,它可以在O(kE)的時間複雜度內求出源點到其他所有點的最短路徑,可以處理負邊。SPFA的實現甚至比Dijkstra或者Bellman_Ford還要簡單:
設Dist代表S到I點的當前最短距離,Fa代表S到I的當前最短路徑中I點之前的一個點的編號。開始時Dist全部為+∞,只有Dist[S]=0,Fa全部為0。
維護一個隊列,裡面存放所有需要進行迭代的點。初始時隊列中只有一個點S。用一個布爾數組記錄每個點是否處在隊列中。
每次迭代,取出隊頭的點v,依次枚舉從v出發的邊v->u,設邊的長度為len,判斷Dist[v]+len是否小於Dist[u],若小於則改進Dist[u],將Fa[u]記為v,並且由於S到u的最短距離變小了,有可能u可以改進其它的點,所以若u不在隊列中,就將它放入隊尾。這樣一直迭代下去直到隊列變空,也就是S到所有的最短距離都確定下來,結束演算法。若一個點入隊次數>=n,則有負權環。
SPFA 在形式上和寬度優先搜尋非常類似,不同的是寬度優先搜尋中一個點出了隊列就不可能重新進入隊列,但是SPFA中一個點可能在出隊列之後再次被放入隊列,也就是一個點改進過其它的點之後,過了一段時間可能本身被改進,於是再次用來改進其它的點,這樣反覆迭代下去。設一個點用來作為迭代點對其它點進行改進的平均次數為k,有辦法證明對於通常的情況,k在2左右
數組實現鄰接表模板
#define N 100004const int INF = (1<<30);int n,m;//n是最後一個點的編號struct edge{ int u,v,w; int next;//同一起點的下一條邊儲存在edge數組中的位置(理解了這個靜態鄰接表就可以了)}e[N*10];int head[N];//以該點為起點的第一條邊儲存在e數組中的位置int dis[N];//記錄與源點距離bool vis[N];//記錄頂點是否在隊列中,SPFA演算法可以入隊列多次int cnt[N];//記錄頂點入隊列次數int ecnt;void init(){ ecnt = 0; memset(head,-1,sizeof(head));}void add(int u,int v,int w){ e[ecnt].u = u; e[ecnt].v = v; e[ecnt].w = w; e[ecnt].next = head[u]; head[u] = ecnt++;//位置更新}bool SPFA(int s){//s是源點編號 queue<int> qq; int i; for(i=1;i<=n;++i){ dis[i] = INF; //將除源點以外的其餘點的距離設定為無窮大 vis[i] = 0; cnt[i] = 0; } dis[s]=0; //源點的距離為0 vis[s] = 1; cnt[s]++; //源點的入隊列次數增加 qq.push(s); int u,v; while(!qq.empty()){ u = qq.front(); qq.pop(); vis[u] = 0; for(i=head[u];i!=-1;i = e[i].next){ v = e[i].v; int cost = e[i].w; if(dis[v] > cost+dis[u]){ dis[v] = cost+dis[u]; if(!vis[v]){ vis[v] = 1; qq.push(v); cnt[v]++; if(cnt[v] >= n)return false; } } } } return true;}
vector實現鄰接表模板
#define N 50001 const int INF = 0x7fffffff;int n,m;typedef struct edge{ int to; int w;}edge,temp;vector<edge> adjmap[N]; //vector實現鄰接表int d[N];bool vis[N]; //記錄頂點是否在隊列中,SPFA演算法可以入隊列多次int cnt[N]; //記錄頂點入隊列次數void SPFA(){ queue<int> myqueue; int i; for(i=2;i<=n;++i) d[i] = INF; //將除源點以外的其餘點的距離設定為無窮大 memset(vis,0,sizeof(vis)); memset(cnt,0,sizeof(cnt)); d[1]=0; //源點的距離為0 vis[1] = true; cnt[1]++; //源點的入隊列次數增加 myqueue.push(1); int topint; while(!myqueue.empty()) { topint = myqueue.front(); myqueue.pop(); vis[topint] = false; for(i=0;i<adjmap[topint].size();++i) { int to = adjmap[topint][i].to; if(d[topint]<INF && d[to]>d[topint]+ adjmap[topint][i].w) { d[to] = d[topint]+ adjmap[topint][i].w; if(!vis[to]) { vis[to] = true; cnt[to]++; if(cnt[to]>=n) //當一個點入隊的次數>=n時就證明出現了負環。 return ; myqueue.push(to); } } } } printf("%d/n",d[n]);}int main(){ freopen("a.txt","r",stdin); scanf("%d%d",&n,&m); int i; int s,e,w; edge temp; for(i=1;i<n+1;++i) //此處特別注意對鄰接表清空 adjmap[i].clear(); for(i=0;i<m;++i) //雙向 { cin>>s>>e>>w; temp.to = e; temp.w = w; adjmap[s].push_back(temp); temp.to = s; adjmap[e].push_back(temp); } SPFA(); return 0;}
數組版
int map[N][N],dist[N],vis[N];void SPFA(int s){ int i,j; for(i=0;i<=n;i++){ dist[i] = maxsum; vis[i] = 0; } queue<int> q; dist[s]=0; q.push(s); vis[s]=1; while(!q.empty()){ int x=q.front(); q.pop(); vis[x]=0; for(i=1;i<=n;i++){ if(map[x][i] != INF && dist[x]+map[x][i]<dist[i]){ dist[i]=dist[x]+map[x][i]; if(!vis[i]){ vis[i]=1; q.push(i); } } } }}
如果要記錄最短路徑的話就需要開多一個數組pre[i],若且唯若dis[i]+map[i][j]<dis[j]時更新pre[j] = i