最短路徑——Bellman-Ford算法


轉自:https://www.cnblogs.com/xzxl/p/7232929.html

一、相關定義

最短路徑:求源點到某特定點的最短距離

特點:Bellman-Ford算法主要是針對有負權值的圖,來判斷該圖中是否有負權回路或者存在最短路徑的點

局限性:算法效率不高,不如SPFA算法

時間復雜度:O(mn)

具體與dijkstra算法的比較

Bellman-Ford算法為何需要循環n-1次來求解最短路徑?Dijkstra從源點開始,更新dis[],找到最小值,再更新dis[]……每次循環都可以確定一個點的最短路。Bellman-Ford算法同樣也是這樣,它的每次循環也可以確定一個點的最短路,只不過代價很大,因為 Bellman-Ford每次循環都是操作所有邊。既然代價這么大,相比Dijkstra 算法,Bellman-Ford算法還有啥用?因為后者可以檢測負權回路啊。Bellman-Ford 算法的時間復雜度為 O(nm),其中 n 為頂點數,m 為邊數。

負權回路

開始不懂,看了下面的圖和我的算法描述后就懂了:

img

img

在循環n-1次的基礎上再次遍歷各邊,對於所有邊,只要存在一條邊e(u, v)使得 dis[u] + w(u,v) < dis[v],則該圖存在負權回路。

【松弛操作】

imgimg

如左圖所示,松弛計算之前,點B的值是8,但是點A的值加上邊上的權重2,得到5,比點B的值(8)小,所以,點B的值減小為5。這個過程的意義是,找到了一條通向B點更短的路線,且該路線是先經過點A,然后通過權重為2的邊,到達點B。
當然,如果出現右邊這種情況,則不會修改點B的值,因為3+4>6。

二、算法描述

關鍵詞:初始化 松弛操作

主要變量如下:

int n    表示有n個點,從1~n標號

int s,t   s為源點,t為終點

int dis[N]  dis[i]表示源點s到點i的最短路徑

int pre[N]  記錄路徑,pre[i]表示i的前驅結點

bool vis[N] vis[i]=true表示點i被標記

初始化

dis數組全部賦值為INF,pre數組全部賦值為-1(表示還不知道前驅),

dis[s] = 0 表示源點不要求最短路徑(或者最短路徑就是0)。

松弛操作

對於每一條邊e(u, v),如果dis[u] + w(u, v) < dis[v],則另dis[v] = dis[u]+w(u, v)。w(u, v)為邊e(u,v)的權值

注:上述循環執行至多n-1次。若上述操作沒有對Distant進行更新,說明最短路徑已經查找完畢或者部分點不可達,跳出循環;否則執行下次循環。

檢測負權回路

為了檢測圖中是否存在負環路,即權值之和小於0的環路。

檢測的方法很簡單,只需在求解最短路徑的 n-1 次循環基礎上,再進行第 n 次循環:對於每一條邊e(u, v),如果存在邊使得dis[u] + w(u, v) < dis[v],則圖中存在負環路,即是說改圖無法求出單源最短路徑。否則數組Distant[n]中記錄的就是源點s到各頂點的最短路徑長度。

小結

Bellman-Ford算法可以大致分為三個部分:

  1. 初始化所有點。每一個點保存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
  2. 進行循環,循環下標為從1到n-1(n等於圖中點的個數)。在循環內部,遍歷所有的邊,進行松弛計算。
  3. 遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況: d(v) > d (u) + w(u,v),若存在,則返回false,表示圖中存在從源點可達的權為負的回路。

之所以需要第三步的原因,是因為,如果存在從源點可達的權為負的回路,則將因為無法收斂而導致不能求出最短路徑。

三、代碼實現

#include<iostream>   
#include<stack> 
using namespace std;
 
const int MAX = 10000;  //假設權值最大不超過10000
 
struct Edge
{
    int u;
    int v;
    int w;
};
 
Edge edge[10000];  //記錄所有邊
int dist[100];     //源點到頂點i的最短距離
int path[100];     //記錄最短路的路徑
int vertex_num;    //頂點數
int edge_num;      //邊數
int source;        //源點 
 
bool BellmanFord()
{
    //初始化
    for (int i = 0; i < vertex_num; i++)
        dist[i] = (i == source) ? 0 : MAX;
 
    //n-1次循環求最短路徑
    for (int i = 1; i <= vertex_num - 1; i++)
    {
        for (int j = 0; j < edge_num; j++)
        {
            if (dist[edge[j].v] > dist[edge[j].u] + edge[j].w)
            {
                dist[edge[j].v] = dist[edge[j].u] + edge[j].w;
                path[edge[j].v] = edge[j].u;
            }
        }
    }
 
    bool flag = true;  //標記是否有負權回路
 
    //第n次循環判斷負權回路
    for (int i = 0; i < edge_num; i++) 
    {
        if (dist[edge[i].v] > dist[edge[i].u] + edge[i].w)
        {
            flag = false;
            break;
        }
    }
 
    return flag;
}
 
void Print()
{
    for (int i = 0; i < vertex_num; i++)
    {
        if (i != source)
        {
            int p = i;
            stack<int> s;
            cout << "頂點 " << source << " 到頂點 " << p << " 的最短路徑是: ";
 
            while (source != p)  //路徑順序是逆向的,所以先保存到棧
            {
                s.push(p);
                p = path[p];
            }
 
            cout << source;
            while (!s.empty())  //依次從棧中取出的才是正序路徑
            {
                cout << "--" << s.top();
                s.pop();
            }
            cout << "    最短路徑長度是:" << dist[i] << endl;
        }
 
    }
}
 
int main()
{
 
    cout << "請輸入圖的頂點數,邊數,源點:";
    cin >> vertex_num >> edge_num >> source;
 
    cout << "請輸入" << edge_num << "條邊的信息:\n";
    for (int i = 0; i < edge_num; i++)
        cin >> edge[i].u >> edge[i].v >> edge[i].w;
 
    if (BellmanFord())
        Print();
    else
        cout << "Sorry,it have negative circle!\n";
 
    return 0;
}

四、實戰

https://acm.ecnu.edu.cn/problem/1817/

求出有 n (1<n<600) 個結點有向圖中,結點 1 到結點 n 的最短路徑。

輸入格式

第一行有 2 個整數 n,m (0<m≤n(n−1)2),接下來 m 行每行有三個整數 u,v,w 結點 u 到 v 有一條權為 w 的邊 (w<106)。

輸出格式

輸出結點 1 到結點 n 之間的最短路徑,如果 1 到 n 之間不存在路徑,輸出 −1。

樣例

input

3 3
1 2 10
2 3 15
1 3 30

output

25

AC代碼

#include<iostream>
#include<vector>
using namespace std;

/**
 * @brief Bellman-Ford單源最短路徑算法
 * 
 * @return int 
 */

const int MAXN = 610;
const int MAXM = MAXN * MAXN;
const int INF = 1e6 + 10;

struct Edge{
    int from, to;
    int length;
    Edge(int f, int t, int l): from(f), to(t), length(l) {}
};

vector<Edge> edges;
int dis[MAXN];

void Bellman_Ford(int start, int n, int m){
    //初始化
    for(int i = 1; i <= n; ++i){
        dis[i] = INF;
    }
    dis[start] = 0;

    //循環n-1次
    for(int i = 1; i < n; ++i){
        //對每一條邊進行松弛操作
        for(int j = 0; j < m; ++j){
            int from = edges[j].from;
            int to = edges[j].to;
            int d = edges[j].length;
            if(dis[from] + d < dis[to]){
                dis[to] = dis[from] + d;
            }
        }
    }

    //判斷是否存在負回路(再來一次遍歷)
    /*for(int j = 0; j < m; ++j){
        int from = edges[j].from;
        int to = edges[j].to;
        int d = edges[j].length;
        if(dis[from] + d < dis[to]){
            return 0;
        }
    }
    return 1;*/
}

int main(){
    int n, m;
    int from, to, length;
    scanf("%d%d", &n, &m);
    
    for(int i = 0; i < m; ++i){
        scanf("%d%d%d", &from, &to, &length);
        edges.push_back(Edge(from, to, length));
    }

    Bellman_Ford(1, n, m);
    if(dis[n] == INF){
        printf("-1\n");
    }
    else{
        printf("%d\n", dis[n]);
    }
    

    //system("pause");
    return 0;
}

五、遇到的坑

之前筆者學習別的單源最短路徑代碼的時候,了解到可將INF定義為INT_MAX,即如下代碼

#include<climits>

const int INF = INT_MAX;

但是在提交題解時最后一個測試點通不過,查看測試數據input並進行測試,發現輸出為-2147483575這個很離譜的結果。在將INF的定義改為如下形式,則順利通過最后一個測試點。

const int INF = 1e6 + 10;

分析得出結論如下:

采用方案1,即INF = INT_MAX,當遇到dis[from]為INF的情況是,dis[from] + d超過INF,即超過int型整數所能表示的最大整數,也就是溢出了,則此時dis[from] + d的值變為等於或者接近INT_MIN,即int型整數所能表示的最小整數。此時,dis[to] > INT_MIN也就是 if判斷條件必定滿足,進而dis[to]就被賦值為一個接近或者等於INT_MIN的值,bug就由此產生。可能會導致終點的dis始終為接近或等於INT_MIN而得不到更新,也就是筆者測試時出現的-2147483575這樣離譜的結果

用下面的列子說明一下:

image-20220127112415950
input
3 3
2 3 15
1 2 10
1 3 30
1 2 3
Init 0 inf inf
round1 edge1 0 inf -inf
round1 edge2 0 10 -inf
round1 edge3 0 10 -inf
round2 edge1 0 10 -inf
round2 edge2 0 10 -inf
round2 edge3 0 10 -inf

知道了哪里出現問題,修改就很容易了,只需要在if條件判斷即可

if(dis[from] != INF && dis[from] + d < dis[v])

而定義為INF = 1e6 + 10,則不會出現這樣的上述bug,原因是即便是dis[from]=INF,只要dis[from] + d沒有超過int型變量所能表示的最大整數即可。但是此值需要根據題目描述來確定。

六、改進

《挑戰程序設計競賽》中關於Bellman Ford算法的代碼如下:

若在n-1次的循環中,某一次遍歷所有的邊后,並沒有更新dis,則算法運行結束

#include<iostream>
#include<vector>
#include<string.h>
#include<climits>
using namespace std;

/**
 * @brief Bellman-Ford單源最短路徑算法
 * 
 * @return int 
 */

const int MAXN = 610;
const int MAXM = MAXN * MAXN;
const int INF = INT_MAX;

struct Edge{
    int from, to;
    int length;
    Edge(int f, int t, int l): from(f), to(t), length(l) {}
};

vector<Edge> edges;
int dis[MAXN];

void Bellman_Ford(int start, int n, int m){
    //初始化
    for(int i = 1; i <= n; ++i){
        dis[i] = INF;
    }
    dis[start] = 0;

    while(true){
        //update標記此趟遍歷是否有更新
        bool update = false;
        for(int i = 0; i < m; ++i){
            Edge e = edges[i];
            if(dis[e.from] != INF && dis[e.from] + e.length < dis[e.to]){
                dis[e.to] = dis[e.from] + e.length;
                update = true;
            }
        }
        if(update == false){	//若此趟沒有更新則算法結束
            break;
        }
    }
}

bool find_negative_loop(int n, int m){
    memset(dis, 0, sizeof(dis));
    for(int i = 0; i < n; ++i){
        for(int j = 0; j < m; ++j){
            Edge e = edges[j];
            if(dis[e.from] + e.length < dis[e.to]){
                dis[e.to] = dis[e.from] + e.length;
                if(i == n - 1){
                    return false;
                }
            }
        }
    }
    return true;
}

int main(){
    int n, m;
    int from, to, length;
    scanf("%d%d", &n, &m);
    
    for(int i = 0; i < m; ++i){
        scanf("%d%d%d", &from, &to, &length);
        edges.push_back(Edge(from, to, length));
    }

    Bellman_Ford(1, n, m);
    if(dis[n] == INF){
        printf("-1\n");
    }
    else{
        printf("%d\n", dis[n]);
    }
    

    system("pause");
    return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM