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

一:定義

弗洛伊德算法是用來求全部頂點到全部頂點的時間複雜度。
雖然咱們能夠直接對每一個頂點經過迪傑斯特拉算法求得全部的頂點到全部頂點的時間複雜度,時間複雜度爲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),不適合計算大量數據。

補充:

不管是迪傑斯特拉算法仍是弗洛伊德算法,對於有向圖,無向圖都是可使用的。 另外咱們的最短路徑通常都是針對有環圖無環圖使用拓撲排序能夠得到
相關文章
相關標籤/搜索