---
# System prepended metadata

title: Bellman-Ford Algorithm
tags: [內湖高中程式競賽]

---

# Bellman-Ford Algorithm
Bellman-Ford 演算法算是比較早期的演算法，而最短路徑問題基本上發展到 Dijkstra 就已經是最佳解了。我們至今還是會用 Bellman-Ford 演算法是因為他可以處理我們無法處理的兩個問題 : 負權邊、找負環

負權邊的問題[上次](https://hackmd.io/@ShanC/Dijkstra)講過了，最短路徑問題[這篇](https://hackmd.io/@ShanC/Shortest-Path)也大概聊過。來說說優點好了，Bellman-Ford 演算法因為本質上是走 walk，而不是 simple path，只要還能鬆弛就能繼續 walk 下去，因此可以幫我們測環

## 邊陣列 Edge Array
這個東西我在[這篇](https://hackmd.io/@ShanC/basic-graph-theory-4)有講過了，但這邊才正式用到，因此再說一次

### 無權邊
```cpp=
pair<int, int> edges[MXN];   // 這裡儲存邊的兩個終點
for(int i = 0; i < m; i++){  // 輸入可以這樣處理
    cin >> edges[i].first >> edges[i].second;
}
```
### 帶權邊
```cpp=
struct Edge{                 // 儲存兩終點 u, v 與邊的權重 w
    int u, v, w;
}edge[MXN];
for(int i = 0; i < m; i++){  // 輸入可以這樣處理
    cin >> edge[i].u >> edge[i].v >> edge[i].w;
}
```

## 鬆弛 Relax
由 $\delta$ 的定義，設一起點 $s$，對於任何邊 $(u, v)$ 而言，必須符合三角不等式 : 
$$
\delta(s, v)\leq \delta(s, u) + w(u, v)
$$

在單源最短路徑的演算法中，若我們搜尋到的邊不符合三角不等式，我們就需要把它替換掉，就如同我們求最小值的技巧一樣

```cpp
/* 此為 Pseudocode */
/* d 存兩點之間的最短距離, w 存邊權 */
Relax(u, v, w)
    if d[s][u] > d[u][v] + w[u][v] :
        d[s][u] = d[u][v] + w[u][v]
```

這個東西是最短路徑的精華

## Bellman-Ford 演算法

### 想法
給一張無負環的圖 $G=(V, E)$、一個邊權函數 $w: E\to \mathbb{Z}$ 與一個起點 $s\in V$。Bellman-Ford 主要的想法是利用鬆弛的機制，每一輪將每條邊都鬆弛。只要鬆弛 $|V|-1$ 輪，那麼距離陣列 $\text{dis}[~]$ 紀錄的就會是最短距離

### 範例

給定以下這張圖，除了起點以外，其他點的 `d[]` 都初始化為 $\infty$

<center>

![shapes at 26-01-29 09.12.17](https://hackmd.io/_uploads/BkeRVNdLbg.png)

</center>

從圖中可以發現 $\infty\to\infty$ 是不可能被鬆弛到的，因為都是無限大。僅有從起點出發的邊可以被鬆弛到
<center>
    
![shapes at 26-01-29 09.12.24](https://hackmd.io/_uploads/SyQxBVu8-x.png)

</center>

接下來注意到右邊那個點，他會被兩條邊鬆弛，最後結果為 $\min\{2+4,4+2,\infty\}=6$。而下面的點在這輪也可以被鬆弛，值為 $\min\{4,2-1\}=1$
<center>

![shapes at 26-01-29 09.22.30](https://hackmd.io/_uploads/HJ1F8VuUWe.png)

</center>

最尾端的點接下來也可以被鬆弛為 $11$。然後會發現因為 $\min\{1+2, 6\}=3$，所以右邊那個點可以被鬆弛

<center>

![shapes at 26-01-29 09.22.36](https://hackmd.io/_uploads/H1nVPEd8Zl.png)


</center>

由於數值有改變的關係，最後尾端那個點也可以被鬆弛變成 $\min\{11, 3+5\}=8$

<center>

![shapes at 26-01-29 09.22.40](https://hackmd.io/_uploads/ByKSvE_L-g.png)


</center>

會發現第 $4$ 輪之後，已經沒有可以鬆弛的邊了

這裡要稍微提醒一下，在例子中，每次鬆弛都是以上一個階段的結果去計算

### 維護最短路徑
這裡 $n=|V|$、$m=|E|$。設第 $t$ 輪鬆弛後，從 $s$ 到其他點 $v$ 的最短距離為 $\text{dis}[t][v]$。那麼其實可以當作 DP 轉移式:

$$
\text{dis}[t][v]=
\begin{cases}
\infty &\text{, if } t=0 \text{ and } v\neq s\\
0 &\text{, if } t=0 \text{ and } v=s\\
\min\limits_{(u,v)\in E}\{\text{dis}[t-1][u]+w(u,v)\} &\text{, if } t\ge 1
\end{cases}
$$

### 程式實作 1
根據以上的定義與想法，可以得到以下程式碼:
```cpp=
for (int t = 0; t < n; t++) {
    for (int i = ; i < m; i++) {
        dis[t][i] = INF; // 很大的數字
    }
}
dis[0][s] = 0;

for (int t = 1; t < n; t++){ // 注意: 這邊只需要跑 n-1 次，而不是 n 次
    for (int i = 0; i < m; i++){
        int u = edge[i].u, v = edge[i].v, w = edge[i].w;
        if (dis[t - 1][u] + w < dis[t][v])
            dis[t][v] = dis[t - 1][u] + w;
    }
}
```
- 時間: $O(nm)$
- 空間: $O(nm)$

### 程式實作 2
由於當我們跑到第 $t$ 輪時，實際會取用到的資料只有第 $t-1$ 輪的資料，前面 $1\sim t-2$ 都用不到，所以其實可以運用 DP 的 rolling 技巧將空間降一個維度

```cpp=
int dis[2][MXN];
for (int i = 0; i < n; i++) 
    dis[0][i] = dis[1][i] = INF;
dis[0][s] = 0;

for (int t = 1; t < n; t++) {
    int cur = t % 2;
    int prev = (t - 1) % 2;
    
    // 先繼承上一輪的最短路徑結果
    for (int i = 0; i < n; i++) 
        dis[cur][i] = dis[prev][i];
    
    for (int i = 0; i < m; i++) {
        int u = edge[i].u, v = edge[i].v, w = edge[i].w;
        if (dis[prev][u] + w < dis[cur][v]) {
            dis[cur][v] = dis[prev][u] + w;
        }
    }
}
```
- 時間: $O(nm)$
- 空間: $O(2n)$

### 程式實作 3
其實我們可以通通寫在同一個一維陣列當中。這樣連續覆寫可能會造成連續走很多步，但是這頂多讓某些節點更快速的找到最佳解，所以沒差
```cpp=
for (int t = 1; t < n; t++){ // 注意: 這邊只需要跑 n-1 次，而不是 n 次
    for (int i = 0; i < m; i++){
        int u = edge[i].u, v = edge[i].v, w = edge[i].w;
        if (dis[u] + w < dis[v])
            dis[v] = dis[u] + w;
    }
}
```

- 時間: $O(nm)$
- 空間: $O(n)$

這種寫法才是真正會用到的程式碼

### 優化
有一種可能就是這張圖其實在第 $n - 1$ 輪鬆弛之前就已經完全鬆弛完。如果真發生這種事，那麼就有好幾輪是浪費的。因此我們可以設一個變數 `relax = true`，來判斷該輪是否還有邊還在鬆弛

```cpp=
bool relax = true;
for(int t = 1; t < n && relax; t++){ 
    relax = false;
    for (int i = 0; i < m; i++){
        int u = edge[i].u, v = edge[i].v, w = edge[i].w;
        if (dis[u] + w < dis[v]) {
            dis[v] = dis[u] + w;
            relax = true;
        }
    }
}
```

雖然最差情況下時間複雜度相同，但是在某些 case 確實可以加速一下

## 測負環 Negative Cycle Detection

### 想法
不難觀察到，如果存在負環，那麼就可以無限地鬆弛下去，屆時所有距離都是 $-\infty$。這顯然不是我們想要到，因此要找出負環

<center>

![image](https://hackmd.io/_uploads/rk4ZoH_IZl.png =200x)

</center>

根據最短路徑樹的性質，找出所有點的最短路徑只需要 $n-1$ 輪。若是到第 $n$ 輪還能鬆弛，就到表有環

### 程式實作
用一個變數 `negcycle` 來檢查即可
```cpp=
for (int t = 1; t < n; t++){
    for (int i = 0; i < m; i++){
        int u = edge[i].u, v = edge[i].v, w = edge[i].w;
        if (dis[u] + w < dis[v])
            dis[v] = dis[u] + w;
    }
}

bool negcycle = false;       // 測負環
for (int i = 0; i < m; i++){ // 第 n 輪鬆弛
    int u = edge[i].u, v = edge[i].v, w = edge[i].w;
    if (dis[u] + w < dis[v]) // 還能鬆弛
        negcycle = true;
}
```

## 備註
- 雖然第 $n$ 輪鬆弛代表存在負環，但是究竟是哪邊有負環，我們並不知道，需要額外再處理。如果是不存在負環的圖，那麼每條邊被鬆弛達到最短後就不會再鬆弛。但如果存在負環的話，在我們鬆弛到負環之後，該負環與後面的節點都會一直被鬆弛
- Bellman-Ford 演算法可以處理的問題是沒有負環的最短路徑
- Bellman-Ford 演算法允許負權邊
- 好像有很多人在差不多同一個時代提出差不多的演算法，但不知道為什麼最後是以 Bellman 跟 Ford 兩人命名

## 題目練習
[CSES Cycle Finding](https://cses.fi/problemset/task/1197)
[CSES High Score](https://cses.fi/problemset/task/1673)
[AtCoder Beginner Contest 061D - Score Attack](https://atcoder.jp/contests/abc061/tasks/abc061_d?lang=en)

----

## 參考資料
- Introduction to Algorithms, Fourth Edition
- [海大 I2CP - Shortest Path](https://hackmd.io/@LeeShoWhaodian/HyT4ib5qJg#/)
- [CP-Algorithms - Bellman-Ford Algorithm](https://cp-algorithms.com/graph/bellman_ford.html)
- [FDHSCPP110 - 最短路徑 shortest path](https://hackmd.io/@fdhscpp110/shortest_path)
- [Aaron - Bellman-Ford algorithm](https://medium.com/algorithm-solving/bellman-ford-algorithm-3cd327630e2d)
- [Wikipedia - Bellman–Ford algorithm](https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm) (這裡的範例gif檔還蠻不錯的，可以看看)

----

> [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC)
> 作者: ShanC
> 更新: 2026/1/29