單源最短路徑:SPFA算法

單源最短路徑:SPFA算法

概述

SPFA(Shortest Path Faster Algorithm)算法,是西南交通大學段凡丁於 1994 年發表的,其在Bellman-ford算法的基礎上加上一個隊列優化,減少了冗餘的鬆弛操作,是一種高效的最短路算法。

問題

在帶權有向圖G=(V,A)中,假設每條弧A[i]的長度爲w[i],找到由頂點V0到其餘各點的最短路徑。

算法描述

算法思想

設立一個隊列用來保存待優化的頂點,優化時每次取出隊首頂點u,並且用u點當前的最短路徑估計值dist[u]對與u點鄰接的頂點v進行鬆弛操作,如果v點的最短路徑估計值dist[v]可以更小,且v點不在當前的隊列中,就將v點放入隊尾。這樣不斷從隊列中取出頂點來進行鬆弛操作,直至隊列空爲止。
判斷有無負環:如果某個點進入隊列的次數大於等於總節點數則存在負環(SPFA無法處理帶負環的圖)。

算法過程

SPFA算法過程示例圖
首先建立源點到各點的最短距離表格
SPFA算法過程圖1
首先源點入隊,當隊列非空時:
1. 隊首節點a出隊,對a的所有出邊鄰接點進行鬆弛操作(此處有b,c,d三個點),此時距離表格狀態爲:
SPFA算法過程圖2
在鬆弛時源點到這三個點的距離都變小了,且這些點現在都不在隊列內,於是將這些點入隊。
2. 隊首節點b點出隊,對b的所有出邊鄰接點進行鬆弛操作(此處只有e點),此時距離表格狀態爲:
SPFA算法過程圖3
e的距離估值也變小了,且e不在隊內,於是將e入隊。此時隊列中的節點爲c,d,e。
3. 隊首節點c出隊,對c的所有出邊鄰接點進行鬆弛操作(此處有e,f兩個點),此時距離表格狀態爲:
SPFA算法過程圖4
e,f的距離估值都變小了,但e已經在隊列中,所以只有f需要入隊。此時隊列中的節點爲d,e,f。
4. 依此類推,之後的距離表格狀態依次爲:
SPFA算法過程圖5
SPFA算法過程圖6
SPFA算法過程圖7
SPFA算法過程圖8
SPFA算法過程圖9
(爲什麼最後出現了兩張一模一樣的圖?因爲倒數第二個在隊列內的節點e對唯一一個出邊鄰接點g鬆弛不成功,g的距離估值沒有變化;最後一個在隊列內的節點b對唯一一個出邊鄰接點e鬆弛不成功,e的距離估值也沒有變化)
到這裏,隊列爲空,算法執行完成。

程序代碼

SPFA的兩種寫法,bfs和dfs,bfs判別負環不穩定,相當於限深度搜索,但是設置得好的話還是沒問題的,dfs的話判斷負環很快。

/*
FILE:spfa_bfs.cpp
LANG:C++
*/
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

const int node_num = 100;    //最大節點數
const int INF = 2147483647;

int matrix[node_num][node_num];    //鄰接矩陣
int dist[node_num];    //距離估值
int path[node_num];    //記錄前驅節點
bool vis[node_num];    //記錄節點是否在隊列內
int v_num, a_num;    //記錄節點數、弧數

bool spfa_bfs(const int);

int main()
{
    cout << "v_num:";
    cin >> v_num;
    cout << "a_num:";
    cin >> a_num;

    for (int i = 0; i < v_num; ++i)
    {
        for (int j = 0; j < v_num; ++j)
        {
            matrix[i][j] = ((i != j) ? INF : 0);    //初始化鄰接矩陣
        }
    }

    int u, v, w;
    for (int i = 0; i < a_num; ++i)
    {
        cin >> u >> v >> w;
        matrix[u][v] = matrix[v][u] = w;
    }

    int src;
    cout << "source:";
    cin >> src;

    if (spfa_bfs(src))
    {
        cout << "E" << endl;    //存在負環
        return 0;
    }

    for (int i = 0; i < v_num; ++i)
    {
        if (i == src)
        {
            continue;
        }
        cout << src << "->" << i << ":" << dist[i] << ":" << i;
        int t = path[i];
        while (t != src)
        {
            cout << "-" << t;
            t = path[t];
        }
        cout << "-" << src << endl;    //倒序輸出最短路徑
    }
    return 0;
}

bool spfa_bfs(const int src)
{
    memset(vis, false, sizeof(vis));
    queue<int> q;
    int cnt[node_num] = {0};    //記錄每個節點的進隊次數
    for (int i = 0; i < v_num; ++i)
    {
        dist[i] = INF;    //初始化距離表
        path[i] = src;    //初始化前驅節點表
    }
    dist[src] = 0;    //設置源點距離自己的距離爲0
    q.push(src);    //源點進隊
    vis[src] = true;    //打上進隊標記
    ++cnt[src];    //記錄進隊次數
    while (!q.empty())    //隊列非空則一直循環
    {
        int x;
        x = q.front();    //讀取隊首節點
        q.pop();    //彈出隊首節點
        vis[x] = false;    //去除隊列標記

        for (int i = 0; i < v_num; ++i)
        {
            if (matrix[x][i] != INF && dist[x] + matrix[x][i] < dist[i])    //如果i是讀取的節點的出邊鄰接點且可以進行鬆弛操作
            {
                dist[i] = dist[x] + matrix[x][i];    //鬆弛操作
                path[i] = x;    //更新前驅
                if (!vis[i])    //如果不在隊列內
                {
                    q.push(i);    //進隊
                    vis[i] = true;    //打上標記
                    ++cnt[i];    //記錄進隊次數
                    if (cnt[i] >= v_num)    //如果這個節點的進隊次數大於等於節點總數
                    {
                        return true;    //說明存在負環,無法處理
                    }
                }
            }
        }
    }
    return false;
}
/*
FILE:spfa_dfs.cpp
LANG:C++
*/
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

const int node_num = 100;
const int INF = 2147483647;
int matrix[node_num][node_num], dist[node_num], path[node_num];
bool vis[node_num];
int v_num, a_num;
queue<int> q;

bool spfa_dfs(const int);
int main()
{
    cout << "v_num:";
    cin >> v_num;
    cout << "a_num:";
    cin >> a_num;

    for (int i = 0; i < v_num; ++i)
    {
        for (int j = 0; j < v_num; ++j)
        {
            matrix[i][j] = ((i != j) ? INF : 0);
        }
    }

    int u, v, w;
    for (int i = 0; i < a_num; ++i)
    {
        cin >> u >> v >> w;
        matrix[u][v] = matrix[v][u] = w;
    }

    int src;
    cout << "source:";
    cin >> src;

    memset(vis, false, sizeof(vis));
    for (int i = 0; i < v_num; ++i)
    {
        dist[i] = INF;
        path[i] = src;
    }
    dist[src] = 0;

    if (spfa_dfs(src))
    {
        cout << "E" << endl;
        return 0;
    }

    for (int i = 0; i < v_num; ++i)
    {
        if (i == src)
        {
            continue;
        }
        cout << src << "->" << i << ":" << dist[i] << ":" << i;
        int t = path[i];
        while (t != src)
        {
            cout << "-" << t;
            t = path[t];
        }
        cout << "-" << src << endl;
    }
    return 0;
}

bool spfa_dfs(const int src)
{
    q.push(src);
    vis[src] = true;
    while (!q.empty())
    {
        int x;
        x = q.front();
        q.pop();
        vis[x] = false;

        for (int i = 0; i < v_num; ++i)
        {
            if (matrix[x][i] != INF && dist[x] + matrix[x][i] < dist[i])
            {
                dist[i] = dist[x] + matrix[x][i];
                path[i] = x;
                if (!vis[i])
                {
                    if (spfa_dfs(i))
                    {
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

示例圖:
SPFA算法示例圖
運行結果:
SPFA算法運行結果

算法優化

SPFA算法有兩個優化算法SLF和LLL:
SLF:Small Label First策略,設要加入的節點是j,隊首元素爲i,若dist(j)<dist(i),則將j插入隊首,否則插入隊尾。
LLL:Large Label Last策略,設隊首元素爲i,每次彈出時進行判斷,隊列中所有dist值的平均值爲x,若dist(i)>x則將i插入到隊尾,查找下一元素,直到找到某一i使得dist(i)<=x,則將i出對進行鬆弛操作。
代碼:

/*
FILE:spfa_bfs_slf_lll.cpp
LANG:C++
*/
#include <iostream>
#include <cstring>
#include <deque>
using namespace std;

const int node_num = 100;
const int INF = 2147483647;

int matrix[node_num][node_num], dist[node_num], path[node_num];
bool vis[node_num];
int v_num, a_num, sum = 0, num = 1;    //sum記錄隊列中所有節點的dist之和,num記錄隊列中節點數

bool spfa_bfs_slf_lll(const int);

int main()
{
    cout << "v_num:";
    cin >> v_num;
    cout << "a_num:";
    cin >> a_num;

    for (int i = 0; i < v_num; ++i)
    {
        for (int j = 0; j < v_num; ++j)
        {
            matrix[i][j] = ((i != j) ? INF : 0);
        }
    }

    int u, v, w;
    for (int i = 0; i < a_num; ++i)
    {
        cin >> u >> v >> w;
        matrix[u][v] = matrix[v][u] = w;
    }

    int src;
    cout << "source:";
    cin >> src;

    if (spfa_bfs_slf_lll(src))
    {
        cout << "E" << endl;
        return 0;
    }

    for (int i = 0; i < v_num; ++i)
    {
        if (i == src)
        {
            continue;
        }
        cout << src << "->" << i << ":" << dist[i] << ":" << i;
        int t = path[i];
        while (t != src)
        {
            cout << "-" << t;
            t = path[t];
        }
        cout << "-" << src << endl;
    }
    return 0;
}

bool spfa_bfs_slf_lll(const int src)
{
    memset(vis, false, sizeof(vis));
    deque<int> q;
    int cnt[node_num] = {0};
    for (int i = 0; i < v_num; ++i)
    {
        dist[i] = INF;
        path[i] = src;
    }
    dist[src] = 0;
    q.push_back(src);
    vis[src] = true;
    ++cnt[src];
    while (!q.empty())
    {
        int x = q.front();
        q.pop_front();
        if (dist[x] * num > sum)    //LLL策略
        {
            q.push_back(x);
            continue;
        }
        vis[x] = false;
        sum -= dist[x];
        --num;

        for (int i = 0; i < v_num; ++i)
        {
            if (matrix[x][i] != INF && dist[x] + matrix[x][i] < dist[i])
            {
                dist[i] = dist[x] + matrix[x][i];
                path[i] = x;
                if (!vis[i])
                {
                    vis[i] = true;
                    if (dist[i] < dist[q.front()])    //SLF策略
                    {
                        q.push_front(i);
                    }
                    else
                    {
                        q.push_back(i);
                    }
                    sum += dist[i];
                    ++num;
                    ++cnt[i];
                    if (cnt[i] >= v_num)
                    {
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

參考

  1. SPFA算法——最短路徑
  2. SPFA 算法詳解( 強大圖解,不會都難!)
  3. 單源最短路徑(3):SPFA 算法
  4. SPFA算法
  5. SPFA的兩種優化SLF和LLL
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章