Bellman-Ford算法
------------------siwuxie095
Bellman-Ford 算法
这里介绍 Bellman-Ford 算法,和 Dijkstra 算法一样,
它也是一个单源最短路径算法
Bellman-Ford 算法解决了 Dijkstra 算法没有解决的问
题:负权边问题,即 Bellman-Ford 算法中可以引入负
权边
看如下实例:
顶点 1 到顶点 2 的负权边的存在,使得虽然当前从顶点 0 到
顶点 1 的权值远远的高于从顶点 0 到顶点 2 的权值,但在绕
道的过程中,负权边让大部分权值都抵消了,反而低于从顶点
0 到顶点 2 的权值
即 0 -> 1 -> 2 的路径比 0 -> 2 的路径更短,如下:
不难看出,表面是在处理负权边,但本质上仍然是一次
松弛操作
换言之,虽然负权边使得 Dijkstra 算法失效了,但依然
要依赖松弛操作
Bellman-Ford 算法虽然解决了负权边问题,但它也有
一定的局限性
看如下实例:
多出一条从顶点 2 到顶点 0 的负权边,就形成了一个负权环,
0 -> 1 -> 2 -> 0 这条路径的总权值为 -2
当一个图中出现了负权环,那么从一点到任何一点只要能经
过该负权环,权值就会更小,而想要找到所谓的最短路径,
就一定要不停地在该负权环中转。因为每转一圈,得到的总
权值就更小
这样一来,相当于图中就不存在最短路径了,或 最短路径的
结果是负无穷
所以,在处理带有负权边的图时,如果图中拥有负权环,则
该图就不再拥有最短路径
「拥有负权环的图,没有最短路径」
注意:不要认为负权环一定至少由三个顶点组成,事实上,
两个顶点之间也可以形成负权环,如下图所示
综上,Bellman-Ford 算法解决的就是图中可以有负权边,
但不能有负权环的单源最短路径问题
「前提:图中不能有负权环」
不过 Bellman-Ford 算法比想象中更加出色,它不一定要
遵守该前提。如果图中有负权环,Bellman-Ford 算法经
过运行之后,虽然找不到最短路径,但是可以判断出图中
有负权环
「Bellman-Ford 算法可以判断图中是否有负权环」
Bellman-Ford 算法如此神奇,相应的代价也是高昂的,
它的时间复杂度:O(E*V)
Bellman-Ford 算法的基本思想:
如果一个图中没有负权环,从一点到另外一点的最短路径,
最多经过所有 V 个顶点,有 V-1 条边,否则,存在顶点被
经过了两次,即 存在负权环
看如下实例:
左边是一张连通带权有向图,右边是起始顶点 0 到各个顶点的
当前最短距离的列表,起始顶点 0 到自身的距离是 0
将顶点 0 进行标识,并作为当前顶点。对当前顶点 0 的所有相
邻顶点依次进行一次松弛操作,同时更新列表
然后将当前顶点 0 的所有相邻顶点依次当做新的当前顶点,并
对新的当前顶点的所有相邻顶点依次进行一次松弛操作,同时
更新列表
… …
对当前顶点进行一次松弛操作,就是找到了经过当前顶点的另
外一条路径,多一条边,权值更小
如果一个图中没有负权环,从一点到另外一点的最短路径,最
多经过所有 V 个顶点,有 V-1 条边
对所有顶点进行 V-1 次松弛操作,理论上就找到了从起始顶点
到其它所有顶点的最短路径
然后再尝试对所有顶点进行第 V 次松弛操作, 如果还可以继续
松弛,就说明图中一定存在负权环
注意:Bellman-Ford 算法主要针对有向图,因为如果是无向图,
一旦图中存在负权边,就相当于存在负权环,而如果图中没有负
权边,就可以直接使用 Dijkstra 算法,效率更高
程序:
Edge.h:
#ifndef EDGE_H #define EDGE_H
#include <iostream> #include <cassert> using namespace std;
//边信息:两个顶点和权值 template<typename Weight> class Edge {
private:
int a, b; //边的两个顶点a和b(如果是有向图,就默认从顶点a指向顶点b) Weight weight; //边上的权值
public:
Edge(int a, int b, Weight weight) { this->a = a; this->b = b; this->weight = weight; }
//默认构造函数 Edge(){}
~Edge(){}
int v(){ return a; }
int w(){ return b; }
Weight wt() { return weight; }
//知道边的一个顶点x,返回另一个顶点 int other(int x) { assert(x == a || x == b); return x == a ? b : a; }
//友元函数重载 friend ostream &operator<<(ostream &os, const Edge &e) { os << e.a << "-" << e.b << ": " << e.weight; return os; }
bool operator<(Edge<Weight> &e) { return weight < e.wt(); }
bool operator<=(Edge<Weight> &e) { return weight <= e.wt(); }
bool operator>(Edge<Weight> &e) { return weight > e.wt(); }
bool operator>=(Edge<Weight> &e) { return weight >= e.wt(); }
bool operator==(Edge<Weight> &e) { return weight == e.wt(); } };
#endif |
SparseGraph.h:
#ifndef SPARSEGRAPH_H #define SPARSEGRAPH_H
#include "Edge.h" #include <iostream> #include <vector> #include <cassert> using namespace std;
// 稀疏图 - 邻接表 template<typename Weight> class SparseGraph {
private:
int n, m; //n 和 m 分别表示顶点数和边数 bool directed; //directed表示是有向图还是无向图 vector<vector<Edge<Weight> *>> g; //g[i]里存储的就是和顶点i相邻的所有边指针
public:
SparseGraph(int n, bool directed) { this->n = n; this->m = 0; this->directed = directed; //g[i]初始化为空的vector for (int i = 0; i < n; i++) { g.push_back(vector<Edge<Weight> *>()); } }
~SparseGraph() {
for (int i = 0; i < n; i++) { for (int j = 0; j < g[i].size(); j++) { delete g[i][j]; } } }
int V(){ return n; } int E(){ return m; }
void addEdge(int v, int w, Weight weight) { assert(v >= 0 && v < n); assert(w >= 0 && w < n);
g[v].push_back(new Edge<Weight>(v, w, weight)); //(1)顶点v不等于顶点w,即不是自环边 //(2)且不是有向图,即是无向图 if (v != w && !directed) { g[w].push_back(new Edge<Weight>(w, v, weight)); }
m++; }
//hasEdge()判断顶点v和顶点w之间是否有边 //hasEdge()的时间复杂度:O(n) bool hasEdge(int v, int w) { assert(v >= 0 && v < n); assert(w >= 0 && w < n);
for (int i = 0; i < g[v].size(); i++) { if (g[v][i]->other(v) == w) { return true; } }
return false; }
void show() {
for (int i = 0; i < n; i++) { cout << "vertex " << i << ":\t"; for (int j = 0; j < g[i].size(); j++) { cout << "{to:" << g[i][j]->w() << ",wt:" << g[i][j]->wt() << "}\t"; } cout << endl; } }
//邻边迭代器(相邻,即 adjacent) // //使用迭代器可以隐藏迭代的过程,按照一定的 //顺序访问一个容器中的所有元素 class adjIterator { private:
SparseGraph &G; //图的引用,即要迭代的图 int v; //顶点v int index; //相邻顶点的索引
public:
adjIterator(SparseGraph &graph, int v) : G(graph) { this->v = v; this->index = 0; }
//要迭代的第一个元素 Edge<Weight> *begin() { //因为有可能多次调用begin(), //所以显式的将index设置为0 index = 0; //如果g[v]的size()不为0 if (G.g[v].size()) { return G.g[v][index]; }
return NULL; }
//要迭代的下一个元素 Edge<Weight> *next() { index++; if (index < G.g[v].size()) { return G.g[v][index]; }
return NULL; }
//判断迭代是否终止 bool end() { return index >= G.g[v].size(); } }; };
#endif |
DenseGraph.h:
#ifndef DENSEGRAPH_H #define DENSEGRAPH_H
#include "Edge.h" #include <iostream> #include <vector> #include <cassert> using namespace std;
// 稠密图 - 邻接矩阵 template<typename Weight> class DenseGraph {
private:
int n, m; //n 和 m 分别表示顶点数和边数 bool directed; //directed表示是有向图还是无向图 vector<vector<Edge<Weight> *>> g; //二维矩阵,存储边指针
public:
DenseGraph(int n, bool directed) { this->n = n; this->m = 0; this->directed = directed; //二维矩阵:n行n列,全部初始化为NULL for (int i = 0; i < n; i++) { g.push_back(vector<Edge<Weight> *>(n, NULL)); } }
~DenseGraph() { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (g[i][j] != NULL) { delete g[i][j]; } } } }
int V(){ return n; } int E(){ return m; }
//在顶点v和顶点w之间建立一条边 void addEdge(int v, int w, Weight weight) { assert(v >= 0 && v < n); assert(w >= 0 && w < n);
//如果顶点v和顶点w之间已经存在一条边,就删掉, //之后按照传入权值重建一条边,即直接覆盖 if (hasEdge(v, w)) { delete g[v][w];
//如果是无向图,还要删除和主对角线对称的值 if (!directed) { delete g[w][v]; }
m--; }
g[v][w] = new Edge<Weight>(v, w, weight);
//如果是无向图,还要在和主对角线对称处添加值 if (!directed) { g[w][v] = new Edge<Weight>(w, v, weight); }
m++; }
//hasEdge()判断顶点v和顶点w之间是否有边 //hasEdge()的时间复杂度:O(1) bool hasEdge(int v, int w) { assert(v >= 0 && v < n); assert(w >= 0 && w < n); return g[v][w] != NULL; }
void show() {
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (g[i][j]) { cout << g[i][j]->wt() << "\t"; } else { cout << "NULL\t"; } } cout << endl; } }
//邻边迭代器(相邻,即 adjacent) class adjIterator { private:
DenseGraph &G; //图引用,即要迭代的图 int v; //顶点v int index; //相邻顶点的索引
public:
adjIterator(DenseGraph &graph, int v) : G(graph) { this->v = v; this->index = -1; }
//要迭代的第一个元素 Edge<Weight> *begin() { //找第一个权值不为NULL的元素,即为要迭代的第一个元素 index = -1; return next(); }
//要迭代的下一个元素 Edge<Weight> *next() { for (index += 1; index < G.V(); index++) { if (G.g[v][index]) { return index; } }
return NULL; }
//判断迭代是否终止 bool end() { return index >= G.V(); } }; };
#endif |
ReadGraph.h:
#ifndef READGRAPH_H #define READGRAPH_H
#include <iostream> #include <string> #include <fstream> #include <sstream> #include <cassert> using namespace std;
//从文件中读取图的测试用例 template <typename Graph, typename Weight> class ReadGraph {
public: ReadGraph(Graph &graph, const string &filename) {
ifstream file(filename); string line; //一行一行的读取 int V, E;
assert(file.is_open());
//读取file中的第一行到line中 assert(getline(file, line)); //将字符串line放在stringstream中 stringstream ss(line); //通过stringstream解析出整型变量:顶点数和边数 ss >> V >> E;
//确保文件里的顶点数和图的构造函数中传入的顶点数一致 assert(V == graph.V());
//读取file中的其它行 for (int i = 0; i < E; i++) {
assert(getline(file, line)); stringstream ss(line);
int a, b; Weight w; ss >> a >> b >> w; assert(a >= 0 && a < V); assert(b >= 0 && b < V); graph.addEdge(a, b, w); } } };
#endif |
BellmanFord.h:
#ifndef BELLMANFORD_H #define BELLMANFORD_H
#include "Edge.h" #include <stack> #include <vector> using namespace std;
//Bellman-Ford 算法实现最短路径 template <typename Graph, typename Weight> class BellmanFord {
private:
Graph &G; //图的引用,即要进行操作的图 int s; //起始顶点 s,s 即 source Weight* distTo; //起始顶点 s 到每个顶点的当前最短距离 vector<Edge<Weight>*> from; //经由哪条边到达了当前顶点 bool hasNegativeCycle; //该图是否有负权环
bool detectNegativeCycle() { //对所有顶点再进行一次松弛操作(第 V 次) for (int i = 0; i < G.V(); i++) { typename Graph::adjIterator adj(G, i); for (Edge<Weight>* e = adj.begin(); !adj.end(); e = adj.next()) { //如果发现还有边没访问过,或还能进行松弛的话,说明一定有负权环 if (!from[e->w()] || distTo[e->v()] + e->wt() < distTo[e->w()]) { return true; } } }
return false; }
public:
BellmanFord(Graph &graph, int s) :G(graph) {
this->s = s; distTo = new Weight[G.V()]; for (int i = 0; i < G.V(); i++) { from.push_back(NULL); }
// Bellman-Ford // //对起始顶点 s 到自身的最短距离进行初始化, //由于不知道 distTo 数组中元素的具体类型, //所以使用模板类型Weight的默认构造函数, //如果指定的模板为 int,会被初始化为 0 distTo[s] = Weight();
//所有顶点都进行 V-1 次松弛操作 for (int pass = 1; pass < G.V(); pass++) {
//所有顶点 for (int i = 0; i < G.V(); i++) { //注意:声明迭代器时,前面还要加 typename,表明 //adjIterator 是 Graph 中的类型,而不是成员变量 typename Graph::adjIterator adj(G, i); //对当前顶点 i 的所有相邻顶点依次进行一次松弛操作 for (Edge<Weight> *e = adj.begin(); !adj.end(); e = adj.next()) { //(1)如果还没有边到达相邻顶点 w //(2)或:"经过"当前顶点 i 到相邻顶点 w 所得到的 //路径小于"不经过"当前顶点 i 到相邻顶点 w 所得到 //的路径,就进行一次松弛操作 if (!from[e->w()] || distTo[e->v()] + e->wt() < distTo[e->w()]) { distTo[e->w()] = distTo[e->v()] + e->wt(); from[e->w()] = e; } } } }
//对所有顶点进行第 V 次松弛操作,判断是否存在负权环 hasNegativeCycle = detectNegativeCycle(); }
~BellmanFord() {
delete []distTo; }
//判断图中是否存在负权环 bool negativeCycle() { return hasNegativeCycle; }
//顶点 s 到顶点 w 的最短距离 Weight shortestPathTo(int w) { assert(w >= 0 && w < G.V()); assert(!hasNegativeCycle); return distTo[w]; }
//判断顶点 s 到顶点 w 是否有路径 bool hasPathTo(int w) { assert(w >= 0 && w < G.V()); return from[w] != NULL; }
//找到从顶点 s 到顶点 w 的最短路径的边的组成:通过from数组 //从顶点 w 倒推回去,并存储在栈中,最后再从栈中转存到向量中 void shortestPath(int w, vector<Edge<Weight>> &vec) {
assert(w >= 0 && w < G.V()); assert(!hasNegativeCycle);
stack<Edge<Weight>*> s;
Edge<Weight> *e = from[w];
//直到倒推到起始顶点,对于有向图 //来说,e->v() 即一条边的起点 while (e->v() != this->s) { s.push(e); e = from[e->v()]; } s.push(e);
//只要栈不为空,就将栈顶元素放入 //向量中,并出栈 while (!s.empty()) { e = s.top(); vec.push_back(*e); s.pop(); } }
//打印从顶点 s 到顶点 w 的最短路径 void showPath(int w) {
assert(w >= 0 && w < G.V()); assert(!hasNegativeCycle);
vector<Edge<Weight>> vec; shortestPath(w, vec); for (int i = 0; i < vec.size(); i++) { cout << vec[i].v() << " -> "; if (i == vec.size() - 1) cout << vec[i].w() << endl; } } };
#endif |
main.cpp:
#include "SparseGraph.h" #include "DenseGraph.h" #include "ReadGraph.h" #include "BellmanFord.h" #include <iostream> using namespace std;
int main() {
string filename = "testG2.txt"; int V = 5;
//稀疏图 SparseGraph<int> g = SparseGraph<int>(V, true); ReadGraph<SparseGraph<int>, int> readGraph(g, filename);
cout << "Test Bellman-Ford:" << endl << endl; BellmanFord<SparseGraph<int>, int> bellmanFord(g, 0); if (bellmanFord.negativeCycle()) { cout << "The graph contain negative cycle!" << endl; } else { for (int i = 1; i < V; i++) { cout << "Shortest Path to " << i << " : " << bellmanFord.shortestPathTo(i) << endl;
bellmanFord.showPath(i);
cout << "----------" << endl; } }
system("pause"); return 0; }
//对所有顶点都要进行 V 次松弛操作,且对每一条边 //都要遍历一遍,共 E 条边,所以最终 Bellman-Ford //算法的时间复杂度是 O(V*E)这个级别的 |
运行一览:
其中,testG2.txt 的内容如下:
该文件可以分成两个部分:
(1)第一行:两个数字分别代表顶点数和边数
(2)其它行:每一行的前两个数字表示一条边,第三个数字表示权值
【made by siwuxie095】
posted on 2017-07-08 03:18 siwuxie095 阅读(1871) 评论(0) 编辑 收藏 举报