<style> .reveal .slides { text-align: left; font-size:35px; } </style> # Shortest Path ## 最短路徑 Introduction to Competitive Programming 3/12 I2CP ---- ## Table of Contents - 複習: 圖論名詞 - 複習: 帶權邊存圖技巧 - 最短路徑問題 - Dijkstra's Algorithm - Bellman-Ford Algorithm - Floyd-Warshall Algorithm --- ### 複習: 圖論名詞 <div style="position: absolute; right: 0%; top:0%;"> ![](https://hackmd.io/_uploads/H1UbYU99p.png =400x) </div> <div style="position: absolute; left: 0%; top:100%;"> - {圖|graph}: $G=(V, E)$ - {節點|vertex}: $v\in V$ 又稱「點」 - {邊|edge}: $e \in E$ 又分為「有向邊」、「無向邊」 - {邊權|weight}: $w: E\to\mathbb{Z}$ 又稱為「花費」(cost) </div> ---- <div style="position: absolute; left: 15%; top:60%;"> ![image](https://hackmd.io/_uploads/Hyb8UU9qp.png =700x) </div> <div style="text-align: center;">圖也有分有向圖和無向圖</div> ---- ### 環 (Cycle) <div style="position: absolute; left: 15%; top:60%;"> ![image](https://hackmd.io/_uploads/H1fflNksT.png) </div> --- ## 複習: 帶權邊存圖技巧 - 鄰接矩陣 - 鄰接串列 - 邊陣列 ---- ### 鄰接矩陣 <div style="position: absolute; right: 0%; top:90%;"> ![](https://hackmd.io/_uploads/H1UbYU99p.png =400x) </div> <div style="position: absolute; left: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/Sk9MiIcqp.png =450x) </div> $5\leftrightarrow 6$ 花費為 $9$,表示為: $\text{matrix}[5][6] = \text{matrix}[6][5] = 9$ ---- ### 鄰接矩陣--存圖 - 有向 ```cpp! int dis[N][N]; void add_edge(int u,int v,int w){ // 起點 u、終點 v、權重 w dis[u][v] = w; } ``` - 無向 ```cpp! int dis[N][N]; void add_edge(int u,int v,int w){ // 連接 u 和 v、權重 w dis[u][v] = w; dis[v][u] = w; } ``` ---- ### 鄰接串列 $1 \to 3$ 權重為 $5$,表示為 $1 : \{3, 5\}$ <div style="position: absolute; right: 5%; top:80%;"> ![](https://hackmd.io/_uploads/H1UbYU99p.png =390x) </div> <div style="position: absolute; left: 0%; top:80%;"> ![image](https://hackmd.io/_uploads/BytUT8996.png =430x) </div> ---- ### 鄰接串列--存圖 - 有向 ```cpp! vector<pair<int,int>>graph[N]; // pair 存 {終點,權重} void add_edge(int u,int v,int w){ // 起點 u、終點 v、權重 w graph[u].push_back({v,w}); } ``` - 無向 ```cpp! vector<pair<int,int>>graph[N]; // pair 存 {終點,權重} void add_edge(int u,int v,int w){ // 連接 u 和 v、權重 w graph[u].push_back({v,w}); graph[v].push_back({u,w}); } ``` ---- ### 邊陣列 ```cpp= struct Edge{ int u, v, w; }edge[100005]; for(int i = 0; i < m; i++){ cin >> edge[i].u >> edge[i].v >> edge[i].w; } ``` --- ## {最短路徑問題|Shortest Path Problem} 通常會有一張圖, 要你算從起點 $s$ 走到終點 $t$ 所需的最小花費 (權重) 總和 ---- $4\to 3\to 5$ 的花費是 $1+3=4$ $2\to 5\to 3$ 的花費是 $4+3=7$ ![image](https://hackmd.io/_uploads/ByHKSLqca.png =500x) ---- $4\to 3\to 5\to 1$ 的花費是 $1+3+1=5$ (最短路徑) $4\to 3\to 1$ 的花費是 $1+5=6$ ![image](https://hackmd.io/_uploads/ByHKSLqca.png =500x) ---- 同一起點、終點之間可能會有多條路徑, 最短路徑就是其中權重和 (花費) 最小的路徑 ---- 如果存在負環那麼就可以走到{無限小的花費|可能不存在最短路徑} ![image](https://hackmd.io/_uploads/BJ0swLcqp.png =600x) 有沒有負環也是最短路徑問題須考慮的一點 ---- 在最短路徑這個演算法上我們要對應不同的圖 使用合適的演算法 ---- ### 正式定義{花費|Cost} 給一個加權的圖 $G=(V,E)$,權重函數 $w:E\to\mathbb{Z}$ 路徑 $p=\langle v_0,\cdots,v_{k}\rangle$ 的花費為: $$ w(p)=\sum_{i=1}^{k} w(v_{i-1},v_i) $$ ---- ### 正式定義{最短路徑|Shortest Path} 從 $u$ 到 $v$ 最小花費為: $$ \delta(u,v)= \begin{cases} &\min\{w(p): u\sim^p v\}&\text{if 存在路徑從 u 到 v}\\ &\infty&\text{if 沒有}\\ \end{cases} $$ 從 $u$ 到 $v$ 的**最短路徑**是滿足 $w(p)=\delta(u,v)$ 的**任何路徑** $p$ ---- ### 最短路徑分類 - {單源|Single Source}{最短路徑|Shortest Path} (固定起點、不固定終點) - {全點對|All-pairs}{最短路徑|Shortest Path} (不固定起點、不固定終點) ---- ### 單源最短路徑 給你一個圖,一個起點 $s$, 求出這個起點到每個點 $v$ 的最短路徑 $\delta(s, v)$ <div style="position: absolute; left: 28%; top:80%;"> ![](https://hackmd.io/_uploads/SJvzxGCqp.png =500x) </div> ---- ### 全點對最短路徑 給你一個圖,多組起點和終點 $(s_i, t_i)$, 求出每組起點到終點的最短路徑 $\delta(s_i, t_i)$ <div style="position: absolute; left: 20%; top:80%;"> ![image](https://hackmd.io/_uploads/SJfkEf0ca.png =600x) </div> ---- ### 演算法們 - 單源最短路徑 - Dijkstra - Bellman-Ford - 全點對最短路徑 - Floyd-warshall --- ## 最短路徑的性質 ---- ### {三角不等式|Triangle Inequality} 給定起點 $s$ 對於任意邊 $(u,v)\in E$, $$ \delta(s,v)\le \delta(s,u)+w(u,v) $$ ---- 在單源最短路徑中,會先創一個陣列 $\text{dis}[]$,起點設 $s$ 一開始會把 $\text{dis}[s]$ 設為 $0$,代表起點到自己的距離是 $0$ 然後把到其他點 $u$ 的值 $\text{dis}[u]$ 設為 $\infty$ ---- ### {鬆弛|Relax} 假設現在走到 $u$,如果存在一個邊 $u\to v$,使得 $$ \text{dis}[v] > \text{dis}[u] + w(u, v) $$ 那麼我們可以更新 $\text{dis}[v] = \text{dis}[u] + w(u, v)$ 這樣子我們稱透過 $(u,v)$ 進行了一次鬆弛 <!-- .element: class="fragment" data-fragment-index="1" --> ---- 假設起點 $s=2$,$\text{dis}[3] = 5$, 由於 $\text{dis}[3] > \text{dis}[1] + w(1, 3),\quad (\because 5 > 4)$, 我們更新 $\text{dis}[3] = 4$ ![image](https://hackmd.io/_uploads/SkCsXNRca.png) ---- 假設現在已經沒有邊給 $\text{dis}[~]$ 鬆弛了 比較一下三角不等式與最終的{陣列|起點為$s$的} $\text{dis}[~]$: $$ \begin{split} &\text{三角不等式: }&\delta(s,v)&\le \delta(s,u)+w(u,v)\\ &\text{陣列: }&\text{dis}[v] &\le \text{dis}[u] + w(u, v) \end{split} $$ 我們做鬆弛的目的, 其實是讓 $\text{dis}[~]$ 陣列最終滿足三角不等式 <!-- .element: class="fragment" data-fragment-index="1" --> --- ## Dijkstra's Algorithm ---- ### 想法 起點設 $s$,定義陣列 $\text{dis}[~]$: $$ \text{dis}[i]= \begin{cases} 0\quad & \text{if }i=s\\ \infty & \text{otherwise}\\ \end{cases} $$ 重複做下面的步驟: 1. 選一個離起點最近並且沒有走過的點 $u$ 2. 把點 $u$ 設定為走過 3. 透過所有連接點 $u$ 和 $v$ 的邊,去鬆弛點 $v$ ---- 學會鬆弛之後我們就可以看看 Dijkstra 在做什麼了 假設一開始起點 $s=2$,要算下圖的最短距離 ![image](https://hackmd.io/_uploads/ByUNwERcT.png =600x) ---- ### 初始化 先把dis初始化,把起點設為0,其他設為INF(很大的數字) <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/ByE7D4Aqp.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | INF | 0 | | 2 | | 0 | 0 | | 3 | | INF | 0 | | 4 | | INF | 0 | | 5 | | INF | 0 | | 6 | | INF | 0 | </div> ---- 1. <span style="color: red">選一個離起點最近並且沒有走過的點$u$ $\to$ 選 $2$</span> 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/BkXvoV0qa.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | INF | 0 | | 2 | | 0 | 0 | | 3 | | INF | 0 | | 4 | | INF | 0 | | 5 | | INF | 0 | | 6 | | INF | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. <span style="color: red">把點$u$設定為走過 $\to$ $\text{visited}[2]=1$</span> 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/BkXvoV0qa.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | INF | 0 | | 2 | | 0 |<span style="color: red">1</span>| | 3 | | INF | 0 | | 4 | | INF | 0 | | 5 | | INF | 0 | | 6 | | INF | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. <span style="color: red">透過所有連接點$u$和$v$的邊,去鬆弛點$v$</span> <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rkMcjN05T.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | INF | 0 | | 2 | | 0 |<span style="color: red">1</span> | | 3 | | INF | 0 | | 4 | | ~~INF~~ <span style="color: red">1</span> | 0 | | 5 | | INF | 0 | | 6 | | ~~INF~~ <span style="color: red">7</span> | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/Hy7JaEA96.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | INF | 0 | | 2 | | 0 |<span style="color: red">1</span>| | 3 | | INF | 0 | | 4 | | 1 | 0 | | 5 | | INF | 0 | | 6 | | 7 | 0 | </div> ---- 1. <span style="color: red">選一個離起點最近並且沒有走過的點$u$ $\to$ 選4</span> 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rkQ7aVC96.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | INF | 0 | | 2 | | 0 |<span style="color: red">1</span>| | 3 | | INF | 0 | | 4 | | 1 | 0 | | 5 | | INF | 0 | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. <span style="color: red">把點$u$設定為走過 $\to \text{visited}[4]=1$</span> 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rkQ7aVC96.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | INF | 0 | | 2 | | 0 |<span style="color: red">1</span> | | 3 | | INF | 0 | | 4 | | 1 |<span style="color: red">1</span> | | 5 | | INF | 0 | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. <span style="color: red">透過所有連接點$u$和$v$的邊,去鬆弛點$v$</span> <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rkmIANRc6.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | ~~INF~~ <span style="color: red">5</span> | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | ~~INF~~ <span style="color: red">2</span> | 0 | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | ~~INF~~ <span style="color: red">5</span>| 0 | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/Hyn9R4Rca.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | 0 | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 5 | 0 | | 6 | | 7 | 0 | </div> ---- 1. <span style="color: red">選一個離起點最近並且沒有走過的點$u$ $\to$ 選$3$</span> 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/HJqU1HAcp.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | 0 | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 5 | 0 | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. <span style="color: red">把點$u$設定為走過 $\to$ $\text{visited}[3]=1$</span> 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/HJqU1HAcp.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 5 | 0 | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. <span style="color: red">透過所有連接點$u$和$v$的邊,去鬆弛點$v$</span> <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/S1pQeB0cp.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | ~~5~~ <span style="color: red">4</span> | 0 | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rJ6kbHC5p.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | 0 | | 6 | | 7 | 0 | </div> ---- 1. <span style="color: red">選一個離起點最近並且沒有走過的點$u\to$ 選$5$</span> 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rkD4ZSRca.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | 0 | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. <span style="color: red">把點u設定為走過 $\to \text{visited}[5]=1$</span> 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rkD4ZSRca.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. <span style="color: red">透過所有連接點$u$和$v$的邊,去鬆弛點$v$</span> <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/Bkx2-r0cp.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/r1O6GB0qa.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | 0 | </div> ---- 1. <span style="color: red">選一個離起點最近並且沒有走過的點$u$ $\to$ 選$1$</span> 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rkEIXr0cT.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | 0 | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. <span style="color: red">把點u設定為走過 $\to \text{visited}[1]=1$</span> 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rkEIXr0cT.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | <span style="color: red">1</span> | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. <span style="color: red">透過所有連接點$u$和$v$的邊,去鬆弛點$v$</span> <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rkEIXr0cT.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | <span style="color: red">1</span> | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | 0 | </div> ---- 1. <span style="color: red">選一個離起點最近並且沒有走過的點$u$ $\to$ 選$6$</span> 2. 把點$u$設定為走過 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rJqWEBA56.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | <span style="color: red">1</span> | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | 0 | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. <span style="color: red">把點$u$設定為走過 $\to$ $\text{visited}[1]=1$</span> 3. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rJqWEBA56.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | <span style="color: red">1</span> | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | <span style="color: red">1</span> | </div> ---- 1. 選一個離起點最近並且沒有走過的點$u$ 2. 把點$u$設定為走過 3. <span style="color: red">透過所有連接點$u$和$v$的邊,去鬆弛點$v$</span> <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/rJqWEBA56.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | <span style="color: red">1</span> | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | <span style="color: red">1</span> | </div> ---- 這樣就走完ㄌ <div style="position: absolute; right: 0%; top:90%;"> ![image](https://hackmd.io/_uploads/HyL_ES0qp.png =500x) </div> <div style="position: absolute; left: 0%; top:90%;"> | | | dis | visited | | --- | --- | --- | --- | | 1 | | 5 | <span style="color: red">1</span> | | 2 | | 0 | <span style="color: red">1</span> | | 3 | | 2 | <span style="color: red">1</span> | | 4 | | 1 | <span style="color: red">1</span> | | 5 | | 4 | <span style="color: red">1</span> | | 6 | | 7 | <span style="color: red">1</span> | </div> ---- 分析一下可能的複雜度,以下假設$V$為點個數,$E$為邊個數 1. 初始化 $\to$<!-- .element: class="fragment" data-fragment-index="1" --> $O(V)$<!-- .element: class="fragment" data-fragment-index="1" --> 2. 選一個離起點最近並且沒有走過的點 $u$ $\to$<!-- .element: class="fragment" data-fragment-index="2" --> $O(V \cdot V)$<!-- .element: class="fragment" data-fragment-index="2" --> 3. 把點 $u$ 設定為走過 $\to$<!-- .element: class="fragment" data-fragment-index="3" --> $O(V \cdot 1)$<!-- .element: class="fragment" data-fragment-index="3" --> 4. 透過所有連接點 $u$ 和 $v$ 的邊,去鬆弛點 $v$ $\to$<!-- .element: class="fragment" data-fragment-index="4" --> $O(E)$<!-- .element: class="fragment" data-fragment-index="4" --> 總複雜度:<!-- .element: class="fragment" data-fragment-index="5" -->$O(V^2+E)$<!-- .element: class="fragment" data-fragment-index="5" --> ---- ### 來看這個[例題](https://cses.fi/problemset/task/1671) 給你一個圖,圖上有$V$個點,$E$條邊,給定起點問你這個起點到每個點的最短距離。 $1 \le V\le10^5$,$1 \le E\le 2\cdot 10^5$ ---- 顯然剛剛的做法砸下去就會過了,那複雜度呢? $O(V^2+E)$<!-- .element: class="fragment" data-fragment-index="1" --> $\to$<!-- .element: class="fragment" data-fragment-index="1" --> $O(10^{10})$<!-- .element: class="fragment" data-fragment-index="1" --> $\to$<!-- .element: class="fragment" data-fragment-index="1" --> TLE<!-- .element: class="fragment" data-fragment-index="1" --> ---- 看看有哪些步驟的複雜度可以被優化掉 1. 初始化 $\to$ $O(V)$ 2. 選一個離起點最近並且沒有走過的點 $u$ $\to$ $O(V \cdot V)$ 3. 把點$u$設定為走過 $\to O(V \cdot 1)$ 4. 透過所有連接點 $u$ 和 $v$ 的邊,去鬆弛點 $v$ $\to$ $O(E)$ 總複雜度:$O(V^2+E)$ ---- 看看有哪些步驟的複雜度是可以被優化掉的 1. 初始化 $\to$ $O(V)$ 2. <span style="color: red">選一個離起點最近並且沒有走過的點 $u$ $\to$ $O(V \cdot V)$ </span> 3. 把點 $u$ 設定為走過 $\to O(V \cdot 1)$ 4. 透過所有連接點 $u$ 和 $v$ 的邊,去鬆弛點 $v$ $\to$ $O(E)$ 總複雜度:$O(V^2+E)$ ---- 可以搭配 priority_queue 這個 STL 工具來用 你可以把每個有被鬆弛過的點 $x$ 套成一個 pair,$(\text{dis}[x],x)$丟進priority_queue裡面,裡面會按照 $\text{dis}$ 去做排序 他可以幫你用 $O(\log V)$ 的時間知道哪個 $u$ 的 $\text{dis}[u]$ 是最小的 ---- 這時候的複雜度就會變成這樣 1. 初始化 $\to$ $O(V)$ 2. <span style="color: green">選一個離起點最近並且沒有走過的點$u$ $\to$ $O(V \cdot \log V)$</span> 3. 把點$u$設定為走過 $\to$$O(V \cdot 1)$ 4. 透過所有連接點$u$和$v$的邊,去鬆弛點$v$ $\to$ $O(E)$ 總複雜度:$O(V\log V+E)$ TLE 就解決了 ---- 範例code: ```cpp! [|6-9|10-11|13-14|15-16|17-22|] #define pii pair<int,int> #define INF 1e18 vector<pair<int,int>>vec[N]; void dijkstra(int s,int t){//起點,終點 int dis[N]; for(int i=0;i<N;i++){//初始化 dis[i]=INF;//值要設為比可能的最短路徑權重還要大的值 } dis[s]=0; priority_queue<pii,vector<pii>,greater<pii>>pq;//以小到大排序 pq.push({dis[s],s}); while(pq.empty()==0){ int u=pq.top().second; pq.pop(); if(vis[u])continue; vis[u]=1; for(auto [v,w]:vec[u]){ if(dis[u]+w<dis[v]){//鬆弛 dis[v]=dis[u]+w; pq.push({dis[v],v}); } } } } ``` 複雜度:$O(V\log V+E)$ ---- ### 小限制 如果圖中有負權邊,Dijkstra 這個演算法不能用 因為 Dijkstra 會取 $\text{dis}$ 最小的點,在有負權邊的情況下會爛掉 ---- 起點為 $1$ 的時候會爛掉 ![](https://hackmd.io/_uploads/SkxmJQccJl.png) ---- 若是邊權只有 $0$ 或 $1$,有沒有更好的優化方式? ---- <div style="position: absolute; left: 15%; top:80%;"> 可以發現我們只要維持每次取出最小值, 因此可以使用 deque 取代 priority_queue ![image](https://hackmd.io/_uploads/BJzFAva4Wx.png) 這樣可以保持 deque 內的{單調|monotonic}性質 (由小到大) </div> ---- 選一個離起點最近並且沒有走過的點 - 邊權為 $0$ $\to$ push_front() - 邊權為 $1$ $\to$ push_back() 複雜度從 $O(\log V)$ 降低到 $O(1)$ 這個技巧叫做 0-1 BFS ---- ### 0-1 BFS ```cpp= vector<pair<int, int>> vec[N]; deque<int> dq; int dis[N]; for(int i = 0; i < N; i++) dis[i] = INF; // 初始化 dq.push_front(0, s), dis[s] = 0; // 記得起點 s 要設為 0 while(!dq.empty()) { int u = dq.front(); // 取出當前最近的點 dq.pop_front(); for(auto [v, w] : vec[u]) { if (dis[u] + w < dis[v]) { // 鬆弛 dis[v] = dis[u] + w; if (w == 0) dq.push_front(v); else dq.push_back(v); }}} ``` ---- ### {最短路徑樹|Shortest Path Tree} 給定了一個起點,從這個起點走到任意的點的路徑會形成一棵樹(也有可能是多顆) 以下面的例子來說,最短路徑樹有兩顆 ![image](https://hackmd.io/_uploads/Sy7kFnJop.png) ---- fun fact: 最短路徑樹是一棵生成樹!! 複習一下[樹的其中一種定義方式](https://hackmd.io/@LeeShoWhaodian/2025-Graph-Theory#/4/13): - 無自環且任兩點僅存在唯一路徑的圖 對於每個點 $v$,可以挑選一條從 $s$ 到 $v$ 的最短路徑放進子圖 $T$ 如此一來 $T$ 當然可以符合此定義 ($T$ 是樹) 且因為 $T$ 包含所有節點,因此 $T$ 是**生成樹** --- ## 下課時間 --- ## Bellman-Ford ---- ### 想法 Bellman-Ford 是以每一條邊去做鬆弛, 能鬆弛就直接去做鬆弛,直到不能鬆弛為止 <div style="position: absolute; left: 20%; top:80%;"> ![image](https://hackmd.io/_uploads/SJJ5Tmyjp.png =600x) </div> ---- 不難發現由於一條最短路最多會經過 $n-1$ 條邊, 因此只要對每個邊鬆弛 $n-1$ 次就好 <div style="position: absolute; left: 20%; top:80%;"> ![image](https://hackmd.io/_uploads/SJJ5Tmyjp.png =600x) </div> ---- 考慮使用邊陣列,以每一條邊去做鬆弛 ```cpp! for (int i = 0; i < m; i++){ if (dis[edge[i].u] + edge[i].w < dis[edge[i].v]){ dis[edge[i].v] = dis[edge[i].u] + edge[i].w; } } ``` 複雜度:總共 $m$ 條邊,鬆弛 $n-1$ 次 $\to O(nm)$ ---- code: ```cpp! for(int t = 1; t < n; t++){ // 注意: 這邊只需要跑 n-1 次,而不是 n 次 for (int i = 0; i < m; i++){ if (dis[edge[i].u] + edge[i].w < dis[edge[i].v]){ dis[edge[i].v] = dis[edge[i].u] + edge[i].w; } } } ``` ---- ### {負環|Negative Cycle} 可以無限減少花費的一個環 ($1 \to 3 \to 2 \to 1$) ![image](https://hackmd.io/_uploads/H1fflNksT.png) ---- 有負環的話 會發現可以無限進行鬆弛操作 ![image](https://hackmd.io/_uploads/HJFglNJip.png) ---- ### 判斷負環 若是第 $n$ 輪鬆弛還有點被鬆弛到$\Rightarrow$這張圖存在負環 ---- ### Shortest Path Faster Algorithm (SPFA) 其實是 Bellman-Ford 的優化版本 ---- ### 想法 可以發現 Bellman-Ford 有很多鬆弛操作是多餘的 只有上一次被鬆弛的節點,所連接的邊,才有可能引起下一次鬆弛 ---- ### code 我們用一個queue來維護「可能會引起鬆弛操作的節點」 ```cpp= struct edge { int v, w; }; int s, n; // 起點 點數量 vector<edge> E[MXN]; vector<int> dis(MXN, INF), cnt(MXN), inq(MXN); // cnt記錄最短路經過幾條邊 // inq記錄節點是否在queue裡面 queue<int> Q; dis[s] = 0; Q.push(s); inq[s] = 1; while (Q.size()) { int u = Q.front(); Q.pop(); inq[u] = 0; for (auto [v, w] : E[u]) { if (dis[v] > dis[u] + w) { dis[v] = dis[u] + w; cnt[v] = cnt[u] + 1; if (cnt[v] >= n) { // 有負環 因為一般最短路只會經過 n - 1 條邊 } if (!inq[v]) Q.push(v), inq[v] = 1; } } } ``` ---- ### 複雜度 在大部分時候 SPFA 跑的非常快 但其實最差的情況複雜度還是 $O(nm)$ 當你發現 Bellman-Ford TLE 的時候 這個時候你就可以用 SPFA --- ## Floyd-Warshall ---- ### 想法 對於點對 $(i,j)$,找 $i$ 走到 $j$ 的最短路時,可能存在中繼點 $k$ 從 $i$ 走到 $k$ 加上 $k$ 走到 $j$ 的路徑可能會更短 <div style="position: absolute; left: 18%; top:80%;"> ![image](https://hackmd.io/_uploads/Sy8gmva4-x.png =600x) </div> 我們可以考慮用鄰接矩陣紀錄結果 ---- 注意到最佳子結構: $p_1$ 與 $p_2$ 的所有點都屬於 $\{1,2,\cdots ,k-1\}$ $p$ 的所有點都屬於 $\{1,2,\cdots, k\}$ <div style="position: absolute; left: 18%; top:80%;"> ![image](https://hackmd.io/_uploads/Sy8gmva4-x.png =600x) </div> ---- ### 定義 可以考慮 DP 來解問題: $\text{dp}[k][i][j]$ 為從 $i$ 到 $j$ 只經過 $1\sim k$ 的最短距離 ---- ### 初始化 $$ \text{dp}[0][i][j]= \begin{cases} w_{ij}\quad & \text{, if} (i,j)\in E\\ 0\quad & \text{, if } i=j\\ \infty\quad & \text{, otherwise}\\ \end{cases} $$ ---- ### 轉移 ```cpp! for (k = 1; k <= n; k++) { for (i = 1; i <= n; i++) { for (j = 1; j <= n; j++) { dp[k][i][j] = min(dp[k - 1][i][j], dp[k - 1][i][k] + dp[k - 1][k][j]); } } } ``` 空間複雜度:$O(N^3)$ <!-- .element: class="fragment" data-fragment-index="1" -->$\leftarrow$這個空間有點糟<!-- .element: class="fragment" data-fragment-index="1" --> 時間複雜度:$O(N^3)$ 正常來說我們不會這樣做 <!-- .element: class="fragment" data-fragment-index="2" --> ---- ### 空間優化 可以發現轉移的時候有一維是可以省略掉 $$ \text{dp}[k][i][j] \to \text{dp}[i][j] $$ 所以空間複雜度可以優化到 $O(N^2)$ ---- 空間可以這樣優化是因為 在更新時並不會取用到其他資訊而影響計算 也就{是說在計算 $d_{ij}^{(k)}$|(這邊換個符號來節省空間)} 時,因 $d_{kk}^{(k-1)}=0$,所以: $$ \begin{split} \text{Case 1: }&d_{ik}^{(k)} = \min(d_{ik}^{(k-1)}, d_{ik}^{(k-1)} + d_{kk}^{(k-1)})=d_{ik}^{(k-1)}\\ \text{Case 2: }&d_{kj}^{(k)} = \min(d_{kj}^{(k-1)}, d_{kk}^{(k-1)} + d_{kj}^{(k-1)})=d_{kj}^{(k-1)}\\ \text{Case 3: }&d_{ij}^{(k)} = \min(d_{ij}^{(k-1)}, d_{ik}^{(k-1)} + d_{kj}^{(k-1)}), (i\neq k, k\neq j) \end{split} $$ ---- <div style="position: absolute; left: 0%; top:100%;"> $\text{Case 1, 2}$ 通過證明發現不影響答案, 而 $\text{Case 3}$ 一定不影響 - $\text{Case 1}$ 就是紅色虛線框 - $\text{Case 2}$ 就是藍色虛線框 - $\text{Case 3}$ 就是其他的部分 </div> <div style="position: absolute; right: 0%; top:100%;"> ![image](https://hackmd.io/_uploads/ByvzowT4bl.png =360x) </div> ---- 實作上,我們只會用輸入的矩陣 in-place 把答案算出來, 而不是開 $N$ 個矩陣 ---- ### 初始化和讀邊 - 自己到自己的距離為 $0$,否則為 INF - 有一個有向邊為 $u$ 走到 $v$,權重為 $w$,更新 $\text{dis}[u][v]$ ```cpp! [1-7|8-10] int dis[N + 1][N + 1]; for(int i = 1; i <= N; i++){ for(int j = 1; j <= N; j++){ dis[i][j] = INF; // INF 就是很大的數 } dis[i][i] = 0; } int u, v, w; cin >> u >> v >> w; dis[u][v]=min(dis[u][v], w); ``` 初始化時間複雜度:$O(N^2)$ ---- 依序枚舉每個中繼點,再枚舉每個點對,順序是 $(k,i,j)$ ```cpp! for(int k=1;k<=N;k++){//窮舉中繼點k for(int i=1;i<=N;i++){ for(int j=1;j<=N;j++){//窮舉點對(i,j) dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]); } } } ``` 時間複雜度:$O(N^3)$ ---- Floyd-Warshall 可以在時間複雜度 $O(N^3)$、空間 $O(N^2)$ 以內做完全點對最短路徑問題 基本上要做全點對最短路徑一定砸 Floyd-Warshall --- ### Summary | 演算法 | 時間 | 存圖 | 備註 | | --- | --- | --- | --- | | Dijkstra | $O(V \log V)$ | priority_queue | No 負邊 | |0-1 BFS | $O(V + E)$ | deque | $w=$ 0 or 1 | | Bellman-Ford | $O(VE)$ | 邊陣列 | 可測負環 | | SPFA | $O(VE)$ | queue | 快一點 | | Floyd-Warshall | $O(V^3)$ | 鄰接矩陣 | 全點對 | ---- 來練習8
{"title":"Shortest Path","image":"https://hackmd.io/_uploads/r1SO1_64Ze.png","description":"海大競程 2026 spring 上課講義 shortest path\n本章主要介紹三種演算法:\n- Dijkstra\n- Bellman-Ford\n- Floyd-Warshall\n除此之外還介紹了:\n- 0-1 BFS\n- SPFA \n等主題","contributors":"[{\"id\":\"4f67a8cd-06ae-45dc-a8e3-62c6a41e5a37\",\"add\":35782,\"del\":6067,\"latestUpdatedAt\":1769610954479}]"}
    101 views
   Owned this note