现在的位置: 首页 > 综合 > 正文

所有点对的最短路径-FloydWarshall算法

2019年05月05日 ⁄ 综合 ⁄ 共 9633字 ⁄ 字号 评论关闭

找出所有点对的最短路径,经典算法是Floyd-Warshall,关于该算法,《算法导论》等书籍给了充分的推理介绍,但均显得过于理论化,阅读起来不容易理解。
以下我以一个例子,详细阐述该算法的解题过程,力图将跳跃性降至最低,所以只阐述算法实现过程,详细证明过程请还是参考书籍。
例图选择《算法导论》的图25-1:
 
首先要明确,这里的最短路径均为简单路径,即:一条路径最多每个顶点通过一次。所以一条最短路径最多含有n-1条边。
那个希腊字母拼写实在不方便,所以设最短路径表示为Min(i,j)。 w(i,j) 表示边(Vi,Vj)的权 
设D(k,i,j) 为从vi到vj只使用v1,v2,...,vk作为中间顶点的最短路径的权。
比如v5到v2
 D(1,5,2) = 只能使用{v1}中的点作为中间顶点 : NULL (表示不存在这样的最短路径,权值为空)
 D(2,5,2) = 只能使用{v1,v2}中的点作为中间顶点 : NULL
 D(3,5,2) = 只能使用{v1, v2, v3}中的点作为中间顶点: NULL
 D(4,5,2) = 只能使用{v1, v2, v3, v4}中的点作为中间顶点: 存在三条路径,最短路径的权为 5
            v5->v4->v3->v2      5
            v5->v4->v1->v2      11
            v5->v4->v1->v3->v2  20
 D(5,5,2) = 只能使用{v1, v2, v3, v4,v5}中的点作为中间顶点: 与D(4,5,2)一样,存在三条路径,最短路径的权为 5
            v5->v4->v3->v2      5
            v5->v4->v1->v2      11
            v5->v4->v1->v3->v2  20
由D的定义,不难看出,D(0,i,j) = w(i,j) 若不存在边(vi,vj)则 D(0,i,j) = INFINIT
D(|V|,i,j) = Min(i,j) 即vi到vj的最短路径。
 
对于k>0 vi到vj只能使用{v1,v2,...vk}作为中间顶点的最短路径 Min(i,j) 存在两种情况
 1. 使用vk作为中间顶点
    即有vi->vk->vj,
        Min(i,j) = Min(i,k) + Min(k,j)
        D(k,i,j) = D(k,i,k) + D(k,k,j)
    由于vi->vj是一条最短路径,所以vi->vk也是一条vi到vk的最短路径(一条定理,反正法很容易证明),
    则vi到vk为只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径,因为vk已经被用了且最短路径上顶点只能用一次,所以D(k,i,k) = D(k-1,i,k)。
        同理
      vk到vj为只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径,所以D(k,k,j) = D(k-1,k,j)。
    即:D(k,i,j) = D(k-1,i,k) + D(k-1,k,j)
    因为D(k,i,j) 为vi到vj只能使用{v1,v2,...,vk}作为中间顶点的最短路径Min(i,j)的权值
    D(k-1,i,k) 为vi到vk只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径Min(i,k)的权值
    D(k-1,k,j) 为vk到vj只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径Min(k,j)的权值
 
    比如上面的k=4时
    Min(5,2)         = Min(5,4) + Min(4,2)    
    v5->v4->v3->v2   = v5->v4 + v4->v3->v2
    仿照最开始的分析可以求得
    D(3,5,4) = v5->v4, D(3,4,2) = v4->v3->v2
    D(4,5,2) = D(3,5,4) + D(3,4,2)
 
 2. 根本不使用vk作为中间顶点
    即只能使用{v1,v2,...,v(k-1)}作为中间顶点
    则D(k,i,j) = D(k-1,i,j) 因为肯定不用vk为中间顶点了,所以相当于只能使用{v1,v2,...,v(k-1)}作为中间顶点的最短路径。
    比如上面的k=5时
    Min(5,2) = D(5,5,2) = D(4,5,2)
    v5->v4->v3->v2      v5没有出现在中间顶点,而是第一个顶点。
    
 综合上面便可以得到递归式:
    D(k,i,j) = min{ D(k-1,i,j), D(k-1,i,k) + D(k-1,k,j) }
 
 按照自底向上计算最短路径权值:
 伪码: FloydWarshall(W)
        {
            n = W.rows;
            D(0,i,j) = W;
            for k = 1 to n
            {// D(k,i,j) = min{ D(k-1,i,j), D(k-1,i,k) + D(k-1,k,j) }
                for i = 1 to n
                {
                    for j = 1 to n
                    {
                        d(k,i,j) = min{ d(k-1,i,j), d(k-1,i,k) + d(k-1,k,j) };
                    }
                }
            }             
        }   
    根据公式,明显:
    D(0,i,j) = W(i,j) //边信息
    D(0,i,j) = 0      3     8      INF  -4
                 INF   0     INF    1    7
                 INF   4     0      INF  INF 
                 2      INF  -5      0    INF
                 INF   INF  INF   6    0 
 
    D(1,i,j) = min{ D(0,i,j), D(0,i,1) + D(0,1,j) },
    即此时D(0,i,j)中的每对点只允许通过{v1}进行最短路径的改善,从图中很容易发现Min(4,2),Min(4,5)会因此得到改善
    D(1,i,j) = 0      3    8     INF  -4
                 INF   0    INF   1     7
                 INF   4    0     INF  INF 
                 2      5    -5     0     -2
                 INF  INF  INF  6     0
    e.g.对于Min(4,2) = D(1,4,2) = D(0,4,1) + D(0,1,2) = 2 + 3 = 5
    从图上看更明显,v4->v2本来没有边即w(4,2)=INF,由于{v1}的引入,才建立了路径v4->v1->v2
    
    D(2,i,j) = min{ D(1,i,j), D(1,i,2) + D(1,2,j) },
    即此时D(0,i,j)中的每对点只允许通过{v1,v2}进行最短路径的改善,
    即D(1,i,j)中的每对点只允许通过{v2}进行最短路径的改善, 从图中很容易发现Min(1,4),Min(3,4),Min(3,5)会因此得到改善,
     D(2,i,j) = 0      3     8    4     -4
                  INF   0     INF  1    7
                  INF   4     0    5    11
                   2     5     -5    0    -2
                  INF  INF  INF  6    0
    e.g.对于Min(1,4) = D(2,1,4) = D(1,1,2) + D(1,2,4) = 3 + 1 = 4     
    从图上看更明显,v1->v4本来没有边即w(1,4)=INF,由于{v1,v2}的引入,才建立了路径v1->v2->v4, 或者说引入{v1}没有缩短路径(1,4),引入v2后达到了效果。
 
    D(3,i,j) = min{ D(2,i,j), D(2,i,3) + D(2,3,j) },
    即此时D(0,i,j)中的每对点只允许通过{v1,v2,v3}进行最短路径的改善,
    即D(2,i,j)中的每对点只允许通过{v3}进行最短路径的改善, 从图中发现Min(4,2)会因此得到改善,
     D(3,i,j) = 0     3      8    4    -4
                  INF  0     INF  1    7
                  INF  4      0    5    11
                   2    -1    -5    0    -2
                  INF  INF  INF  6    0
    e.g.对于Min(4,2) = D(3,4,2) = D(2,4,3) + D(2,3,2) = -5 + 4 = -1
    从图上看更明显,由于{v1,v2}的引入,v4->v2建立了路径为v4->v1->2,Min(4,2)=5,但当引入了{v3}后能再一次改善最短路径Min(4,2) v4->v3->v2。
 
    D(4,i,j) = min{ D(3,i,j), D(3,i,4) + D(3,4,j) },
    即此时D(0,i,j)中的每对点只允许通过{v1,v2,v3,v4}进行最短路径的改善,
    即D(3,i,j)中的每对点只允许通过{v4}进行最短路径的改善, 从图中或根据公式发现以下点对会因此得到改善,
     D(4,i,j) = 0    3   -1    4   -4
                  3     0   -4    1    -1
                  7     4    0    5    3
                  2    -1   -5    0   -2
                  8    5    1     6    0
    e.g.对于Min(3,1) = D(4,3,1) = D(3,3,4) + D(3,4,1) = 5 + 2 = 7
    从图上看更明显,{v1,v2,v3}的引入,并没有帮助v3->v1建立路径,Min(3,1)=INF,但当引入了{v4}后能改善最短路径Min(3,1) v3->v2->v4->v1。
 
    D(5,i,j) = min{ D(4,i,j), D(4,i,5) + D(3,5,j) },
    即此时D(0,i,j)中的每对点只允许通过{v1,v2,v3,v4,v5}进行最短路径的改善,
    即D(4,i,j)中的每对点只允许通过{v5}进行最短路径的改善, 从图中或根据公式发现以下点对会因此得到改善,
     D(5,i,j) = 0    1   -3    2    -4
                  3     0   -4    1    -1
                  7     4    0    5    3
                  2    -1   -5    0   -2
                  8     5    1    6    0
    e.g.对于Min(1,3) = D(5,1,3) = D(4,1,5) + D(4,5,3) = -4 + 1 = -3
    从图上看更明显,由于{v1,v2,v3,v4}的引入,v1->v3建立了路径为 v1->v2->v4->v3,Min(1,3)=-1,但当引入了{v5}后能再一次改善最短路径Min(1,3) v1->v5->v4->v3

    
 上面的分析看似需要n+1个矩阵D来存储点对间对于不同k值的最短路径D(k,i,j)。
 由递归公式可以明显看出D(k)只依赖于D(k-1)所以至多需要两个矩阵就够了。
 接下来看,能不能再进一步,只保留一个矩阵进行迭代更新呢?
 先设有两个矩阵D(k),D(k-1)其中D(k)对于D(k-1)做了一些最短路径权值更新,
 只要D(k)更新的元素都保证不被用于计算更新其他元素,这样就可以在D(k-1)矩阵上进行原地更新,即只需要一个矩阵。
 比如D(1)相对D(0)更新了Min(4,2), Min(4,5) 只要Min(4,2) 不被用来计算Min(4,5)则在D(0)上直接做元素更新是能得到D(1)的。
 根据公式看,如果D(k,i,j) = D(k-1,i,j) 则显然不会影响,因为元素等于上一次的值,根本不用更新。
 剩下 D(k,i,j) = D(k-1,i,k) + D(k-1,k,j) = D(k,i,k) + D(k,k,j), 前面绿色公式已经证明。
 也就是说对于D(k)的更新目前只依赖D(k)本身的元素,所以可以在其基础上进行更新。
 亦即公式D(k,i,j) = min{ D(k-1,i,j), D(k-1,i,k) + D(k-1,k,j) } 右边的项均不改变值,所以不需要进行额外存储,完全可以利用其对该矩阵其他元素进行更新。
 综上,我们可以只利用一个矩阵完成迭代计算。
 
 于是有下面的算法:
    FloydWarshall(W)
    {
        int n = W.rows;
        int D[n][n];
        D = W; //D(0,i,j)
        for k = 1 to n
        {
            for i = 1 to n
            {
                for j = 1 to n
                {
                    if (D[i][k] + D[k][j] < D[i][j]) //D(k-1,i,j) > D(k-1,i,k) + D(k-1,k,j)
                    {
                        D[i][j] = D[i][k] + D[k][j]; //D(k,i,j) = D(k-1,i,k) + D(k-1,k,j)
                    }      
                }
            }
        }
        print(D);
    }
   很明显,该算法的时间复杂度为O(n^3) 至此问题算是解决一半了。
   得到了点对最短路径权值,但如何记录最短路径是什么呢?分析雷同,就不详述了,下面的代码实现中含有路径信息,有兴趣的可以阅读下。
 
代码实现:
    注意:这次用的图的存储方式为邻接矩阵 ,而不是前面的邻接表形式了。 
    该程序最终会打印出最短路径权值矩阵,同时以一对顶点为例打印其最短路径。
    该代码主要以实现功能,验证此算法为目的,对于一些细节问题,如错误保护机制等并为考虑。比如要判断图是否有回路,可以通过对d(k,i,j)的值来判断。
 
C++语言 : Codee#2582
#include <iostream>
using namespace std ;

const int MAX_VERTEX_NUM = 20 ;
const int INFINIT = 65535 ;

struct VertexNode
{
    char data [ 2 ];
    int op ;
};
struct Graph
{
    VertexNode VertexNode [ MAX_VERTEX_NUM ];
    int ** Matrix ;
    int vertexNum ;
    int edgeNum ;
};

void CreateGraph (Graph & g )
{
     g . Matrix = NULL ;
     int i , j , edgeStart , edgeEnd , weight ;
     cout << "Please input vertex and edge num (vnum enum):" << endl ;
     cin >> g . vertexNum >> g . edgeNum ;
    
     cout << "Please input vertex information (v1) /n note: every vertex info end with Enter" << endl ;
     for (i = 0 ;i < g . vertexNum ;i ++ )
     {
         cin >> g . VertexNode [ i ]. data ; // vertex data info.
     }
     int n = g . vertexNum ;

     // in order to use like D[i][j]
     g . Matrix = new int * [ n];
     for (i = 0 ; i < n; ++ i )
     {
         g . Matrix [ i ] = new int [ n];
     }

     for (i = 0 ; i < n; i ++ )
     {
        for (j = 0 ;j < n; j ++ )
        {
            if (j == i )
                g . Matrix [ i ][ j ] = 0 ;
            else
                g . Matrix [ i ][ j ] = INFINIT ;
        }
     }
     cout << "Please input edge information(start end weight):" << endl ;
     for (j = 0 ; j < g . edgeNum ; j ++ )
     {
         cin >> edgeStart >> edgeEnd >> weight ;

         g . Matrix [ edgeStart - 1 ][ edgeEnd - 1 ] = weight ;
     }
}

void PrintAdjMatrix (const Graph & g )
{
    int i , j , n, weight ;
    n = g . vertexNum ;
    cout << "Adjacent matrix for this graph is: /n " ;
    for (i = 0 ; i < n; i ++ )
    {
        for (j = 0 ; j < n; j ++ )
        {
            weight = g . Matrix [ i ][ j ];
            if (weight == INFINIT )
                cout << "INF /t " ;
            else
            {
                cout << weight << " /t " ;
            }
        }
        cout << " /n " ;
    }
    cout << "The original edge info is: /n " ;
    for (int i = 0 ; i < n; i ++ )
    {
        for (j = 0 ; j < n; j ++ )
        {
            if (g . Matrix [ i ][ j ] < INFINIT && i != j )
                cout << g . VertexNode [ i ]. data << "->" << g . VertexNode [ j ]. data << " /n " ;
        }
    }
}
void DeleteGraph (Graph & g )
{
    if (g . Matrix != NULL )
        delete [] g . Matrix ;
    g . Matrix = NULL ;
}
void PrintShortestMatrix (int ** matrix , int size )
{
    cout << "The shortest path matrix is: /n " ;
    int weight ;
    for (int i = 0 ; i < size ; ++ i )
    {
        for (int j = 0 ; j < size ; ++ j )
        {
            weight = matrix [ i ][ j ];
            if (weight == INFINIT )
                cout << "INF /t " ;
            else
            {
                cout << weight << " /t " ;
            }
        }
        cout << " /n " ;
    }
}
void PrintShortestPath (int ** path , int i , int j )
{
    static int fi = 0 ;
    static int fj = 0 ;
    if (fi == 0 )
    {
        cout << "The shortest path from v" << i + 1 << " to v" << j + 1 << " is: /n " ;
        cout << "v" << i + 1 << "->" ;
        fi = 1 ;
    }
    if (path [ i ][ j ] == INFINIT )
        return ;
    int k = path [ i ][ j ];
    cout << "v" << k + 1 << "->" ;
    PrintShortestPath (path , i , k );
    PrintShortestPath (path , k , j );
    if (fj == 0 )
    {
        cout << "v" << j + 1 << endl ;
        fj = 1 ;
    }
}
void FloydWarshall (Graph & g )
{
    int i , j , k , n;
    n = g . vertexNum ;
    int ** d = new int * [ n];
    int ** path = new int * [ n];

    for (i = 0 ; i < n; ++ i )
    {
        d [ i ] = new int [ n];
        path [ i ] = new int [ n];
    }
    for (i = 0 ; i < n; ++ i )
    {
        for (j = 0 ; j < n; ++ j )
        {
            d [ i ][ j ] = g . Matrix [ i ][ j ];
            path [ i ][ j ] = INFINIT ;
        }
    }

    for (k = 0 ; k < n; k ++ )
    {
        for (i = 0 ; i < n; i ++ )
        {
            for (j = 0 ; j < n; j ++ )
            {
                if (d [ i ][ k ] + d [ k ][ j ] < d [ i ][ j ])
                {
                    d [ i ][ j ] = d [ i ][ k ] + d [ k ][ j ];
                    path [ i ][ j ] = k ;
                }
            }
        }
    }
    PrintShortestMatrix (d , n);
    //input i-1, j-1
    PrintShortestPath (path , 3 , 1 );
    delete [] d ;
    delete [] path ;
}

int main (int argc , const char ** argv )
{
    Graph g ;
    CreateGraph (g );
    PrintAdjMatrix (g );
    FloydWarshall (g );
    DeleteGraph (g );
    return 0 ;
}
输出结果:

 
上面对于该算法的分析,求解过程实际上是典型的动态规划解题过程,
1. 描述最优子结构:那个公式的推倒,思考过程。
2. 得到一个递归解:那个公式。
3. 按自底向上的方式计算最优解:最短路径权值矩阵
4. 有计算结果构造一个最优解:给出最短路径 path

之所以开始没提动态规划,是避免有人看了这四个字就被吓跑了,因为这个算法不是很好理解,需要反复研读,练习。
希望Floyd算法能提供一个很好的动态规划样例。

抱歉!评论已关闭.