先備知識與注意事項
本篇文章將介紹Bellman-Ford Algorithm來回應上一篇Single-Source Shortest Path:Intro(簡介)的問題,演算法的概念主要圍繞在:
- Relaxation
- Convergence property
- Path-relaxation property
建議讀者可以先進傳送門精彩回顧一下。
目錄
Graph之表示法(representation)
在BFS/DFS系列文章使用Adjacency List代表Graph,不過edge上沒有weight,可以使用如下資料結構:
std::vector<std::list<int>> AdjList;
MST系列文章的Graph利用Adjacency Matrix,因此可以使用如下資料結構,把weight放進矩陣之元素:
std::vector<std::vector<int>> AdjMatrix;
本篇文章將利用Adjacency List來代表weighted edge,方法是使用C++標準函式庫(STL)的std::pair<int,int>
,使得AdjList
的資料結構更新為:
std::vector<std::list<std::pair<int,int>>> AdjList;
若有一條從vertex(0)(from
)指向vertex(1)(to
)的edge(0,1)之weight為\(5\),若利用新的AdjList
表示,如圖一:
- 將
from
以std::vector
的index表示; - 每一個
std::pair<int,int>
都是一個std::list
的node; - 將
to
放在std::pair<int,int>
的第一個int
資料項; - 將weight放在
std::pair<int,int>
的第二個int
資料項,
關於std::pair<int,int>
的用法,可以參考:Cplusplus:std::pair。
圖一。
Bellman-Ford Algorithm
根據Path-Relaxation Property,考慮一條從vertex(0)到vertex(K)之路徑\(P:v_0-v_1-...-v_K\),如果在對path之edge進行Relax()
的順序中,曾經出現edge(v0,v1)、edge(v1,v2)、...、edge(vK-1,vK)的順序,那麼這條path一定是最短路徑,滿足distance[K]
\(=\delta(v_0,v_K)\)。
- 在對edge(v1,v2)進行
Relax()
之前,只要已經對edge(v0,v1)進行過Relax()
,那麼,不管還有其餘哪一條edge已經進行過Relax()
,distance[2]
必定會等於\(\delta(0,2)\),因為Convergence property。
Bellman-Ford Algorihm就用最直覺的方式滿足Path-Relaxation Property:
- 一共執行\(|V|-1\)次迴圈。
- 在每一次迴圈裡,對「所有的edge」進行
Relax()
。 - 經過\(|V|-1\)次的「所有edge之
Relax()
」後,必定能夠產生「按照最短路徑上之edge順序的Relax()
順序」,也就能夠得到最短路徑。- 由於從起點走到任一vertex之最短路徑,最多只會有\(|V|-1\)條edge,因此,執行\(|V|-1\)次迴圈必定能夠滿足「最壞情況」(worst case)。
考慮如圖二(a)之Graph,以vertex(0)作為起點。
並且根據圖二(a)之Adjacency List,得到在Bellman-Ford Algorihm中對所有edge進行Relax()
之順序如圖二(b)。
圖二(a)。
圖二(b):根據圖二(a)之AdjList
,對所有edge進行Relax()
之順序。
首先,對Graph的資料項目distance
與predecessor
進行初始化,見圖二(c):
distance
:將起點vertex之distance
設為\(0\),並將其餘vertex之distance
設為無限大(\(\infty\))。- 如果在演算法結束後,某個vertex之
distance
仍然無限大(\(\infty\)),則表示Graph中沒有一條path能夠從起點vertex走到該vertex。 - 回顧
Relax()
,因為只有起點vertex(0)之distance
設為\(0\),其餘distance
都是無限大,因此,在進行「所有edge之Relax()
」時,一定是從與起點vertex(0)相連之edge先開始。
- 如果在演算法結束後,某個vertex之
predecessor
:將所有vertex之predecessor
設為\(-1\),表示目前還沒有predecessor
。對其餘vertex來說,可以想成還沒有被起點vertex找到。
圖二(c)。
接著開始\(|V|-1\)次的迴圈,並在每次迴圈,都對「所有edge」進行Relax()
。
(此例,共\(5\)次迴圈,每次按照圖二(b)之順序處理\(10\)條edge)。
第一次迴圈(iteration#1)
對第一條edge(0,1)進行Relax()
,見圖三(a):
- 比較
distance[1]
與distance[0]+w(0,1)
,發現」從vertex(0)走到vertex(1)」之成本較原先的成本低(原先的成本即為distance[1]
),因此更新distance[1]
為distance[0]+w(0,1)
。 - 同時更新
predecessor[1]=0
,表示vertex(1)是從vertex(0)走過去的。
圖三(a)。
對第二、三條edge(1,2)、edge(1,4)進行Relax()
,見圖三(b):
- 由於
distance[2]
與distance[4]
皆大於從vertex(1)走過去的成本,因此更新:distance[2]=distance[1]+w(1,2)
;distance[4]=distance[1]+w(1,4)
;
圖三(b)。
對第四、五條edge(2,4)、edge(2,5)進行Relax()
,見圖三(c):
- 由於
distance[4]
小於「從vertex(2)走過去的成本(distance[2]+w(2,4)
),因此,仍維持從vertex(1)走到vertex(4)之路徑。 - 而
distance[5]
大於distance[2]+w(2,5)
,因此更新:distance[5]=distance[2]+w(2,5)
;predecessor[5]=2
;
圖三(c)。
對第六條edge(3,2)進行Relax()
,見圖三(d):
- 顯然,「從vertex(1)走到vertex(2)」之成本
distance[2]
遠小於「從vertex(3)走到vertex(2)」之成本distance[3]+w(2,3)
,因此,不更新抵達vertex(2)之路徑。
圖三(d)。
對第七、八條edge(4,3)、edge(4,5)進行Relax()
,見圖三(e):
- 由於
distance[3]
大於從vertex(4)走到vertex(3)的成本(distance[4]+w(4,3)
),因此更新:distance[3]=distance[4]+w(4,3)
;predecessor[3]=4
;
- 而且
distance[5]
也大於distance[4]+w(4,5)
,表示「從vertex(4)走到vertex(5)」會比「從vertex(2)走到vertex(5)」還近,便更新:distance[5]=distance[4]+w(4,5)
;predecessor[5]=4
;
圖三(e)。
對第九、十條edge(5,0)、edge(5,1)進行Relax()
,見圖三(f):
- 比較
distance
及edge之weight後發現,從vertex(5)走到vertex(0)與vertex(1)之成本皆高於當前走到此二vertex之路徑,因此不更新。
圖三(f)。
經過了第一次迴圈的洗禮,大致能夠掌握Relax()
的精髓:如果有比目前的成本更低的路徑,就選擇那條路徑。
例如,圖三(c)中,原本從vertex(0)走到vertex(5)的路徑是\(Path:0-1-2-5\),但是在圖三(e)時,發現了更短的路徑\(Path:0-1-4-5\),便更新其成為目前「從vertex(0)走到vertex(5)」的最短路徑。
還有沒有可能更新出成本更低的路徑?
當然有,因為第一次迴圈的Relax()
明顯沒有更新到和edge(3,2)有關路徑,其原因在於,Relax()
進行的edge順序。
在第一次迴圈中,是先對edge(3,2)進行Relax()
,不過此時的distance[3]
還是無限大,因此沒有作用,之後才更新出「從vertex(4)走到vertex(3)」的路徑。
就圖三(f)看來,distance[2]
為\(11\),而\(Path:0-1-4-3-2\)之成本只有\(6\),顯然是低於當前的路徑\(Path:0-1-2\)。
因此需要\(|V|-1\)次迴圈,以確保演算法一定能找到最短路徑。
第二次直到第五次迴圈的邏輯同上,在此就不再贅述。
最後得到的最短路徑之Predecessor Subgraph如圖三(g):
圖三(g)。
根據圖三(g),從vertex(0)走到Graph中其餘vertex之最短路徑為\(Path:0-1-4-3-2-5\),若把Bellman-Ford Algorithm中,每一次迴圈對所有edge進行Relax()
之順序攤開,如圖三(h),可以驗證Bellman-Ford Algorithm確實滿足Path-Relaxation Property,按照最短路徑之順序對edge進行Relax()
。
(Bellman-Ford Algorithm就是暴力解決,當然會滿足。)
圖三(h)。
程式碼
程式碼包含幾個部分:
class Graph_SP
:
- 使用
AdjList
,並利用std::pair<int,int>
儲存edge的weight。 InitializeSingleSource(int Start)
:對資料項目distance
與predecessor
進行初始化,並以int Start
作為最短路徑之起點。Relax()
:對edge進行Relaxation的主要函式。BellmanFord()
:進行Bellman-Ford Algorithm的主要函式,內容如前一小節所述。
以及main()
:建立如圖二(a)之AdjList
,並進行BellmenFord()
。
// C++ code
#include <iostream>
#include <vector>
#include <list>
#include <utility> // for std::pair<>
#include <iomanip> // for std::setw()
const int Max_Distance = 100;
class Graph_SP{ // SP serves as Shortest Path
private:
int num_vertex;
std::vector<std::list<std::pair<int,int>>> AdjList;
std::vector<int> predecessor, distance;
public:
Graph_SP():num_vertex(0){};
Graph_SP(int n):num_vertex(n){
AdjList.resize(num_vertex);
}
void AddEdge(int from, int to, int weight);
void PrintDataArray(std::vector<int> array);
void InitializeSingleSource(int Start); // 以Start作為起點
void Relax(int X, int Y, int weight); // 對edge(X,Y)進行Relax
bool BellmanFord(int Start = 0); // 以Start作為起點
// if there is negative cycle, return false
};
bool Graph_SP::BellmanFord(int Start){
InitializeSingleSource(Start);
for (int i = 0; i < num_vertex-1; i++) { // |V-1|次的iteration
// for each edge belonging to E(G)
for (int j = 0 ; j < num_vertex; j++) { // 把AdjList最外層的vector走一遍
for (std::list<std::pair<int,int> >::iterator itr = AdjList[j].begin();
itr != AdjList[j].end(); itr++) { // 各個vector中, 所有edge走一遍
Relax(j, (*itr).first, (*itr).second);
}
}
}
// check if there is negative cycle
for (int i = 0; i < num_vertex; i++) {
for (std::list<std::pair<int,int> >::iterator itr = AdjList[i].begin();
itr != AdjList[i].end(); itr++) {
if (distance[(*itr).first] > distance[i]+(*itr).second) { // i是from, *itr是to
return false;
}
}
}
// print predecessor[] & distance[]
std::cout << "predecessor[]:\n";
PrintDataArray(predecessor);
std::cout << "distance[]:\n";
PrintDataArray(distance);
return true;
}
void Graph_SP::PrintDataArray(std::vector<int> array){
for (int i = 0; i < num_vertex; i++)
std::cout << std::setw(4) << i;
std::cout << std::endl;
for (int i = 0; i < num_vertex; i++)
std::cout << std::setw(4) << array[i];
std::cout << std::endl << std::endl;
}
void Graph_SP::InitializeSingleSource(int Start){
distance.resize(num_vertex);
predecessor.resize(num_vertex);
for (int i = 0; i < num_vertex; i++) {
distance[i] = Max_Distance;
predecessor[i] = -1;
}
distance[Start] = 0;
}
void Graph_SP::Relax(int from, int to, int weight){
if (distance[to] > distance[from] + weight) {
distance[to] = distance[from] + weight;
predecessor[to] = from;
}
}
void Graph_SP::AddEdge(int from, int to, int weight){
AdjList[from].push_back(std::make_pair(to,weight));
}
int main(){
Graph_SP g7(6);
g7.AddEdge(0, 1, 5);
g7.AddEdge(1, 4, -4);g7.AddEdge(1, 2, 6);
g7.AddEdge(2, 4, -3);g7.AddEdge(2, 5, -2);
g7.AddEdge(3, 2, 4);
g7.AddEdge(4, 3, 1);g7.AddEdge(4, 5, 6);
g7.AddEdge(5, 0, 3);g7.AddEdge(5, 1, 7);
if (g7.BellmanFord(0))
std::cout << "There is no negative cycle.\n";
else
std::cout << "There is negative cycle.\n";
return 0;
}
output:
predecessor[]:
0 1 2 3 4 5
-1 0 3 4 1 2
distance[]:
0 1 2 3 4 5
0 5 6 2 1 4
There is no negative cycle.
檢查Graph中是否存在negative cycle
聰明的讀者一定已經發現了,BellmanFord()
還有個功能,可以檢查Graph中是否存在negative cycle。
上一篇文章Single-Source Shortest Path:Intro(簡介)曾經提到,塵世間最痛苦的事情莫過於在處理Single-Source Shortest Path問題時碰上negative cycle,因為negative cycle存在會使得最短路徑出現\(-\infty\)。
若存在從vertex(from)指向vertex(to)的edge(from,to),並具有weight,根據Relax()
的運作原則,:
- 若
distance[to]
\(>\)distance[from]+weight
,便更新distance[to]=distance[from]+weight
」。
因此,在對Graph中所有edge進行Relax()
後,所有edge(from,to)所連結的兩個vertex之distance
必定滿足:
distance[to]
\(\leq\)distance[from]+weight
而BellmanFord()
的檢查方法,便是在\(|V|-1\)次「對所有edge進行Relax()
」後,檢查,若存在任何一條edge(from,to)所連結的兩個vertex之distance
關係為:
distance[to]
\(>\)distance[from]+weight
就表示存在某一條edge之weight永遠可以讓該edge連結之vertex的distance
下降,因此Graph中必定存在negative cycle。
以上便是Bellman-Ford Algorithm之介紹。
只要了解:
- Relaxation
- Convergence property
- Path-relaxation property
之概念,即可掌握Bellman-Ford Algorithm的運作邏輯。
下一篇文章將介紹的是,在DAG(directed acyclic graph)中處理Single-Source Shortest Path之演算法。
參考資料:
- Introduction to Algorithms, Ch24
- Fundamentals of Data Structures in C++, Ch6
- Wikipedia:Shortest path problem
- Cplusplus:std::pair
Shortest Path系列文章
Shortest Path:Intro(簡介)
Single-Source Shortest Path:Bellman-Ford Algorithm
Single-Source Shortest Path:on DAG(directed acyclic graph)
Single-Source Shortest Path:Dijkstra's Algorithm
All-Pairs Shortest Path:Floyd-Warshall Algorithm
回到目錄: