最短路径问题是一个很常见的问题,与其相关的算法也很多,本文总结了三种不同的算法来解决这个问题,并进行了一些对比分析。本文不像教科书那样详细介绍每种算法的具体细节,可以阅读这篇文章:http://dsqiu.iteye.com/blog/1689163,里面讲的和详细。
一.Dijkstra算法
相信说到单源最短路径问题,大家都会想到著名的Dijkstra算法。Dijkstra算法本质上是一种贪心算法,可以把所有节点分为两个集合S(已找到最短路径的集合)和T(剩余节点的集合),每步都从T中选择一个权值最小的节点加入到S中,同时对与该节点相连的并且在T中的节点进行松弛操作。正因为这种贪心的思想,导致了Dijkstra算法不适用于带有负权值的环的图,但适用于带有权值为负的边的图。
此处稍微解释一下,我们知道贪心算法的思想是每步选择最优,从而使得最终得到最优结果。但当图中存在权值为负的环时,局部最优不一定会导致最终最优。例如在下图中:
源节点为0,按照Dijkstra算法的规则,第一步选择节点1加入集合S,则节点0到节点1的最短路径权值为4,但如图可知最短路径为0-2-1,权值为3。故Dijkstra算法不适用于带有负权值的环的图。
代码:
#include <iostream>
#include <vector>
#include <queue>
#include <fstream>
using namespace std;
struct State
{
int index, weight;
State(int i, int w) :index(i), weight(w){}
bool operator<(const State& S) const
{
return weight > S.weight;
}
};
int main()
{
ifstream in("data.txt");
int n, m, s, u, v, w; //n表示图中节点个数,m表示边的条数,s表示起始节点的索引号
vector<vector<pair<int, int>>> Adj; //图的邻接表
vector<int> Cost; //保存图中每个节点到起始节点的最短路径权值
priority_queue<State> Q;
in >> n >> m >> s;
Adj.assign(n, vector<pair<int, int>>());
Cost.assign(n, -1);
for (int i = 0; i < m; i++) //用邻接表存储图的信息
{
in >> u >> v >> w;
Adj[u].push_back(pair<int, int>(v, w));
}
Q.push(State(s, 0));
while (!Q.empty())
{
State s = Q.top();
Q.pop();
if (Cost[s.index] != -1) continue;
Cost[s.index] = s.weight;
for (int i = 0; i < Adj[s.index].size(); i++)
{
pair<int, int> p = Adj[s.index][i];
if (Cost[p.first] != -1) continue;
Q.push(State(p.first, s.weight + p.second));
}
}
cout << "Result:\n";
for (int j = 0; j < n; j++)
{
if (j == s) continue;
cout << "From " << s << " to " << j << ": ";
if (Cost[j] == -1) cout << "No Path\n";
else cout << Cost[j] << endl;
}
system("pause");
return 0;
}
二.BFS算法 BFS算法也是一种很经典的算法,它有很多种变体,BFS算法也可以用来解决有向图的单源最短路径问题,思路如下:用一个状态数组保存各个节点到源节点的路径权值,在广度遍历图的过程中对该数组进行松弛操作。BFS算法也适用于存在权值为负的边的图,当图中存在权值为负的环时,该算法会在环上进行无限循环,所以在遍历过程中同时保存节点到源节点路径长度,我们知道n个节点的图中,最短路径的长度不超过n-1,当路径长度超过n-1时结束循环,就可以避免上述问题。 代码:
#include <iostream>
#include <vector>
#include <queue>
#include <fstream>
using namespace std;
int main()
{
ifstream in("data.txt");
int n, m, s, u, v, w; //n表示图中节点个数,m表示边的条数,s表示起始节点的索引号
vector<vector<pair<int, int>>> Adj; //图的邻接表
vector<int> Cost; //保存图中每个节点到起始节点的最短路径权值
queue<pair<int,int>> Q; //pair中第一个值为节点索引号,第二个值记录当前状态下本节点到源节点的路径长度
in >> n >> m >> s;
Adj.assign(n, vector<pair<int, int>>());
Cost.assign(n, INT_MAX);
for (int i = 0; i < m; i++) //用邻接表存储图的信息
{
in >> u >> v >> w;
Adj[u].push_back(pair<int, int>(v, w));
}
Q.push(pair<int, int>(s, 0));
Cost[s] = 0;
while (!Q.empty())
{
pair<int, int> p = Q.front();
Q.pop();
int i;
for (i = 0; i < Adj[p.first].size(); i++) //遍历与节点p相连的节点,同时做松弛操作
{
pair<int, int> q=Adj[p.first][i];
if (Cost[p.first] + q.second < Cost[q.first])
{
Cost[q.first] = Cost[p.first] + q.second;
Q.push(pair<int, int>(q.first, p.second + 1));
if (p.second + 1 == n) break; //此处防止当图中存在负权值的环时,在环上无限循环
}
}
if (i < Adj[p.first].size()) break;
}
cout << "Result:\n";
if (!Q.empty()) { cout << "图中存在权值为负的环!\n"; return 0; }
for (int j = 0; j < n; j++)
{
if (j == s) continue;
cout << "From " << s << " to " << j << ": ";
if (Cost[j] == INT_MAX) cout << "No Path\n";
else cout << Cost[j] << endl;
}
system("pause");
return 0;
}
三.Bellman-Ford算法 这种算法采用了动态规划的思想:按照最短路径的长度可以将最短路径问题分解为n-1步(最短路径的长度至多为n-1),状态转移方程为:dp[v][k]=max(dp[v][k-1],dp[u][k-1]+w(u,v)),(u,v)为图中的边,对某一v,取尽所有的u,其中dp[v][k]表示以节点v到源节点长度为k的最短路径。动态规划是一种很好的思想,但将其灵活运用去很难,往往即使写出了状态转移方程,其实现也需要一定的技巧。这种方法可以适用于存在权值为负的图中,若图中存在权值为负的环,得出的最终结果中一定存在某些边违反三角不等式(即dp[u]+w(u,v)>=dp[v]),检测各个边就可以发现这种情况。上述状态转移方程经过优化,实现代码如下:
#include <iostream>
#include <vector>
#include <fstream>
using namespace std;
struct Edge
{
int s, e, weight;
//Edge(int u, int v, int w) :s(u), e(v), weight(w){}
};
int main()
{
ifstream in("data.txt");
int n, m, s, u, v, w; //n表示图中节点个数,m表示边的条数,s表示起始节点的索引号
vector<Edge> Graph; //保存图的边信息
in >> n >> m >> s;
Graph.assign(m, Edge());
vector<int> dp(n, INT_MAX); //保存每个节点到源节点的路径,经过n-1轮的优化,最终dp中保存的是最短路径
dp[s] = 0;
for (int i = 0; i < m; i++) in >> Graph[i].s >> Graph[i].e >> Graph[i].weight;
bool flag;
for (int k = 1; k < n; k++)
{
flag = false;
for (int i = 0; i < m; i++)
{
Edge E = Graph[i];
if (dp[E.s] + E.weight < dp[E.e]) //松弛操作
{
dp[E.e] = dp[E.s] + E.weight;
flag = true;
}
}
if (!flag) break; //此处做了一个小的优化:如果在k的某轮循环中,没有对dp进行任何的优化,则可以直接终止循环
}
int i;
for (i = 0; i < m; i++)
{
Edge E = Graph[i];
if (dp[E.e]>dp[E.s] + E.weight) break;
}
cout << "Result:\n";
if (i < m) { cout << "图中存在权值为负的环!\n"; getchar(); return 0; }
for (int j = 0; j < n; j++)
{
if (j == s) continue;
cout << "From " << s << " to " << j << ": ";
if (dp[j] == INT_MAX) cout << "No Path!\n";
else cout << dp[j] << endl;
}
system("pause");
return 0;
}