Bellman-Ford演算法及其優化
與Dijkstra演算法一樣,我們定義一幅加權有向圖的結構如下:
//帶權有向圖
struct EdgeWeightedDigraph
{
size_t V; //頂點數
size_t E; //邊數
map<int, forward_list<tuple<int, int, double>> adj; //改進後的鄰接表,tuple儲存的是邊集
}
Bellman-Ford演算法
在加權有向圖的最短路徑求解演算法中,Dijkstra演算法只能處理所有邊的權值都是非負的圖(是否有環不影響求解),而基於拓撲順序的演算法雖然能線上性時間內高效處理負權重圖,但僅侷限於無環圖。為此還需要一個更為普遍的最短路徑求解演算法:能夠處理負權重圖,也能處理有環的情況。
Bellman-Ford演算法是求含負權重圖的單源最短路徑的一種演算法。其原理為連續進行鬆弛,對於含有V個頂點的加權有向圖,在每次鬆弛時把每條邊都更新一下,若在V-1次鬆弛後還能更新,則說明圖中有負權重環,因此無法得出結果,否則就完成。
vector<int> Bellman-Ford(EdgeWeightedDigraph &g) { vector<int> edge(g.V); //定義並初始化dis[] vector<double> dis(g.V, DBL_MAX); dis.at(0) = 0.0; //進行V-1次鬆弛 for (size_t i = 0; i < g.V-1; ++i) //鬆弛計數 { for (auto ite = g.adj.cbegin(); ite != g.adj.cend(); ++ite) { for (const auto &e : (*ite).second) //鬆弛操作 { if (dis.at(get<0>(e)) + get<2>(e) < dis.at(get<1>(e))) { dis.at(get<1>(e)) = dis.at(get<0>(e)) + get<2>(e); edge.at(get<1>(e)) = get<0>(e); } } } } //判斷是否存在負權重環 for (auto ite = g.adj.cbegin(); ite != g.adj.cend(); ++ite) { for (const auto &e : (*ite).second) { if (dis.at(get<0>(e)) + get<2>(e) < dis.at(get<1>(e))) { cerr << "含有負權重環,無解\n"; vector<int> tmp; return tmp; } } } return edge; }
效能
樸素的Bellman-Ford演算法實現非常簡單,在每一輪迭代中都會放鬆E條邊,共進行V輪迭代,因此時間複雜度為O(VE)。這種實現在實際應用中並不常見,因為它的效率不高,而且我們只需要對Bellman-Ford演算法稍作修改就能大幅提高在一般場景下的執行時間。
SPFA演算法
分析Bellman-Ford演算法,最外層迴圈(迭代次數)V-1實際上是演算法是否有解的上限,因為需要的迭代遍數等於最短路徑樹的高度。如果不存在負權重環,平均情況下的最短路徑樹的高度應該遠遠小於V-1,在此情況下,多餘最短路徑樹高的迭代遍數就是時間上的浪費,由此,可以依次來實施優化。
實際上,在任意一輪中許多邊的鬆弛都不會成功:只有上一輪中的dis[]值發生變化的頂點指出的邊才能夠改變其他dis[]的值。即,從演算法執行的角度來說,如果某一輪迭代中鬆弛操作未執行,說明此次迭代所有的邊都沒有被鬆弛,因此可以證明:至此後,邊集中所有的邊都不需要再被鬆弛,從而可以提前結束迭代過程。
為了實現這樣的優化,我們可以用佇列來記錄鬆弛操作被成功執行的頂點。同時還需要一個向量mark[]來指示頂點是否已經存在於佇列中,以防止將頂點重複插入佇列。
vector<int> SPFA(EdgeWeightedDigraph &g)
{
vector<int> edge(g.V);
queue<int> q;
vector<int> mark(g.V, 0);
//定義並初始化dis[]
vector<double> dis(g.V, DBL_MAX);
dis.at(0) = 0.0;
int v = (*g.adj.cbegin()).first;
q.push(v);
mark.at(v) = 1;
int cnt = 0;
while (!q.empty())
{
v = q.front();
q.pop();
mark.at(v) = 0;
//鬆弛操作
for (const auto &e : g.adj.at(v))
{
int w = get<1>(e);
if (dis.at(v) + get<2>(e) < dis.at(w))
{
dis.at(w) = dis.at(v) + get<2>(e);
edge.at(w) = v;
if (mark.at(w) == 0)
{
q.push(w);
mark.at(w) = 1;
}
}
if (++cnt % g.V == 0)
{
cerr << "存在負權重環,無解\n";
vector<int> tmp;
return tmp;
}
}
}
return edge;
}
效能
SPFA演算法是Bellman-Ford演算法的改進,一般情況下其路徑長度的比較次數的數量級為O(E+V) 。但如果加權有向圖中存在負權重環,由於每次都會有邊被鬆弛,因而不可能提前終止外層迴圈。這對應了最壞情況,其時間複雜度仍舊為O(VE) 。