Dijkstra算法,Bellman-Ford算法和BFS算法解决有向图的单源最短路径问题

最短路径问题是一个很常见的问题,与其相关的算法也很多,本文总结了三种不同的算法来解决这个问题,并进行了一些对比分析。本文不像教科书那样详细介绍每种算法的具体细节,可以阅读这篇文章:http://dsqiu.iteye.com/blog/1689163,里面讲的和详细。

一.Dijkstra算法

相信说到单源最短路径问题,大家都会想到著名的Dijkstra算法。Dijkstra算法本质上是一种贪心算法,可以把所有节点分为两个集合S(已找到最短路径的集合)和T(剩余节点的集合),每步都从T中选择一个权值最小的节点加入到S中,同时对与该节点相连的并且在T中的节点进行松弛操作。正因为这种贪心的思想,导致了Dijkstra算法不适用于带有负权值的环的图,但适用于带有权值为负的边的图。

此处稍微解释一下,我们知道贪心算法的思想是每步选择最优,从而使得最终得到最优结果。但当图中存在权值为负的环时,局部最优不一定会导致最终最优。例如在下图中:

Dijkstra算法,Bellman-Ford算法和BFS算法解决有向图的单源最短路径问题_第1张图片

源节点为0,按照Dijkstra算法的规则,第一步选择节点1加入集合S,则节点0到节点1的最短路径权值为4,但如图可知最短路径为0-2-1,权值为3。故Dijkstra算法不适用于带有负权值的环的图。

代码:

#include 
#include 
#include 
#include 
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>> Adj; //图的邻接表
	vector Cost; //保存图中每个节点到起始节点的最短路径权值
	priority_queue Q;
	in >> n >> m >> s;
	Adj.assign(n, vector>());
	Cost.assign(n, -1);
	for (int i = 0; i < m; i++) //用邻接表存储图的信息
	{
		in >> u >> v >> w;
		Adj[u].push_back(pair(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 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 
#include 
#include 
#include 
using namespace std;

int main()
{
	ifstream in("data.txt");
	int n, m, s, u, v, w; //n表示图中节点个数,m表示边的条数,s表示起始节点的索引号
	vector>> Adj; //图的邻接表
	vector Cost; //保存图中每个节点到起始节点的最短路径权值
	queue> Q; //pair中第一个值为节点索引号,第二个值记录当前状态下本节点到源节点的路径长度
	in >> n >> m >> s;
	Adj.assign(n, vector>());
	Cost.assign(n, INT_MAX);
	for (int i = 0; i < m; i++) //用邻接表存储图的信息
	{
		in >> u >> v >> w;
		Adj[u].push_back(pair(v, w));
	}
	
	Q.push(pair(s, 0));
	Cost[s] = 0;
	while (!Q.empty())
	{
		pair p = Q.front();
		Q.pop();
		int i;
		for (i = 0; i < Adj[p.first].size(); i++) //遍历与节点p相连的节点,同时做松弛操作
		{
			pair 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(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 
#include 
#include 
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 Graph; //保存图的边信息
	in >> n >> m >> s;
	Graph.assign(m, Edge());
	vector 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;
}




你可能感兴趣的:(算法)