最短路径算法:迪杰斯特拉算法和弗洛伊德算法(天勤数据结构高分笔记)

迪杰斯特拉算法算法思想:
    设有两个顶点集合S和T,集合S存放途中已经找到最短路径的顶点,集合T存放的是途中剩余顶点。初始状态是,集合S只包含源点V0,然后不断从集合T中
    选取到顶点V0的路径长度最短的顶点Vu并入到初始集合中。集合S每并入一个新的顶点Vu,都要修改顶点V0到集合T中顶点最短路径长度值。不断重复此过
    程,直到集合T中所有顶点全部并入到S中为止。
    在理解“集合S没并入一个新的顶点Vu,都要修改顶点V0到集合T中顶点的最短路径长度值”时候需要注意:在Vu被选入S中之后,Vu被确定为路径上的顶点,
    此时Vu就像到达T中顶点的中转站,多了一个中转站,就会多一些到达T中顶点的新的路径,而这些新的路径有可能回避之前V0到T中顶点的路径要短,因此
    需要修改原有V0达到其他顶点的路径长度。此时对于T中的一个顶点Vk,有两种情况:一种是V0不经Vu到达Vk的路径长度为a(旧的路径长度),另一种是V0经
    过Vu到达Vk的路径长度为b(新的路径长度)。如果a<=b,则什么也不用做;如果a>=b,则用b来代替a。用同样的方法处理T中其他顶点。当T中所有顶点都被处
    理完时,会出现一组新的V0到T中各个顶点的路径,这些路径中有一条最短的,对应了T中一个顶点,就是新的Vu,将其并入S。重复上述过程,最后T中所有
    的顶点都会被并入S中,此时就可以得到V0到图中所有顶点的最短路径

    迪杰斯特拉算法的执行过程:
    需要引进3个辅助数组:dist[]、path[]、set[]。
    dist[Vi]表示当前已经找到的从V0到每个终端Vu的最短路径长度。它的初始状态为:若从V0到Vi有边,则dist[Vi]为边上的权值,否则dist[Vi]为∞。
    path[Vi]中保存从V0到Vi最短路径上Vi的一个顶点,假设最短路径上的顶点序列为V0,V1,V2.......Vi-1,Vi,则path[Vi] = Vi-1。path[]的初始状态为:如果
    V0到Vi有边,则path[Vi] = V0,否则path[Vi] = -1。
    set[]标记为数组,set[Vi] == 0表示Vi在T中,即没有被并入最短路径;set[Vi] == 1表示Vi在S中,即已经被并入最短路径。set[]的初态为:set[V0] = 1,其
    余元素全为0
    迪杰斯特拉算法执行过程如下:
        1、从当前dist[]数组中选出最小值,假设为dist[Vu],将set[Vi]设置为1,表示当前新并入的顶点为Vu;
        2、循环扫描图中顶点,对每个顶点进行以下检测:
            假设当前顶点为Vj,检索Vj是否已经被并入S中,即查看set[Vj] == 1是否成立。如果set[Vj] == 1,则什么都不做;如果set[Vj] == 0,则比较dist[Vj]
            和dist[Vu]+w的大小,其中w为边的权值。这个比较就是要看V0经过旧的最短路径到达Vj和V0经过含有Vu的新的路径达到Vj哪个更短,如果dist[Vj] 
            > dist[Vu]+w,则用新的路径来更新旧的,并把顶点Vu加入到路径中,且作为路径上Vj之前的那个顶点,否则什么都不做
        3、对1和2循环执行n-1次(n图中顶点个数)即可得到V0到其余所有顶点的最短路径。

    path[]数组中其实保存的是一颗树。这是一颗用双亲存储结构表示的树,通过这棵树可以打印出从源结点到任何一个顶点最短路径经过的所有顶点。树的双亲表示法只
    能输出由叶子结点到根结点路径上的结点,而无法实现逆向输出。因此需要借助一个栈来实现逆向输出。打印路径的函数如下:
 

void Printf_Path(int path[],int a)
{
    stack pathstack;
    //下面这个循环负责由叶子结点到根结点的顺序将其逆向输出
    while (path[a] != -1)
    {
        pathstack.push(path[a]);
        a = path[a];
    }
    pathstack.push(a);
    while (!(pathstack.empty() ))
    {
        cout << pathstack.top() << endl;    //出栈并打印出栈元素
        pathstack.pop();
    }
}
//根据上面的讲解可以写出迪杰斯特拉算法
void DijsTra(MGraph &G, int v, int dist[], int path[])
{
    int set[MAXSIZE] = { 0 };
    int min, i, j, u;
    //从这句开始对个数组进行初始化
    for (i = 0; i < G.n; ++i)
    {
        dist[i] = G.edges[v][i];
        set[i] = 0;
        if (G.edges[v][i] < INT8_MAX)
            path[i] = v;
        else
            path[i] = -1;
    }
    set[v] = 1; path[v] = -1;    //将当前其实顶点v纳入集合S中,并将路径设置为-1,表示这就是所有路径的起始顶点
    //初始化结束
    //关键操作开始
    for (i = 0; i < G.n; ++i)
    {
        min = INT8_MAX;
        //这个循环每次从剩余顶点中选取出一个顶点,通往这个顶点的路径在通往所有剩余顶点的路径中长度是最短的
        for (j = 0; j < G.n; ++j)
        {
            if (0 == set[j] && dist[j] < min)
            {
                u = j;
                min = dist[j];
            }
        }
        set[u] = 1;    //将选出的顶点放入最短路径中
        //下面这个循环以刚并入的顶点作为中间点,对所有通往剩余顶点的路径进行检测
        for ( j = 0 ;j < G.n ; ++j)
        {
            //下面这个if语句判断顶点u的加入是否会出现通往顶点j更短的路径,如果出现则改变原来的路径及其长度,否则什么也不做
            if (0 == set[j] && dist[u] + G.edges[u][j] < dist[j])
            {
                dist[j] = dist[u] + G.edges[u][j];
                path[j] = u;
            }
        }
    }    //关键操作结束
}//函数结束的时候,dist[]数组中存放了顶点v到其余顶点的最短路径长度,path[]中存放了顶点v到其余各顶点的最短路径


    迪杰斯特拉算法主要部分为一个双重循环,外层循环为两个并列的单层循环,可以任取一个循环内的操作为基本操作。基本操作执行的总 次数即为双重循环执行的操作次数,为n*n次。因此迪杰斯特拉算法的时间复杂度为O(n*n)
   弗洛伊德算法:迪杰斯特拉算法是求图中某一顶点到其余各顶点的最短路径。如果求图中任意一对顶点之间的最短路径,。则通常使用的是弗洛伊德算法
  弗洛伊德算法采用的算法思想就是动态规划。动态规划,百度百科上的解释说:“动态规划”通过把原来较复杂的问题递归的分解
为一组较简单的子问题,并通过存储每个子问题的解,使得每个子问题只计算一次就可以解决原问题的思想。

    动态规划的本质就是递归或者迭代。递归是从大变小,例如斐波那契数列中求F(n),则F(n)=F(n-1)+F(n-2),n从大到小变为1,这就是递归
    迭代过程则和递归过程相反,迭代是由小变大的。例如斐波那契数列中求F(n),则F(i+2)=F(i+1)+F(i),i从1开始逐渐增大变为n。
    动态规划就是在迭代的基础上增加一个数组(假设为A[n]),将每一步的F(i)的计算结果保存在数组A[i]中。这在迭代过程中可能多次会用到F(i)
    ,(例如求F(i+1) = F(i) + F(i-1) 和求F(i+2)=F(i+1)+F(i)时都要两次用到F(i) )此时就不需要再进行大量的计算了,直接从数组A[i]中取就行了
    动态规划的目的就是为了减少迭代和递归过程中不必要的重复性计算,每个中间结果计算一次就行了

弗洛伊德算法求解最短路径的一般过程:
    1、设置两个矩阵A[][]和Path[][]。初始时将图的邻接矩阵赋值给A[][].。将矩阵Path[][]中元素全部设置为-1。
    (根据Path矩阵可以算出任意两个顶点间最短路径的序列,它保存就是的两顶点间的最短路径上的后继顶点的信息)
    2)以顶点k为中间结点,k取0~n-1 (n为图中顶点个数),对图中所有顶点对{i,j}进行如下检测:
        如果A[i][j] > A[i][k] + A[k][j]的值,就将Path[i][j]改为k,否则什么都不做
    代码如下:
 

void Floyd(MGraph G, int path[][MAXSIZE])
{
    int i = 0,j = 0,k = 0;    //局部变量最好初始化为0,否则容易读到内存中的脏数据
    int A[MAXSIZE][MAXSIZE] = { 0 };
    for ( i = 0 ; i < G.n ;    ++i)
    {
        for (j = 0; j < G.n; ++j)
        {
            A[i][j] = G.edges[i][j];
            path[i][j] = -1;
        }
    }
    //下面这个三层循环是弗洛伊德算法的主要操作,完成了以k为中间点对所有顶点对(i,j)进行检测和修改
    for ( k = 0; k < G.n ; ++k)
        for (i = 0; i < G.n; ++i)
            for (j = 0; j < G.n; ++j)
            {
                if(A[i][j] > A[i][k]+ A[k][j])
                path[i][j] = k;
            }
}
//弗洛伊德算法的时间复杂度为O(n*n*n)
//打印出弗洛伊德算法中求得的最短路径的序列
void Print_Path(int U, int V, int path[][MAXSIZE])
{
    if (-1 == path[U][V])
        cout << U << " " << endl;
    else
    {
        int mid = path[U][V];
        Print_Path(U, mid, path);    //处理mid前半段路径
        Print_Path(mid, V, path);    //处理mid后半段路径
    }
}

更新于2019.9.30:

     这些算法其实也不能算是天勤书上写的,算法原出处应该是巫俊泽的《挑战程序设计竞赛》或者刘汝佳的《算法竞赛入门经典》三部曲,(行内人士将刘汝佳的三部曲称之为算法竞赛中的“黑书”)。安利一波,这两本书真的不错。话说我当初学习算法的时候没有人带,走了很多弯路,给出这两本书的名字希,望后来者学习的不要走这么多弯路。

你可能感兴趣的:(数据结构与算法)