:+1: [2020 競技程設教材 HackMD Book](https://hackmd.io/@nckuacm/ryLIV6BYI) :+1: 2019 Week 13: Graph 2 = 本週主題內容較為複雜,時間會帶給各位對圖論演算法的直覺感悟 # 最大流 (Maximum flow) :::info 給圖 $G=(V, E)$,對邊 $(u, v) \in E$ 有**剩餘容量** $R(u, v)$ 求從點 $s$ 到點 $t$ 的最大流量。 ::: 以下圖為例: 邊上面的數字代表**容量上限** > 例如 $R(e, b) = 2$ ```graphviz digraph { a -> b[label=1] b -> t[label=2]; s -> e[label=2] e -> f[label=1]; rankdir="LR"; { rank="same" s->a [label=1]} { rank="same" e->b [label=2]} { rank="same" f->t [label=1]} } ``` 而 $s \rightarrow e \rightarrow b\rightarrow t$ 可得流量 $1$ 或 $2$, 且當此路徑流量為 $1$ 時,剩餘容量皆為 $1$ > 流量 $2$ 則剩餘容量皆 $0$ ```graphviz digraph { a -> b[label=1] b -> t[label=2, color=red]; s -> e[label=2, color=red] e -> f[label=1]; rankdir="LR"; { rank="same" s->a [label=1]} { rank="same" e->b [label=2, color=red]} { rank="same" f->t [label=1]} } ``` 若 $s \rightarrow e \rightarrow f\rightarrow t$ 則這流量最大只能為 $1$,是由於邊**最多承載**容量為 $1$ ```graphviz digraph { a -> b[label=1] b -> t[label=2]; s -> e[label="1/2", color=red] e -> f[label="1/1", color=red]; rankdir="LR"; { rank="same" s->a [label=1]} { rank="same" e->b [label=2]} { rank="same" f->t [label="1/1", color=red]} } ``` ==這裡 $x/y$ 表示 $x$ 流量,$y$ 容量**上限**,而剩餘容量為 $y-x$== 也就是說: $s \rightarrow e \rightarrow f\rightarrow t$ 流量 $1$ $s \rightarrow a \rightarrow b\rightarrow t$ 流量 $1$ 能得流量 $2$ ```graphviz digraph { a -> b[label="1/1", color=red] b -> t[label="1/2", color=red]; s -> e[label="1/2", color=red] e -> f[label="1/1", color=red]; rankdir="LR"; { rank="same" s->a [label="1/1", color=red]} { rank="same" e->b [label=2]} { rank="same" f->t [label="1/1", color=red]} } ``` 若是 $s \rightarrow e \rightarrow f\rightarrow t$ 流量 $1$ $s \rightarrow e \rightarrow b\rightarrow t$ 流量 $1$ $s \rightarrow a \rightarrow b\rightarrow t$ 流量 $1$ 能得流量 $3$,這就是**最大流** ```graphviz digraph { a -> b[label="1/1", color=red] b -> t[label="2/2", color=blue]; s -> e[label="2/2", color=blue] e -> f[label="1/1", color=red]; rankdir="LR"; { rank="same" s->a [label="1/1", color=red]} { rank="same" e->b [label="1/2", color=red]} { rank="same" f->t [label="1/1", color=red]} } ``` 在找到最大流量之前,得先討論怎樣的圖,存在著一條可以獲得流量($>0$)的路徑 ## Augmenting path 給下圖: ```graphviz digraph { s t Node[label=""] s -> 5 -> 6[dir=none]; 2 -> 3 -> 4 -> t[dir=none]; rankdir="LR"; s -> 1 -> 2 [dir=none]; { rank="same" 2->6 [dir=none] } 6 -> 7 -> 8 -> t [dir=none]; } ``` **隨便找**到一條流 $f_1$: ```graphviz digraph { s t Node[label=""] s -> 5 -> 6[dir=none]; 2 -> 3 -> 4 -> t[dir=none]; rankdir="LR"; s -> 1 -> 2 [color=red, penwidth=4] { rank="same" 2->6 [color=red, penwidth=4]}; 6 -> 7 -> 8 -> t [color=red, penwidth=4] } ``` 找到第二條流 $f_2$: 這是無向邊,所以它想要往上跑 如果==邊容量允許==的話這是可行的 ```graphviz digraph { s t Node[label=""] s -> 5 -> 6[color=blue, penwidth=4]; 2 -> 3 -> 4 -> t[color=blue, penwidth=4]; { rank="same" 6->2 [color=blue, penwidth=4]}; { rank="same" 2->6 [dir=none] } rankdir="LR"; s -> 1 -> 2 [color=red, penwidth=4] { rank="same" 2->6 [color=red, penwidth=4]}; 6 -> 7 -> 8 -> t [color=red, penwidth=4] } ``` ### 若 $f_1 = f_2$ 中間兩流相遇的部份會**相消** ```graphviz digraph { s t Node[label=""] { rank="same" 2->6 [dir=none] } rankdir="LR"; s -> 1 -> 2 [color=red, penwidth=4] 6 -> 7 -> 8 -> t [color=red, penwidth=4] s -> 5 -> 6[color=blue, penwidth=4]; 2 -> 3 -> 4 -> t[color=blue, penwidth=4]; } ``` ### 若 $f_1 > f_2$ 中間相遇部份 $f_1$ 多少會被減弱一些 > 所以將顏色改變一下,因為**流量**不見得與另外兩色相同 ```graphviz digraph { s t Node[label=""] s -> 1 -> 2 [color=red, penwidth=4] 6 -> 7 -> 8 -> t [color=red, penwidth=4] rankdir="LR"; s -> 5 -> 6[color=blue, penwidth=4]; { rank="same" 2->6 [color=orange, penwidth=4]}; 2 -> 3 -> 4 -> t[color=blue, penwidth=4]; } ``` ### 具體來說 給下圖: > 幾乎跟上面例子一樣 特別注意,$R(b, f) = 3$ 而 $R(f,b)=0$ ```graphviz digraph { s t b f Node[label=""] s -> e -> f[ label=1]; b -> c -> d -> t[ label=1]; rankdir="LR"; s -> a -> b [ label=3]; { rank="same" a->e [ style=invis] } { rank="same" b->f [ label=3] } { rank="same" c->g [ style=invis] } { rank="same" d->h [ style=invis] } f -> g -> h -> t [ label=3]; } ``` 一樣的 $f_1$: ```graphviz digraph { s t b f Node[label=""] t [label="t:3"]; s -> e -> f[ label=1]; b -> c -> d -> t[ label=1]; rankdir="LR"; s -> a -> b [ label="3/3", color=red]; { rank="same" a->e [ style=invis] } { rank="same" b->f [ label="3/3", color=red] } { rank="same" c->g [ style=invis] } { rank="same" d->h [ style=invis] } f -> g -> h -> t [ label="3/3", color=red]; } ``` 接著 $f_2$: ```graphviz digraph { s t b f Node[label=""] t [label="t:3"]; s -> e -> f[ label="1/1", color=blue]; b -> c -> d -> t[ label=1]; rankdir="LR"; s -> a -> b [ label="3/3", color=red]; { rank="same" a->e [ style=invis] } { rank="same" b->f [ label="3/3", color=red] } { rank="same" c->g [ style=invis] } { rank="same" d->h [ style=invis] } f -> g -> h -> t [ label="3/3", color=red]; } ``` 但是 $f_2$ **過不去**,因為剩餘容量為 $0=R(f, b)$ 若在流 $f_1$ 形成時,給予**反向邊[^4]容量**,就能使得 $f_2$ 流得過去 也就是: 接下來**只需紀錄剩餘容量**就足夠知道流量為何了 所以==現在邊上數字代表的是**剩餘容量**== ```graphviz digraph { s t b f Node[label=""] t [label="t:3"]; s -> e -> f[ label=1]; b -> c -> d -> t[ label=1]; rankdir="LR"; s -> a -> b [ label="0", color=red]; { rank="same" a->e [ style=invis] } { rank="same" b->f [ label="0", color=red] } { rank="same" c->g [ style=invis] } { rank="same" d->h [ style=invis] } f -> g -> h -> t [ label="0", color=red]; } ``` 然後**給予反向邊容量**: >這裡避免圖過於複雜,把 $0$ 邊去掉 ```graphviz digraph { s t b f Node[label=""] t [label="t:3"]; s -> e -> f[ label=1]; b -> c -> d -> t[ label=1]; rankdir="LR"; s -> a -> b [ label="3", dir=back]; { rank="same" a->e [ style=invis] } { rank="same" b->f [ label="3", dir=back] } { rank="same" c->g [ style=invis] } { rank="same" d->h [ style=invis] } f -> g -> h -> t [ label="3", dir=back]; } ``` 所以現在 $f_2$ 能流過去了! ```graphviz digraph { s t b f Node[label=""] t [label="t:4"]; s -> e -> f[ label=1, color=blue]; b -> c -> d -> t[ label=1, color=blue]; rankdir="LR"; s -> a -> b [ label="3", dir=back]; { rank="same" a->e [ style=invis] } { rank="same" b->f [ label="3", dir=back, color=blue] } { rank="same" c->g [ style=invis] } { rank="same" d->h [ style=invis] } f -> g -> h -> t [ label="3", dir=back]; } ``` 實際上, $b$ 到 $t$ 的流量原本 $3$,經過 $f_2$ 後變為 $\textbf{2}$ 也就是說,若==現在邊上代表的是**流量**==: ```graphviz digraph { s t b f Node[label=""] t [label="t:4"]; s -> e -> f[ label=1, color=blue]; b -> c -> d -> t[ label=1, color=blue]; rankdir="LR"; s -> a -> b [ label="3", color=red]; { rank="same" a->e [ style=invis] } { rank="same" b->f [ label="2", color=orange] } { rank="same" c->g [ style=invis] } { rank="same" d->h [ style=invis] } f -> g -> h -> t [ label="3", color=red]; } ``` 所以直覺的,每次**找到 augmenting path**,讓流送到 $t$ **不斷重複**這個過程,直到找不到 augmenting path,就能求得最大流 > 這稱作 Ford-Fulkerson method ## Edmonds-Karp algorithm 如果邊是無向的, ```graphviz digraph { rankdir=LR u -> v[dir=none, label=x] } ``` 則將邊變為兩條**剩餘容量相等**的**有向邊** ```graphviz digraph { rankdir=LR u -> v [dir=back, label=x] u -> v [style=invis] u -> v [label=x] } ``` 用 **BFS** 找出從 $s$ 到 $t$ 的 augmenting path 將路徑上各邊的剩餘容量扣除**流量**,以及將各反向邊[^4]剩餘容量加上**流量**。 **重複** BFS 直到找不到 augmenting path ```cpp int max_flow = 0; while (1) { memset(vis, false, sizeof(vis)); vis[s] = true; memset(flow, 0, sizeof(flow)); flow[s] = INF; queue<int> Q; Q.push(s); while (!Q.empty()) { int u = Q.front(); Q.pop(); for (int v = s; v <= t; v++) { if (!R[u][v] || vis[v]) continue; vis[v] = true; flow[v] = min(flow[u], R[u][v]); // 流只挑最小的剩餘容量 Q.push(v); pre[v] = u; // 紀錄 augmenting path } } if (flow[t] == 0) break; max_flow += flow[t]; for (int v = t; v != s; v = pre[v]) { int u = pre[v]; R[u][v] -= flow[t]; R[v][u] += flow[t]; } } ``` > 感受下圖像化的流程: [WilliamFiset/ Edmonds Karp Algorithm](https://www.youtube.com/watch?v=RppuJYwlcI8) #### 練習: [UVa OJ 820 Internet Bandwidth](https://uva.onlinejudge.org/external/8/820.pdf) [UVa OJ 10330 Power Transmission](https://uva.onlinejudge.org/external/103/10330.pdf) [UVa OJ 11987 Almost Union-Find](https://uva.onlinejudge.org/external/119/11987.pdf) [POJ 2584 T-Shirt Gumbo](http://poj.org/problem?id=2584) [POJ 3228 Gold Transportation](http://poj.org/problem?id=3228) <!-- 考慮下圖: ```graphviz digraph { s t Node[label=""] s -> e -> t[ label=q]; rankdir="LR"; s -> a -> t[ label="p"]; { rank="same" e->a [label="1"] } } ``` 如果每次 augmenting path 流量都為 $1$,則時間成本為 $p + q$ 故演算法複雜度 --> # Articulation point Articulation point 中譯 關節點 (簡稱 AP), 當連通圖上此**點**被拔除,圖會分為多塊**連通圖** :::info 給定連通圖,找到所有 AP ::: 考慮**樹**,顯然除**葉**以外,其餘的節點都為 AP 且若**根**只有一個子節點,則不為 AP ```graphviz graph { 3 -- 4; 3 -- 5 -- 6; 5 -- 7; 7 -- 8; } ``` 如何讓 AP 不再是 AP 呢? 例如: 將 $5$ 變非 AP,可讓 $7$ 連到 $3$,及 $6$ 連到 $4$。 ```graphviz graph { {9 [ style=invis, width=0, label=""]} {3 -- 9 [ style=invis]} {3 -- 9 [ style=invis]} 3 -- 4; {"10" [ style=invis, width=0, label=""]} {3 -- 10 [ style=invis]} 3 -- 5 -- 6; 6 -- 4 [color=red]; { rank="same" 4--5 [ style=invis] } 5 -- 7; 7 -- 3 [color=red]; { rank="same" 6--7 [ style=invis] } 7 -- 8; } ``` 也就是說,若問**連通圖**中有哪些點是 AP,先遍歷出棵**樹** (例如 DFS 樹) 接著直覺的,節點的子孫**有邊**回到比此節點還高[^5] (淺)的節點,則此節點非 AP。 > 高指的是 level 數字較小,淺指的是 depth 數字較小 而這個**連向祖先的邊**,稱作 **back edge** ## Tarjan’s algorithm for APs 實作上透過下列兩個函數,以找出所有 AP - $\text{dep}(n)$ 代表節點 $n$ 的 depth 若節點還未拜訪過 $\text{dep}(n) = -1$ - $\text{low}(n)$ 代表節點 $n$ 透過 back edge 能連向**最高**節點的 depth 值, 若無 back edge 則為 $\text{dep}(n)$ ![](https://i.imgur.com/GZi6H8N.gif) [^tarjamapdemo] Tarjan 用 DFS 遍歷出樹: ```cpp void dfs(int p, int u, int d) { // p := previous, d := depth dep[u] = low[u] = d; int cnt = !!d; // (parent and children of u) in dfs tree bool back_error = false; for (int v: E[u]) if (v != p) { if (dep[v] != -1) { low[u] = min(low[u], dep[v]); continue; } cnt++; dfs(u, v, d+1); low[u] = min(low[u], low[v]); if (low[v] >= d) back_error = true; } if (cnt > 1 && back_error) AP.push_back(u); } ``` #### 練習: [UVa OJ 315 Network](https://uva.onlinejudge.org/external/3/315.pdf) [ICPC World Finals 2003 Building Bridges](https://icpcarchive.ecs.baylor.edu/external/27/2721.pdf) [CODEFORCES 194C Cutting Figure](https://codeforces.com/contest/194/problem/C) # Strongly Connected Components (SCC) 強連通是只屬於**有向圖**的概念 根據[第十一週教材](https://hackmd.io/s/SkCtEVcq4), - 把圖的**方向**拿掉,連通的話稱此圖**弱連通** - 加上方向後仍然連通,則此圖**強連通** 雖然有向圖不總有強連通性,但能把圖分成幾塊**強連通分塊**: ![](https://i.imgur.com/6xhmvUF.png)[^scc] :::info 給定圖,求圖**至少**有幾塊強連通分塊 ::: ## Kosaraju's Algorithm 連通意味著任兩點 $u, v$ 之間有路 那麼把 $u\leadsto v$ 的各邊反過來,對 $v\leadsto u$ 也反過來,圖理當也連通 > $u\leadsto v$ 表示 $u$ 到 $v$ 路徑 因為 $u\leadsto v$ 及 $v\leadsto u$ 兩路合併,能得到環,**環反過來還是環** 也就是說,對於強連通圖: ```graphviz digraph { node[label=""] rankdir="LR" b -> c -> a a -> d [dir=back] c -> e -> d a -> b } ``` 把邊方向反過來仍具有強連通性: ```graphviz digraph { node[label=""] edge[dir=back] rankdir="LR" b -> c -> a a -> d [dir=forward] c -> e -> d a -> b } ``` - 對於**無相圖**,[第四週教材](https://hackmd.io/s/SJ7nxwRL4#%E7%AF%84%E4%BE%8B-UVa-OJ-572-Oil-Deposits%EF%BC%9A)提到 DFS 能判斷是否具有連通性 - 對於**有向圖** DFS 拜訪到的節點與反邊圖 DFS 拜訪的**節點相同**,則為強連通 > 反邊圖就是把圖上的邊全改為反向 ```cpp void forward(int u) { vis[u] = true; for (int v = 0; v < n; v++) if (E[u][v] && !vis[v]) forward(v); st.push(u); } void backward(int u) { vis[u] = true; for (int v = 0; v < n; v++) if (E[v][u] && !vis[v]) backward(v); } ``` > 這裡 `E` 是鄰接矩陣 stack `st` 紀錄有哪些點是在 forward 走過,但 backward 沒走過 ```cpp memset(vis, false, sizeof(vis)); for (int u = 0; u < n; u++) if (!vis[u]) forward(u); memset(vis, false, sizeof(vis)); int cnt = 0; // 紀錄有多少強連通分塊 while (!st.empty()) { int u = st.top(); st.pop(); if (!vis[u]) cnt++, backward(u); } ``` 基於 stack 的特性,可將 forward 先一次做完,再一次次做 backward #### 練習: [ICPC Regionals 2008 Taipei Road Networks](https://icpcarchive.ecs.baylor.edu/external/42/4262.pdf) [UVa OJ 247 Calling Circles](https://uva.onlinejudge.org/external/2/247.pdf) [UVa OJ 11838 Come and Go](https://uva.onlinejudge.org/external/118/11838.pdf) \* [ICPC Regionals 2010 Hangzhou Traffic Real Time Query System](https://icpcarchive.ecs.baylor.edu/external/48/4839.pdf) <!-- ## Tarjan's algorithm for SCCs --> [^4]: 當邊為點 $u$ 到點 $v$,則*反向邊*指的是 $v$ 到 $u$ [^5]: 所謂的比較高,意思是距離根比較近 [^tarjamapdemo]: [Wikipedia/ A demo of Tarjan's algorithm to find cut vertices.](https://en.wikipedia.org/wiki/Biconnected_component#/media/File:TarjanAPDemoDepth.gif) [^scc]: [Wikipedia/ Graph with strongly connected components marked](https://en.wikipedia.org/wiki/Strongly_connected_component#/media/File:Scc.png)