單源最短路徑:SPFA算法
概述
SPFA(Shortest Path Faster Algorithm)算法,是西南交通大學段凡丁於 1994 年發表的,其在Bellman-ford算法的基礎上加上一個隊列優化,減少了冗餘的鬆弛操作,是一種高效的最短路算法。
問題
在帶權有向圖G=(V,A)中,假設每條弧A[i]的長度爲w[i],找到由頂點V0到其餘各點的最短路徑。
算法描述
算法思想
設立一個隊列用來保存待優化的頂點,優化時每次取出隊首頂點u,並且用u點當前的最短路徑估計值dist[u]對與u點鄰接的頂點v進行鬆弛操作,如果v點的最短路徑估計值dist[v]可以更小,且v點不在當前的隊列中,就將v點放入隊尾。這樣不斷從隊列中取出頂點來進行鬆弛操作,直至隊列空爲止。
判斷有無負環:如果某個點進入隊列的次數大於等於總節點數則存在負環(SPFA無法處理帶負環的圖)。
算法過程
首先建立源點到各點的最短距離表格
首先源點入隊,當隊列非空時:
1. 隊首節點a出隊,對a的所有出邊鄰接點進行鬆弛操作(此處有b,c,d三個點),此時距離表格狀態爲:
在鬆弛時源點到這三個點的距離都變小了,且這些點現在都不在隊列內,於是將這些點入隊。
2. 隊首節點b點出隊,對b的所有出邊鄰接點進行鬆弛操作(此處只有e點),此時距離表格狀態爲:
e的距離估值也變小了,且e不在隊內,於是將e入隊。此時隊列中的節點爲c,d,e。
3. 隊首節點c出隊,對c的所有出邊鄰接點進行鬆弛操作(此處有e,f兩個點),此時距離表格狀態爲:
e,f的距離估值都變小了,但e已經在隊列中,所以只有f需要入隊。此時隊列中的節點爲d,e,f。
4. 依此類推,之後的距離表格狀態依次爲:
(爲什麼最後出現了兩張一模一樣的圖?因爲倒數第二個在隊列內的節點e對唯一一個出邊鄰接點g鬆弛不成功,g的距離估值沒有變化;最後一個在隊列內的節點b對唯一一個出邊鄰接點e鬆弛不成功,e的距離估值也沒有變化)
到這裏,隊列爲空,算法執行完成。
程序代碼
SPFA的兩種寫法,bfs和dfs,bfs判別負環不穩定,相當於限深度搜索,但是設置得好的話還是沒問題的,dfs的話判斷負環很快。
/*
FILE:spfa_bfs.cpp
LANG:C++
*/
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int node_num = 100; //最大節點數
const int INF = 2147483647;
int matrix[node_num][node_num]; //鄰接矩陣
int dist[node_num]; //距離估值
int path[node_num]; //記錄前驅節點
bool vis[node_num]; //記錄節點是否在隊列內
int v_num, a_num; //記錄節點數、弧數
bool spfa_bfs(const int);
int main()
{
cout << "v_num:";
cin >> v_num;
cout << "a_num:";
cin >> a_num;
for (int i = 0; i < v_num; ++i)
{
for (int j = 0; j < v_num; ++j)
{
matrix[i][j] = ((i != j) ? INF : 0); //初始化鄰接矩陣
}
}
int u, v, w;
for (int i = 0; i < a_num; ++i)
{
cin >> u >> v >> w;
matrix[u][v] = matrix[v][u] = w;
}
int src;
cout << "source:";
cin >> src;
if (spfa_bfs(src))
{
cout << "E" << endl; //存在負環
return 0;
}
for (int i = 0; i < v_num; ++i)
{
if (i == src)
{
continue;
}
cout << src << "->" << i << ":" << dist[i] << ":" << i;
int t = path[i];
while (t != src)
{
cout << "-" << t;
t = path[t];
}
cout << "-" << src << endl; //倒序輸出最短路徑
}
return 0;
}
bool spfa_bfs(const int src)
{
memset(vis, false, sizeof(vis));
queue<int> q;
int cnt[node_num] = {0}; //記錄每個節點的進隊次數
for (int i = 0; i < v_num; ++i)
{
dist[i] = INF; //初始化距離表
path[i] = src; //初始化前驅節點表
}
dist[src] = 0; //設置源點距離自己的距離爲0
q.push(src); //源點進隊
vis[src] = true; //打上進隊標記
++cnt[src]; //記錄進隊次數
while (!q.empty()) //隊列非空則一直循環
{
int x;
x = q.front(); //讀取隊首節點
q.pop(); //彈出隊首節點
vis[x] = false; //去除隊列標記
for (int i = 0; i < v_num; ++i)
{
if (matrix[x][i] != INF && dist[x] + matrix[x][i] < dist[i]) //如果i是讀取的節點的出邊鄰接點且可以進行鬆弛操作
{
dist[i] = dist[x] + matrix[x][i]; //鬆弛操作
path[i] = x; //更新前驅
if (!vis[i]) //如果不在隊列內
{
q.push(i); //進隊
vis[i] = true; //打上標記
++cnt[i]; //記錄進隊次數
if (cnt[i] >= v_num) //如果這個節點的進隊次數大於等於節點總數
{
return true; //說明存在負環,無法處理
}
}
}
}
}
return false;
}
/*
FILE:spfa_dfs.cpp
LANG:C++
*/
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int node_num = 100;
const int INF = 2147483647;
int matrix[node_num][node_num], dist[node_num], path[node_num];
bool vis[node_num];
int v_num, a_num;
queue<int> q;
bool spfa_dfs(const int);
int main()
{
cout << "v_num:";
cin >> v_num;
cout << "a_num:";
cin >> a_num;
for (int i = 0; i < v_num; ++i)
{
for (int j = 0; j < v_num; ++j)
{
matrix[i][j] = ((i != j) ? INF : 0);
}
}
int u, v, w;
for (int i = 0; i < a_num; ++i)
{
cin >> u >> v >> w;
matrix[u][v] = matrix[v][u] = w;
}
int src;
cout << "source:";
cin >> src;
memset(vis, false, sizeof(vis));
for (int i = 0; i < v_num; ++i)
{
dist[i] = INF;
path[i] = src;
}
dist[src] = 0;
if (spfa_dfs(src))
{
cout << "E" << endl;
return 0;
}
for (int i = 0; i < v_num; ++i)
{
if (i == src)
{
continue;
}
cout << src << "->" << i << ":" << dist[i] << ":" << i;
int t = path[i];
while (t != src)
{
cout << "-" << t;
t = path[t];
}
cout << "-" << src << endl;
}
return 0;
}
bool spfa_dfs(const int src)
{
q.push(src);
vis[src] = true;
while (!q.empty())
{
int x;
x = q.front();
q.pop();
vis[x] = false;
for (int i = 0; i < v_num; ++i)
{
if (matrix[x][i] != INF && dist[x] + matrix[x][i] < dist[i])
{
dist[i] = dist[x] + matrix[x][i];
path[i] = x;
if (!vis[i])
{
if (spfa_dfs(i))
{
return true;
}
}
}
}
}
return false;
}
示例圖:
運行結果:
算法優化
SPFA算法有兩個優化算法SLF和LLL:
SLF:Small Label First策略,設要加入的節點是j,隊首元素爲i,若dist(j)<dist(i)
,則將j插入隊首,否則插入隊尾。
LLL:Large Label Last策略,設隊首元素爲i,每次彈出時進行判斷,隊列中所有dist值的平均值爲x,若dist(i)>x
則將i插入到隊尾,查找下一元素,直到找到某一i使得dist(i)<=x
,則將i出對進行鬆弛操作。
代碼:
/*
FILE:spfa_bfs_slf_lll.cpp
LANG:C++
*/
#include <iostream>
#include <cstring>
#include <deque>
using namespace std;
const int node_num = 100;
const int INF = 2147483647;
int matrix[node_num][node_num], dist[node_num], path[node_num];
bool vis[node_num];
int v_num, a_num, sum = 0, num = 1; //sum記錄隊列中所有節點的dist之和,num記錄隊列中節點數
bool spfa_bfs_slf_lll(const int);
int main()
{
cout << "v_num:";
cin >> v_num;
cout << "a_num:";
cin >> a_num;
for (int i = 0; i < v_num; ++i)
{
for (int j = 0; j < v_num; ++j)
{
matrix[i][j] = ((i != j) ? INF : 0);
}
}
int u, v, w;
for (int i = 0; i < a_num; ++i)
{
cin >> u >> v >> w;
matrix[u][v] = matrix[v][u] = w;
}
int src;
cout << "source:";
cin >> src;
if (spfa_bfs_slf_lll(src))
{
cout << "E" << endl;
return 0;
}
for (int i = 0; i < v_num; ++i)
{
if (i == src)
{
continue;
}
cout << src << "->" << i << ":" << dist[i] << ":" << i;
int t = path[i];
while (t != src)
{
cout << "-" << t;
t = path[t];
}
cout << "-" << src << endl;
}
return 0;
}
bool spfa_bfs_slf_lll(const int src)
{
memset(vis, false, sizeof(vis));
deque<int> q;
int cnt[node_num] = {0};
for (int i = 0; i < v_num; ++i)
{
dist[i] = INF;
path[i] = src;
}
dist[src] = 0;
q.push_back(src);
vis[src] = true;
++cnt[src];
while (!q.empty())
{
int x = q.front();
q.pop_front();
if (dist[x] * num > sum) //LLL策略
{
q.push_back(x);
continue;
}
vis[x] = false;
sum -= dist[x];
--num;
for (int i = 0; i < v_num; ++i)
{
if (matrix[x][i] != INF && dist[x] + matrix[x][i] < dist[i])
{
dist[i] = dist[x] + matrix[x][i];
path[i] = x;
if (!vis[i])
{
vis[i] = true;
if (dist[i] < dist[q.front()]) //SLF策略
{
q.push_front(i);
}
else
{
q.push_back(i);
}
sum += dist[i];
++num;
++cnt[i];
if (cnt[i] >= v_num)
{
return true;
}
}
}
}
}
return false;
}