图之最短路径问题(多源篇)

星空
这篇文章用来复习最短路径问题之多源最短路径问题.
解决这个问题的算法可以是Floyd,也是以人名命名的,他还发现了堆排序.也是图灵奖的获得者.
引用wiki的一句话描述这个算法.

In computer science, the Floyd–Warshall algorithm is an algorithm for finding shortest paths in a weighted graph with positive or negative edge weights (but with no negative cycles)

它可以解决带有负权值的图.
Floyd是一个利用动态规划的算法,在1962年被发表的.有着近60年的历史.
Floyd的神奇之处在于它可以只有五行代码实现对每对顶点的最短路径进行求解.
当然,解决这个问题也可以对每个顶点进行一次Dijkstra.如此以来也可以求得每对顶点间的最短路径.若再加上使用优先队列来配合,可以达到O(V · E · log (V)) 的时间复杂度.
但是不能求解带有负权值的图(其实可以通过其他办法如通过对负值加一个值变成正值来解决这个问题),而且代码量比较大(相比Floyd).
Floyd算法的运行时间为O(V3),不适合用于大量数据的计算.


Floyd的算法描述:

依次扫描每个顶点K,并以该顶点为中介点,计算出通过K点的其他任意两点(i,j)的最短距离.

  • 基本形式如下:
// 初始化图
let dist be a |V| × |V| array of minimum distances initialized to ∞ (infinity)
// 使用邻接矩阵存储,自己到自己距离为0.
for each vertex v
    dist[v][v] ← 0
// 插入边
 for each edge (u,v)
    dist[u][v] ← w(u,v)  // the weight of the edge (u,v)
 for k from 1 to |V|
    for i from 1 to |V|
       for j from 1 to |V|
          if dist[i][j] > dist[i][k] + dist[k][j] 
             dist[i][j] ← dist[i][k] + dist[k][j]
         end if

通过以上操作能够在原来包含图的连接基本信息的矩阵的基础上计算出每对顶点之间的最短路径

关于路径的记录和初始化:

  • 路径记录
    需要一个同样大小的矩阵.
    在需要更新dist的时候
path[i][j] = path[k][j]

以"5->6->2->1"路径为例

path[5][3] = 2
path[5][3] = 6
path[5][6] = 5
path[5][5] = -1 // 或其他不存在的顶点,可用于终止路径输出
  • 路径的初始化
    在初始化的时候将全部顶点都赋值-1,与顶点编号无关的点;
    之后在插入一条边的时候,例如加入一条 _edge,则path的操作为:
path[_edge.start][_edge.end] = _edge.start
  • 关于距离的记录
    要是在原来存储了图的基本信息的基础上进行操作的话,会破坏它原来的基本存储信息,例如两点间的权值,那就可能不是权值的,而是这对点的最短路径.故我认为新建立一个矩阵用来进行Floyd操作十个不错的主意,但是这个会使得存储量翻番.这个记录距离的矩阵要求和原矩阵相同,即在初始化和插入边的时候进行相同的操作.
// 初始化边的操作
for (int i = 0; i < vertexNum; i++)
	{
    	// 原图矩阵
		vertexPtr[i]	= new Node_s[vertexNum]();
		// 记录距离的矩阵
        distance[i]		= new int[vertexNum]();
		for (int j = 0; j < vertexNum; j++)
		{
			vertexPtr[i][j].verIndex = j;
			if (i == j)
			{
				vertexPtr[i][j].weight = 0;
				distance[i][j] = 0;
			}
			else
			{
				vertexPtr[i][j].weight = MAXDIS;
				distance[i][j] = MAXDIS;
			}
		}
	}
  
  // 插入边的操作
 int CGraph::addEdge(edge_s _edge){
    vertexPtr[_edge.start][_edge.end].weight= _edge.weight;
    distance[_edge.start][_edge.end]		= _edge.weight;
    path[_edge.start][_edge.end] = _edge.start;
    edgeNum ++;
    return edgeNum;
  }
  • Floyd的函数如下:
void CGraph::floyd(){
	for (int k = 0; k < vertexNum; k++)
	{
		for (int i = 0; i < vertexNum; i++)
		{
			for (int j = 0; j < vertexNum; j++)
			{
				if (distance[i][k] + distance[k][j] < distance[i][j])
				{
					distance[i][j] = distance[i][k] + distance[k][j];
					path[i][j] = path[k][j];
				}
			}
		}
	}
}

关于路径的输出:

根据上面的案例可以有如下的实现:

void CGraph::showShortPath(int _startVertex,int _endVertex){
	// 使用双端队列模拟堆栈
	std::deque <int>Qvertex;
	Qvertex.push_back(_endVertex);
	// 不断回退到没有上一个节点的节点,判断标志为回退的结果是否为-1
	int endVertex = _endVertex;
	while (path[_startVertex][endVertex] != -1)
	{
		Qvertex.push_back(path[_startVertex][endVertex]);
		endVertex = path[_startVertex][endVertex];
	}
	// 模拟栈输出
	while (!Qvertex.empty())
	{
		std::cout <<Qvertex.back() <<" ";
		Qvertex.pop_back();
	}
	// 再输出总距离结束
	std::cout <<"("<<distance[_startVertex][_endVertex]<<")"<< std::endl;
}

实现中可能会用到的变量:

	// 记录距离的数组
	int ** distance;
	// 记录路径的数组
	int ** path;

总结:

  • 了解了动态规划在最短路问题中的应用.
  • Floyd不能解决带有负圈的图,但是能检测带有负圈的图,可以检查distance的对角线上的元素,若出现了负数则说明至少有一个负圈.

代码中接口部分和前面复习的图的遍历基本保持一致.
详细代码见我的github

原文地址:https://www.cnblogs.com/leihui/p/6036630.html