二:Floyd-Warshall算法
该算法适用于边权重可以为负值,但环路权重和不能为负值的图,其运行时间为Θ(V3)。
假设dkij为从结点i到结点j的所有中间结点全部取自集合{1,2,…,k}的一条最短路径权重。当k=0时,从结点i到结点j的一条不包括编号大于0的中间结点的路径将没有任何中间结点。这样的路径最多只有一条边,因此d(0)ij=wij。因此如果k=0,dkij=wij,若k>=1,则dkij=min(d(k?1)ij,d(k?1)ik+d(k?1)kj)。
另外,我们可以在计算最短路径权重的同时计算前驱矩阵pred,具体来说,我们将计算一个矩阵序列,pred(0),pred(1)...pred(n),这里pred(k)ij为从结点i到结点j的一条所有中间结点都取自集合{1,2…,k}的最短路径上的j的前驱结点。当k=0时,从i到j的一条最短路径没有中间结点,因此如果i==j或wij=∞,则pred(0)ij=NIL,否则pred(0)ij=i。当k>=1时,如果d(ijk?1)≤d(k?1)ik+d(k?1)kj,则pred(k)ij=pred(k?1)ij,否则pred(k)ij=pred(k?1)kj。
代码如下:
void floyd_warshall(const vector<vector<double>>& edge_weights,vector<vector<double>>& smallest_length,vector<vector<int>>& pred)
{
const int NIL=-1; //NIL表示不存在的顶点;
const int vertex_number=edge_weights.size();
//从顶点i到顶点j没有经过其它顶点时的smallest_length,所以此时为边的权重edge_weights;
//pred表示此时对应的最短路径
for(int i=0;i!=vertex_number;++i)
for(int j=0;j!=vertex_number;++j)
{
if(i==j||edge_weights[i][j]==DBL_MAX)
pred[i][j]=NIL;
else
pred[i][j]=i;
smallest_length[i][j]=edge_weights[i][j];
}
vector<vector<double>> L(vertex_number);
vector<vector<double>> p(vertex_number);
for(int i=0;i!=vertex_number;++i)
{
L[i].resize(vertex_number);
p[i].resize(vertex_number);
}
for(int k=0;k!=vertex_number;++k)
{
//矩阵L表示中间顶点来自集合[0..k]时的顶点i到顶点j的最短路径权重和,矩阵p表示对应的路径;
//smallest_length表示中间顶点来自集合[0..k-1]时的顶点i到顶点j的最短路径权重和,矩阵pred表示对应的路径。
for(int i=0;i!=vertex_number;++i)
for(int j=0;j!=vertex_number;++j)
if(smallest_length[i][k]!=DBL_MAX&&smallest_length[k][j]!=DBL_MAX){
if(smallest_length[i][j]<=smallest_length[i][k]+smallest_length[k][j]){
p[i][j]=pred[i][j];
L[i][j]=smallest_length[i][j];
}
else{
p[i][j]=pred[k][j];
L[i][j]=smallest_length[i][k]+smallest_length[k][j];
}
}
else{
L[i][j]=smallest_length[i][j];
p[i][j]=pred[i][j];
}
//把矩阵L和p分别赋值给smallest_length和pred,使得smallest_length此时
//表示中间顶点来自集合[0..k]时的顶点i到顶点j的最短路径权重和,矩阵pred表示对应的路径
for(int i=0;i!=vertex_number;++i)
for(int j=0;j!=vertex_number;++j)
{
smallest_length[i][j]=L[i][j];
pred[i][j]=p[i][j];
}
}
}
有向图的传递闭包
给定有向图G=(V,E),结点集合V={1,2,..n},我们希望判断对于所有的结点对i和j,图G是否存在一条从结点i到结点j的路径。一种时间复杂度为Θ(n3)的计算图G的传递闭包的办法是给E的每条边赋予权重1,然后运行floyd-warshall算法。如果存在一条从结点i到结点j的路径,则有dij<n,否则dij=∞。
还有另一种类似的方法,其时间复杂度也是Θ(n3),但在实际场景中能够节省时间和空间。对于i,j,k=1,2,…n,我们定义:如果图G中存在一条从结点i到结点j的所有中间结点都取自集合{1,2,…,k}的路径,则t(k)ij=1,否则,t(k)ij=0,因此如果从i到j中存在着一条路径,则t(n)ij=1。类似的递归公式为,当k=0时,如果i==j或者wij≠∞,则t(0)ij=1,否则t0ij=0,对于k>=1,t(k)ij=t(k?1)ij||t(k?1)ik&&t(k?1)kj。
代码如下:
vector<vector<double>> transitive_closure(const vector<list<int>>& graph,
const vector<vector<double>>& edge_weights)
{
const int vertex_number=graph.size();
vector<vector<double>> t(vertex_number);
vector<vector<double>> tmp(vertex_number);
for(int i=0;i!=t.size();++i)
{
t[i].resize(vertex_number);
tmp[i].resize(vertex_number);
}
for(int i=0;i!=vertex_number;++i)
for(int j=0;j!=vertex_number;++j)
if(i==j||edge_weights[i][j]!=DBL_MAX)
t[i][j]=1;
else
t[i][j]=0;
for(int k=0;k!=vertex_number;++k)
{
for(int i=0;i!=vertex_number;++i)
for(int j=0;j!=vertex_number;++j)
tmp[i][j]=t[i][j]||(t[i][k]&&t[k][j]);
for(int i=0;i!=vertex_number;++i)
for(int j=0;j!=vertex_number;++j)
t[i][j]=tmp[i][j];
}
return t;
}
三:用于稀疏图的Johnson算法
Johnson算法要么返回一个包含所有结点对的最短路径权重的矩阵,要么报告输入图包含一个权重为负值的环路。
Johnson算法使用的技术成为重新赋予权重。该技术的工作原理如下:如果图G=(V,E)中所有边权重w皆为非负值,我们可以对每个结点运行一次Dijkstra算法来找到所有结点对之间的最短路径;如果图G包含权重为负值的边,但没有权重为负值的环路,那么只要计算出一组新的非负权重值,然后使用同样的方法即可。新赋予的权重必须满足下面两个重要性质:
1. 对于所有节点对(u,v),一条路径p是在使用原先权重函数时从结点u到结点v的一条最短路径,当且仅当p是在使用新权重函数时从u到v的一条最短路径;
2. 对于所有的边(u,v),新权重为非负值。
具体的操作过程见25.3节。代码如下:
//在已有图(假设有k个顶点)的基础上增加一个新的顶点,假设该新顶点的标号为k+1;
void add_source(vector<list<int>>& graph,vector<vector<double>>& edge_weights,vector<edge>& graph_edge)
{
//链表adjacent_vertex中存储的是与新顶点相邻的顶点,新顶点与原先顶点全部相邻,但原先顶点不与新顶点相邻;
const int new_vertex_label=graph.size();
list<int> adjacent_vertex;
for(int i=0;i!=graph.size();++i){
adjacent_vertex.push_back(i);
graph_edge.push_back(edge(new_vertex_label,i));
}
graph.push_back(adjacent_vertex);
for(int i=0;i!=edge_weights.size();++i)
edge_weights[i].push_back(DBL_MAX); //原先图中顶点与新顶点不相邻;
//新增加的顶点与相邻顶点的边的权重值为0,
//这时候graph.size()已经包括了新增的顶点;
vector<double> new_edges_weights(graph.size(),0);
edge_weights.push_back(new_edges_weights);
}
void Johnson(vector<list<int>>& graph,vector<vector<double>>& edge_weights,vector<edge>& graph_edge,
vector<vector<double>>& smallest_length,vector<vector<int>>& pred)
{
add_source(graph,edge_weights,graph_edge);
const int new_vertex_label=graph.size()-1;
//矢量smallest_weights存储的的是图中顶点到新增加顶点的最短路径权重,
//矢量p存储的是对应的各个顶点的前驱结点;
vector<double> smallest_weights(graph.size());
vector<int> p(graph.size());
if(bellman_ford(new_vertex_label,graph,edge_weights,graph_edge,smallest_weights,p)==false){
cout<<"The input graph contains a negative-weight cycle"<<endl;
return;
}
vector<double> h;
for(int i=0;i!=smallest_weights.size();++i)
h.push_back(smallest_weights[i]);
for(int u=0;u!=edge_weights.size();++u)
for(int v=0;v!=edge_weights[u].size();++v)
if(edge_weights[u][v]!=DBL_MAX)
edge_weights[u][v]+=h[u]-h[v]; //确保图中各个边的权重大于0,以便调用Dijkstra算法。
//矩阵smallest_length存储的是原先图中结点对之间的最短路径权重;
//矩阵pred存储的是对应路径中各个顶点的前驱结点。
for(int u=0;u!=new_vertex_label;++u)
{
Dijkstra(u,graph,edge_weights,smallest_weights,p);
for(int i=0;i!=new_vertex_label;++i)
{
smallest_length[u][i]=smallest_weights[i];
pred[u][i]=p[i];
}
for(int v=0;v!=new_vertex_label;++v)
smallest_length[u][v]+=h[v]-h[u];
}
}