1. 程式人生 > >數據結構(五)圖---最短路徑(弗洛伊德算法)

數據結構(五)圖---最短路徑(弗洛伊德算法)

直接 char getchar 更新 none typedef article truct 使用

一:定義

弗洛伊德算法是用來求所有頂點到所有頂點的時間復雜度。
雖然我們可以直接對每個頂點通過迪傑斯特拉算法求得所有的頂點到所有頂點的時間復雜度,時間復雜度為O(n*3),但是弗洛伊德算法更加簡潔優雅

二:弗洛伊德的使用介紹

若是求一個頂點到其他頂點的最短距離,例如迪傑斯特拉算法,我們的距離數組和路徑數組使用一維即可,但是我們這裏是獲取所有頂點到其余頂點的最短距離,所以我們對於數組和路徑都需要使用二維數組來表示

下面我們使用一個有三個頂點的圖來進行講解:

技術分享圖片

(1)我們先定義兩個二維數組D0[3][3]和P0[3][3]

D0表示頂點到頂點的最短路徑權值和的矩陣。
P0表示對於頂點的最小路徑前驅矩陣
將D0初始化為他的初始的圖的鄰接矩陣
將P0初始化為圖中所示每行從0-n

(2)處理兩個數組

技術分享圖片

上面的公式是以v0作為中轉點,實際上我們可以使用所有鄰接點作為中轉點,所以我們程序使用的是下面的轉化公式

技術分享圖片

註意(重點):

D1數組是我們獲取的最短路徑,我們直接使用頂點對應的行即可獲得所有的從該頂點出發的到其他頂點的最短路徑權重和
P1數組是我們獲取的前驅結點,我們使用的不是頂點所對應的行,而是其對應的列,這個才是我們需要的路徑

上面只是使用了一個簡單的圖來講解,對於復雜的圖我們依舊可以使用它

技術分享圖片

初始化

技術分享圖片

處理後

技術分享圖片

三:弗洛伊德基本思想

弗洛伊德算法定義了兩個二維矩陣:
矩陣D記錄頂點間的最小路徑 
  例如D[0][3]= 10,說明頂點0 到 3 的最短路徑為10;
矩陣P記錄頂點間最小路徑中的中轉點 
  例如P[0][3]= 1 說明,0 到 3的最短路徑軌跡為:0 -> 1 -> 3。
它通過3重循環,k為中轉點,v為起點,w為終點,循環比較D0[v][w] 和 D0[v][k] + D0[k][w] 最小值,如果D0[v][k] + D0[k][w] 為更小值,則把D0[v][k] + D0[k][w] 覆蓋保存在D1[v][w]中。

核心思想是:

D1[v][w] = min{D0[v][k] + D0[k][w],D0[v][w]}

其中D0代表原來未更新前的數據,D1表示我們修改更新後的新的數據

四:代碼實現

(一)結構定義

//鄰接矩陣結構
typedef struct
{
    VertexType vers[MAXVEX];    //頂點表
    EdgeType arc[MAXVEX][MAXVEX];    //
鄰接矩陣,可看作邊表 int numVertexes, numEdges; //圖中當前的頂點數和邊數 }MGraph;

(二)弗洛伊德算法

    //使用弗洛伊德核心算法,三層循環求解
    for (k = 0; k < G.numVertexes;k++)
    {
        for (i = 0; i < G.numVertexes;i++)
        {
            for (j = 0; j < G.numVertexes;j++)
            {
                if ((*dist)[i][j]>((*dist)[i][k]+(*dist)[k][j])&&i!=j)  //i!=j使不更新中間自己到自己的數據和路徑
                {
                    //將權值和更新,路徑也變為中轉點
                    (*dist)[i][j] = (*dist)[i][k] + (*dist)[k][j];
                    (*path)[i][j] = (*path)[i][k];
                }
            }
        }
    }

(三)打印最短路徑

技術分享圖片
void ShowDistAndPath(Path P, Dist D,int n)
{
    int i, j;
    printf("Printf Dist:\n");
    for (i = 0; i < n;i++)
    {
        for (j = 0; j < n; j++)
        {
            if (i==j)
                printf("    0");    //需要將我們的無窮轉換一下再顯示
            else
                printf("%5d", D[i][j]);
        }
        printf("\n");
    }

    printf("Printf Path:\n");
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < n; j++)
            printf("%5d", P[i][j]);
        printf("\n");
    }
}
ShowDistAndPath

五:全部代碼實現

技術分享圖片
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include "queue.h"

#define MAXVEX 100    //最大頂點數
#define INFINITY 65535    //用0表示∞

typedef char VertexType;    //頂點類型,字符型A,B,C,D...
typedef int EdgeType;    //邊上權值類型10,15,...

//鄰接矩陣結構
typedef struct
{
    VertexType vers[MAXVEX];    //頂點表
    EdgeType arc[MAXVEX][MAXVEX];    //鄰接矩陣,可看作邊表
    int numVertexes, numEdges;    //圖中當前的頂點數和邊數
}MGraph;

typedef int Dist[MAXVEX][MAXVEX];    //存放各個頂點到其余頂點的最短路徑權值和
typedef int Path[MAXVEX][MAXVEX];    //存放各個頂點到其余頂點前驅頂點位置

//創建鄰接矩陣
void CreateMGraph(MGraph* G);
//顯示鄰接矩陣
void showGraph(MGraph G);

void Floyd(MGraph G,Path* path,Dist* dist);
void ShowDistAndPath(Path P, Dist D,int n);

void Floyd(MGraph G, Path* path, Dist* dist)
{
    int i,j,k;

    //初始化path和dist
    for (i = 0; i < G.numVertexes;i++)
    {
        for (j = 0; j < G.numVertexes;j++)
        {
            (*dist)[i][j] = G.arc[i][j];
            (*path)[i][j] = j;    //初始化為這個的一個好處就是自己到自己的路徑就是自己,我們不用修改
        }
    }

    //使用弗洛伊德核心算法,三層循環求解
    for (k = 0; k < G.numVertexes;k++)
    {
        for (i = 0; i < G.numVertexes;i++)
        {
            for (j = 0; j < G.numVertexes;j++)
            {
                if ((*dist)[i][j]>((*dist)[i][k]+(*dist)[k][j])&&i!=j)
                {
                    //將權值和更新,路徑也變為中轉點
                    (*dist)[i][j] = (*dist)[i][k] + (*dist)[k][j];
                    (*path)[i][j] = (*path)[i][k];
                }
            }
        }
    }
}

void ShowDistAndPath(Path P, Dist D,int n)
{
    int i, j;
    printf("Printf Dist:\n");
    for (i = 0; i < n;i++)
    {
        for (j = 0; j < n; j++)
        {
            if (i==j)
                printf("    0");    //需要將我們的無窮轉換一下再顯示
            else
                printf("%5d", D[i][j]);
        }
        printf("\n");
    }

    printf("Printf Path:\n");
    for (i = 0; i < n; i++)
    {
        for (j = 0; j < n; j++)
            printf("%5d", P[i][j]);
        printf("\n");
    }
}

int main()
{
    MGraph MG;
    CreateMGraph(&MG);
    showGraph(MG);
    Path path;
    Dist dist;
    Floyd(MG, &path, &dist);
    ShowDistAndPath(path, dist, MG.numVertexes);
    system("pause");
    return 0;
}

//生成鄰接矩陣
void CreateMGraph(MGraph* G)
{
    int i, j, k, w;
    G->numVertexes = 9;
    G->numEdges = 16;
    //讀入頂點信息
    G->vers[0] = A;
    G->vers[1] = B;
    G->vers[2] = C;
    G->vers[3] = D;
    G->vers[4] = E;
    G->vers[5] = F;
    G->vers[6] = G;
    G->vers[7] = H;
    G->vers[8] = I;

    //getchar();    //可以獲取回車符
    for (i = 0; i < G->numVertexes; i++)
        for (j = 0; j < G->numVertexes; j++)
            G->arc[i][j] = INFINITY;    //鄰接矩陣初始化

    //創建了有向鄰接矩陣
    G->arc[0][1] = 1;
    G->arc[0][2] = 5;
    G->arc[1][2] = 3;
    G->arc[1][3] = 7;
    G->arc[1][4] = 5;
    G->arc[2][4] = 1;
    G->arc[2][5] = 7;
    G->arc[3][4] = 2;
    G->arc[3][6] = 3;
    G->arc[4][5] = 3;
    G->arc[4][6] = 6;
    G->arc[4][7] = 9;
    G->arc[5][7] = 5;
    G->arc[6][7] = 2;
    G->arc[6][8] = 7;
    G->arc[7][8] = 4;

    for (i = 0; i < G->numVertexes;i++)
        for (k = i; k < G->numVertexes;k++)
            G->arc[k][i] = G->arc[i][k];
}


//顯示鄰接矩陣
void showGraph(MGraph G)
{
    for (int i = 0; i < G.numVertexes; i++)
    {
        for (int j = 0; j < G.numVertexes; j++)
        {
            if (G.arc[i][j] != INFINITY)
                printf("%5d", G.arc[i][j]);
            else
                printf("    0");
        }
        printf("\n");
    }
}
View Code

技術分享圖片

六:循環分析

問:可不可以先循環i和j,然後把k放到最內層呢?

答案是不行的,如果打亂了i、j、k的順序,則程序無法得出正確的結果。

可以把k想象成一個階段,即k為中轉點時,枚舉i、j,通過k的變動不停地松弛i、j之間的最短路。因為i、j可以重復遍歷,但k不能。如果k在內層循環,程序無法進行多次的松弛操作,也就是程序出錯的原因。
我們可以認為,我們每一次的整個數組的變化都是建立在同一個中轉k值基礎上才能夠得到正確的數據,我們每次更新完整個數組後才可以去變化k值,去重新更新一次新的,正確的數組
當我們將k放入內層,數組的每次內部更新變為動態了,我們不確定那些是正確的最短路徑,因為某些數據沒有得到正確的結果,就被拿到下一次繼續使用了

錯誤實驗:

  for (i = 0; i < G.numVertexes;i++)
    {
        for (j = 0; j < G.numVertexes;j++)
        {
            for (k = 0; k < G.numVertexes; k++)
            {
                if ((*dist)[i][j]>((*dist)[i][k] + (*dist)[k][j]) && i != j)
                {
                    //將權值和更新,路徑也變為中轉點
                    (*dist)[i][j] = (*dist)[i][k] + (*dist)[k][j];
                    (*path)[i][j] = (*path)[i][k];
                }
            }
        }
    }

技術分享圖片

七:性能分析

Floyd算法適用於APSP(All Pairs Shortest Paths,多源最短路徑),是一種動態規劃算法,稠密圖效果最佳,邊權可正可負。
此算法簡單有效,由於三重循環結構緊湊,對於稠密圖,效率要高於執行|V|次Dijkstra算法,也要高於執行|V|次SPFA算法。
優點:容易理解,可以算出任意兩個節點之間的最短距離,代碼編寫簡單。
缺點:時間復雜度比較高O(n*3),不適合計算大量數據。

補充:

無論是迪傑斯特拉算法還是弗洛伊德算法,對於有向圖,無向圖都是可以使用的。
另外我們的最短路徑一般都是針對有環圖,無環圖使用拓撲排序可以獲得

數據結構(五)圖---最短路徑(弗洛伊德算法)