1. 程式人生 > >最短路徑---SPFA演算法(C++)

最短路徑---SPFA演算法(C++)

適用範圍:給定的圖存在負權邊,這時類似Dijkstra等演算法便沒有了用武之地,而Bellman-Ford演算法的複雜度又過高,SPFA演算法便派上用場了。 我們約定有向加權圖G不存在負權迴路,即最短路徑一定存在。當然,我們可以在執行該演算法前做一次拓撲排序,以判斷是否存在負權迴路,但這不是我們討論的重點。

演算法思想:我們用陣列d記錄每個結點的最短路徑估計值,用鄰接表來儲存圖G。我們採取的方法是動態逼近法:設立一個先進先出的佇列用來儲存待優化的結點,優化時每次取出隊首結點u,並且用u點當前的最短路徑估計值對離開u點所指向的結點v進行鬆弛操作,如果v點的最短路徑估計值有所調整,且v點不在當前的佇列中,就將v點放入隊尾。這樣不斷從佇列中取出結點來進行鬆弛操作,直至佇列空為止

期望的時間複雜度O(ke), 其中k為所有頂點進隊的平均次數,可以證明k一般小於等於2。

 

實現方法:

  建立一個佇列,初始時佇列裡只有起始點,再建立一個表格記錄起始點到所有點的最短路徑(該表格的初始值要賦為極大值,該點到他本身的路徑賦為0)。然後執行鬆弛操作,用佇列裡有的點作為起始點去重新整理到所有點的最短路,如果重新整理成功且被重新整理點不在佇列中則把該點加入到佇列最後。重複執行直到佇列為空。

判斷有無負環:
  如果某個點進入佇列的次數超過N次則存在負環(SPFA無法處理帶負環的圖)

首先建立起始點a到其餘各點的
最短路徑表格

首先源點a入隊,當佇列非空時:
 1、隊首元素(a)出隊,對以a為起始點的所有邊的終點依次進行鬆弛操作(此處有b,c,d三個點),此時路徑表格狀態為:

在鬆弛時三個點的最短路徑估值變小了,而這些點佇列中都沒有出現,這些點
需要入隊,此時,佇列中新入隊了三個結點b,c,d

隊首元素b點出隊,對以b為起始點的所有邊的終點依次進行鬆弛操作(此處只有e點),此時路徑表格狀態為:

在最短路徑表中,e的最短路徑估值也變小了,e在佇列中不存在,因此e也要
入隊,此時佇列中的元素為c,d,e

隊首元素c點出隊,對以c為起始點的所有邊的終點依次進行鬆弛操作(此處有e,f兩個點),此時路徑表格狀態為:

在最短路徑表中,e,f的最短路徑估值變小了,e在佇列中存在,f不存在。因此
e不用入隊了,f要入隊,此時佇列中的元素為d,e,f

 隊首元素d點出隊,對以d為起始點的所有邊的終點依次進行鬆弛操作(此處只有g這個點),此時路徑表格狀態為:

在最短路徑表中,g的最短路徑估值沒有變小(鬆弛不成功),沒有新結點入隊,佇列中元素為f,g

隊首元素f點出隊,對以f為起始點的所有邊的終點依次進行鬆弛操作(此處有d,e,g三個點),此時路徑表格狀態為:

在最短路徑表中,e,g的最短路徑估值又變小,佇列中無e點,e入隊,佇列中存在g這個點,g不用入隊,此時佇列中元素為g,e

隊首元素g點出隊,對以g為起始點的所有邊的終點依次進行鬆弛操作(此處只有b點),此時路徑表格狀態為:

在最短路徑表中,b的最短路徑估值又變小,佇列中無b點,b入隊,此時佇列中元素為e,b
隊首元素e點出隊,對以e為起始點的所有邊的終點依次進行鬆弛操作(此處只有g這個點),此時路徑表格狀態為:

在最短路徑表中,g的最短路徑估值沒變化(鬆弛不成功),此時佇列中元素為b

隊首元素b點出隊,對以b為起始點的所有邊的終點依次進行鬆弛操作(此處只有e這個點),此時路徑表格狀態為:

在最短路徑表中,e的最短路徑估值沒變化(鬆弛不成功),此時佇列為空了

最終a到g的最短路徑為14

 

程式碼:

spfa.h

#ifndef SPFA_H
#define SPFA_H

#pragma once

#include<iostream>
#include<string>
#include<queue>
using namespace std;

/*
本演算法是使用SPFA來求解圖的單源最短路徑問題
採用了鄰接表作為圖的儲存結構
可以應用於任何無環的圖
*/

struct ArcNode
{
    int adjvex;     // 尾端的頂點下標
    ArcNode *next;  // 下一條邊的尾端頂點
    int weight;
};

struct Vnode
{
    string data;   // 頂點資訊
    ArcNode *firstarc;  // 第一條依附在該頂點的邊
};

struct Dis
{
    string path;  // 從源點到該頂點的最短路徑
    int weight;   // 最短路徑的權重
};

class Graph
{
private:
    int vexnum;  // 點的個數
    int edge;    // 邊的個數
    Vnode *node; // 鄰接表
    Dis *dis;    // 記錄最短路徑資訊的陣列
public:
    Graph(int vexnum, int edge);
    ~Graph();
    void CreateGraph(int kind);
    bool check_edge_value(int start, int end);
    void print();
    bool SPFA(int begin);
    void print_path(int begin);
};



#endif // SPFA_H

spfa.cpp

#include "spfa.h"

Graph::Graph(int vexnum, int edge) {
    //對頂點個數和邊的條數進行賦值
    this->vexnum = vexnum;
    this->edge = edge;

    //為鄰接矩陣開闢空間
    node = new Vnode[this->vexnum];
    dis = new Dis[this->vexnum];
    int i;
    //對鄰接表進行初始化
    for (i = 0; i < this->vexnum; ++i) {
        node[i].data = "v" + to_string(i + 1);
        node[i].firstarc = NULL;
    }
}

Graph::~Graph() {
    int i;
    //釋放空間,但是記住圖中每個結點的連結串列也要一一釋放
    ArcNode *p, *q;
    for (i = 0; i < this->vexnum; ++i) {
        //一定要注意這裡,要判斷該頂點的出度到底是否為空,不然會出錯
        if (this->node[i].firstarc) {
            p = node[i].firstarc;
            while (p) {
                q = p->next;
                delete p;
                p = q;
            }
        }

    }
    delete [] node;
    delete [] dis;
}

// 判斷我們每次輸入的的邊的資訊是否合法
//頂點從1開始編號
bool Graph::check_edge_value(int start, int end) {
    if (start<1 || end<1 || start>vexnum || end>vexnum) {
        return false;
    }
    return true;
}

void Graph::print() {
    cout << "圖的鄰接表的列印:" << endl;
    int i;
    ArcNode *temp;
    //遍歷真個鄰接表
    for (i = 0; i < this->vexnum; ++i) {
        cout << node[i].data << " ";
        temp = node[i].firstarc;
        while (temp) {
            cout << "<"
                << node[i].data
                << ","
                << node[temp->adjvex].data
                << ">="
                << temp->weight
                << " ";
            temp = temp->next;
        }
        cout << "^" << endl;
    }
}

void Graph::CreateGraph(int kind)
{
    //kind代表圖的種類,2為無向圖
    cout << "輸入邊的起點和終點以及各邊的權重(頂點編號從1開始):" << endl;
    int i;
    int start;
    int end;
    int weight;
    for (i = 0; i < this->edge; ++i)
    {
        cin >> start >> end >> weight;
        //判斷輸入的邊是否合法
        while (!this->check_edge_value(start, end)) {
            cout << "輸入邊的資訊不合法,請重新輸入:" << endl;
            cin >> start >> end >> weight;
        }
        ArcNode *temp = new ArcNode;
        temp->adjvex = end - 1;
        temp->weight = weight;
        temp->next = nullptr;
        //如果該頂點依附的邊為空,則從以第一個開始
        if (node[start-1].firstarc == nullptr)
        {
            node[start-1].firstarc = temp;
        }
        else
        {
            //否則,則插入到該連結串列的最後一個位置
            ArcNode *now = node[start - 1].firstarc;
            //找到連結串列的最後一個結點
            while (now->next)
            {
                now = now->next;
            }
            now->next = temp;
        }

         //如果是無向圖,則反向也要新增新的結點
        if (kind == 2)
        {
            //新建一個新的表結點
            ArcNode *temp_end = new ArcNode;
            temp_end->adjvex = start - 1;
            temp_end->weight = weight;
            temp_end->next = nullptr;
            //如果該頂點依附的邊為空,則從以第一個開始
            if (node[end - 1].firstarc == nullptr)
            {
                node[end - 1].firstarc = temp_end;
            }
            else
            {
                //否則,則插入到該連結串列的最後一個位置
                ArcNode *now = node[end - 1].firstarc;
                //找到連結串列的最後一個結點
                while (now->next)
                {
                    now = now->next;
                }
                now->next = temp_end;
            }
        }
    }
}

bool Graph::SPFA(int begin)
{
    bool *visit;
    //visit用於記錄是否在佇列中
    visit = new bool[this->vexnum];
    int *input_queue_time;
    //input_queue_time用於記錄某個頂點入佇列的次數
    //如果某個入佇列的次數大於頂點數vexnum,那麼說明這個圖有環,
    //沒有最短路徑,可以退出了
    input_queue_time = new int[this->vexnum];
    queue<int> s;  //佇列,用於記錄最短路徑被改變的點

    /*
    各種變數的初始化
    */
    int i;
    for (i = 0; i < this->vexnum; ++i)
    {
        visit[i] = false;
        input_queue_time[i] = 0;
        //路徑開始都初始化為直接路徑,長度都設定為無窮大
        dis[i].path = this->node[begin - 1].data + "-->" + this->node[i].data;
        dis[i].weight = INT_MAX;
    }

    //首先是起點入佇列,我們記住那個起點代表的是頂點編號,從1開始的
    s.push(begin - 1);
    visit[begin - 1] = true;
    ++input_queue_time[begin - 1];

    dis[begin - 1].path = this->node[begin - 1].data;
    dis[begin - 1].weight = 0;

    int temp;
    ArcNode *temp_node;
    // 進入佇列的迴圈
    while (!s.empty())
    {
        //取出隊首的元素,並且把隊首元素出佇列
        temp = s.front();
        s.pop();

        //必須要保證第一個結點不為空(為了避免出現"人為選擇了非法結點-1這種情況")
        if (node[temp].firstarc)
        {
            temp_node = node[temp].firstarc;
            while (temp_node)
            {
                //如果邊<temp,temp_node>的權重加上temp這個點的最短路徑
                //小於之前temp_node的最短路徑的長度,則更新
                //temp_node的最短路徑的資訊
                if (dis[temp_node->adjvex].weight > (temp_node->weight + dis[temp].weight))
                {
                    //更新dis陣列的資訊
                    dis[temp_node->adjvex].weight = temp_node->weight + dis[temp].weight;
                    dis[temp_node->adjvex].path = dis[temp].path + "-->" + node[temp_node->adjvex].data;
                    //如果還沒在佇列中,加入佇列,修改對應的資訊
                    if (!visit[temp_node->adjvex])
                    {
                        visit[temp_node->adjvex] = true;
                        ++input_queue_time[temp_node->adjvex];
                        s.push(temp_node->adjvex);
                        if (input_queue_time[temp_node->adjvex] > this->vexnum)
                        {
                            cout << "圖中有負環" << endl;
                            return false;
                        }
                    }
                }
                temp_node = temp_node->next;
            }
            visit[temp] = false;
        }
    }
    return true;
}


void Graph::print_path(int begin)
{
    cout << "以頂點" << this->node[begin - 1].data
        << "為起點,到各個頂點的最短路徑的資訊:" << endl;
    int i;
    for (i = 0; i < this->vexnum; ++i) {
        if (dis[i].weight == INT_MAX) {
            cout << this->node[begin - 1].data << "---" << this->node[i].data
                 << "  無最短路徑,這兩個頂點不連通"
                 << endl;
        }
        else
        {
            cout << this->node[begin - 1].data << "---" << this->node[i].data
                 << "  weight: " << dis[i].weight
                 << "  path: " << dis[i].path
                 << endl;
        }
    }
}



main.cpp

#include"spfa.h"

//檢驗輸入邊數和頂點數的值是否有效,可以自己推算為啥:
//頂點數和邊數的關係是:((Vexnum*(Vexnum - 1)) / 2) < edge
bool check(int Vexnum, int edge) {
    if (Vexnum <= 0 || edge <= 0 || ((Vexnum*(Vexnum - 1)) / 2) < edge)
        return false;
    return true;
}
int main() {
    int vexnum; int edge;
    cout << "輸入圖的種類:1代表有向圖,2代表無向圖" << endl;
    int kind;
    cin >> kind;
    //判讀輸入的kind是否合法
    while (1) {
        if (kind == 1 || kind == 2) {
            break;
        }
        else {
            cout << "輸入的圖的種類編號不合法,請重新輸入:1代表有向圖,2代表無向圖" << endl;
            cin >> kind;
        }
    }

    cout << "輸入圖的頂點個數和邊的條數:" << endl;
    cin >> vexnum >> edge;
    while (!check(vexnum, edge)) {
        cout << "輸入的數值不合法,請重新輸入" << endl;
        cin >> vexnum >> edge;
    }


    /*------正文------*/
    Graph graph(vexnum, edge);
    graph.CreateGraph(kind);
    graph.print();
    //記得SPFA一個引數,代表起點,這個起點從1開始
    if (graph.SPFA(1))
    {
        graph.print_path(1);
    }
    return 0;
}

執行結果:

1.檢測正環:

2.檢測負環:

注:

SPFA演算法相當於在Bellman-Ford演算法的基礎上,對選邊策略做了優化,採用類似BFS的遍歷順序對邊進行relax操作,從源點處開始擴散,從而提高演算法的效率。

參考資料:

http://lib.csdn.net/article/datastructure/10344

https://blog.csdn.net/qq_35644234/article/details/61614581

參考資料2程式碼中存在的問題:

  • SPFA函式中,visit陣列沒有重置成false,以及res變數定義但沒使用
  • main函式需要判斷是否有負環,再進行路徑列印
  • SPFA可以對負權值的圖進行操作,所以check_edge_value可以不傳weight形參