# 【資工考研】DSA: Graph (3) MST + Articulation Point + Flow Network [前一篇:【資工考研】DSA: Graph (2)](https://hackmd.io/@RyoukiWei/graph_2) 1. Graph Representations 2. BFS & DFS 3. Shortest Path Problem 4. Minimum Spanning Tree 5. Articulation Point 6. Flow Network ## Minimum Spanning Tree 給定 $G = (V,E)$ 為一個 undirect weighted graph 1. 若 $H$ 為 $G$ 的一個 spanning subgraph 且 $H$ 為 tree 則稱 $H$ 為 $G$ 的一個生成樹 2. 假設 $T = (V,E')$ 為 $G$ 的一個 spanning tree,定義 $w(T) = \sum\limits_{e\in E'}w(e)$ ,其中 $w(e)$ 為 $e$ 的 weight,稱 $w(T)$ 為 $T$ 之權重 3. 若 $T$ 為 $G$ 之 spanning tree 中擁有最小權重者,稱之為 $G$ 之最小生成樹 (minimum spanning tree) 簡單講,生成樹本身會是樹,並且頂點與原圖一樣,就差它可能從原圖上拔掉了一些邊 樹本身是 acyclic 的,並且至少需要有 $\left| V \right| - 1$ 條邊 求 MST 即要使這些邊的成本總和最小 MST 能夠應用在城市電網佈局或是交通建設等使成本最小化的問題上,是個重要的問題 ### Kruskal's Algorithm 採用 greedy 的策略求出 MST 每次挑選 weight 最小的邊加入 MST $T$,過程中必須保證加入的邊不會使 $T$ 出現 cycle ```cpp! struct Edge { int from, to, weight; }; struct Graph { int num; std::vector<Edge> edges; explicit Graph(int n) : num(n) {} void addEdge(int u, int v, int w) { edges.push_back({u, v, w}); edges.push_back({v, u, w}); } }; std::vector<Edge> kruskalMST(const Graph &g) { if (g.num < 2) return {}; DisjointSet<int> ds(g.num); std::vector<Edge> mst; mst.reserve(g.num - 1); // Sort all edges in non-decreasing order of their weights. // Sorting is essential to always pick the smallest edge first. auto edgesNew = g.edges; std::sort(edgesNew.begin(), edgesNew.end(), [](const Edge &a, const Edge &b) { return a.weight < b.weight; }); for (const auto &edge : edgesNew) { // If the edge connects two different components (no cycle), add it to MST. // The `unionSets` function returns true if the edge connects two different sets (no cycle). if (ds.unionSets(edge.from, edge.to)) { mst.push_back(edge); if (mst.size() == g.num - 1) break; } } return mst; } ``` 這邊,我們使用 disjoint set 來輔助檢查 我們可以將 $T$ 中所有 component 視為一個 set,在檢查每次加入 $e = (u,v)$ 會不會產生 cycle 時,相當於判斷 $u$ 跟 $v$ 是否在同一個 set 中,若不屬於同個 set 表示可以加入 $e$ 在 `unionSets` 我們就會去比 `find(u)` 是否等於 `find(v)` 了 以下提供一個簡易的 disjoint set 實作: ```cpp! /** * @class DisjointSet * @brief A generic implementation of the Disjoint Set data structure with * path compression and union by rank for efficient set operations. * * @tparam T The type of elements in the disjoint set. Typically, this will be an * integral type such as int or size_t. */ template <typename T> class DisjointSet { static_assert(std::is_integral<T>::value, "This DisjointSet implementation requires an integral type."); private: std::vector<T> parent; std::vector<int> rank; public: /** * @brief Constructor, initialize disjoint set with `n` elements (`0` to `n-1`) * @param n The number of elements. */ explicit DisjointSet(size_t n) : parent(n), rank(n) { std::iota(parent.begin(), parent.end(), 0); } /** * @brief Union operation by rank. Merges the sets containing elements `x` and `y`. * @param x An element in the first set to be merged. * @param y An element in the second set to be merged. * @return `true` if the sets were merged successfully, or `false` if `x` and `y` * were already in the same set. */ bool unionSets(T x, T y) { T rootX = find(x); T rootY = find(y); if (rootX == rootY) // Already in the same set return false; if (rank[rootX] < rank[rootY]) std::swap(rootX, rootY); parent[rootY] = rootX; if (rank[rootX] == rank[rootY]) ++rank[rootX]; return true; } /** * @brief Find operation with path compression. Finds the root of the set containing `x`. * @param x The element whose set root is to be found. * @return The root of the set containing `x`. */ T find(T x) { using UnsignedT = typename std::make_unsigned<T>::type; assert(static_cast<UnsignedT>(x) < parent.size() && "Index out of bounds"); return parent[x] != x ? parent[x] = find(parent[x]) : parent[x]; } }; ``` 雖然排序需要 $O(\left| E \right|\lg \left| E \right|)$ 的時間,但如果邊不多 (通常啦,課本就這樣),我們可以改說是 $O(\left| E \right|\lg \left| V \right|)$ disjoint set 所需時間是 $O(\left| E \right|\alpha (\left| V \right|))$ ,而 $\alpha (\left| V \right|)$ 成長非常緩慢 所以我們直接就說時間複雜度是 $O(\left| E \right|\lg \left| V \right|)$ ### Prim's Algorithm 也是採用 greedy 的策略,但不同的是,我們每次從 $V$ 中任選一點形成集合 $S$ ,每次選取 $e = \left\{ u,v \right\}$ 加入 $E'$ ,其中 $u\in S, v\notin S$ ,想當然我們每次都挑 weight 最小的邊,並且保證 $S$ 維持 connected 直到所有頂點都放入 $S$ 了,$E'$ 中的邊即形成一個 MST ```cpp! // Don't mind struct pair_hash { template <class T1, class T2> std::size_t operator()(const std::pair<T1, T2> &pair) const noexcept { auto hash1 = std::hash<T1>{}(pair.first); auto hash2 = std::hash<T2>{}(pair.second); return hash1 ^ (hash2 << 1); } }; struct Graph { int num; std::vector<std::vector<int>> adjList; std::unordered_map<std::pair<int, int>, int, pair_hash> edgeWeights; struct Vertex { int id; int key; bool inMST; Vertex *pi; Vertex(int id) : id(id), key(INT_MAX), pi(nullptr), inMST(false) {} }; std::vector<Vertex> vertices; Graph(int n) : num(n), adjList(n), vertices(n) {} void addEdge(int u, int v, int weight) { adjList[u].push_back(v); adjList[v].push_back(u); edgeWeights[{u, v}] = weight; edgeWeights[{v, u}] = weight; } int w(int u, int v) const noexcept { auto it = edgeWeights.find({u, v}); if (it != edgeWeights.end()) return it->second; return INT_MAX; // If edge doesn't exist, treat as ∞ } struct CompareKey { bool operator()(const Vertex *u, const Vertex *v) { return u->key > v->key; } }; }; std::vector<Graph::Vertex *> primMST(Graph &g) { for (int i = 0; i < g.num; ++i) { g.vertices[i].key = INT_MAX; g.vertices[i].pi = nullptr; g.vertices[i].inMST = false; } g.vertices[0].key = 0; std::priority_queue<Graph::Vertex *, std::vector<Graph::Vertex *>, Graph::CompareKey> pq; for (int i = 0; i < g.num; ++i) { pq.push(&g.vertices[i]); } std::vector<Graph::Vertex *> mstV; while (!pq.empty()) { Graph::Vertex *u = pq.top(); pq.pop(); u->inMST = true; mstV.push_back(u); for (int v : g.adjList[u->id]) { if (!g.vertices[v].inMST) { const int weight = g.w(u->id, v); if (g.vertices[v].key > weight) { pq.push(&g.vertices[v]); g.vertices[v].key = weight; g.vertices[v].pi = u; } } } } return mstV; } ``` 你會發現,這演算法怎麼這麼像 [Dijkstra](https://hackmd.io/@RyoukiWei/graph_2#Dijkstras-Algorithm) 確實,而且他們的時間複雜度也雷同,同樣受到 priority queue 實作的影響 | \ | $\mathrm{Initialization}$ | $\mathrm{Extract-Min()}$ | $\mathrm{Decrease-Key}$ | Total | | :---: | :---: | :---: | :---: | :---: | | Array | $\theta (\left\| V \right\|)$ | $O(\left\| V \right\|^2)$ | $O(\left\| E \right\|)$ | $O(\left\| V \right\|^2)$ | | Binary Heap | $\theta (\left\| V \right\|)$ | $O(\left\| V \right\|\lg \left\| V \right\|)$ | $O(\left\| E \right\|\lg \left\| V \right\|)$ | $O(\left\| V \right\|\lg \left\| V \right\| + \left\| E \right\|\lg \left\| V \right\|)$ | | Fibonacci Heap | $\theta (\left\| V \right\|)$ | $O(\left\| V \right\|\lg \left\| V \right\|)$ | $O(\left\| E \right\|)$ | $O(\left\| V \right\|\lg \left\| V \right\| + \left\| E \right\|)$ | ### Borůvka (Sollin)'s algorithm 這個以考試來說偏冷門 簡單來說,就是改看成一群樹 (森林),一開始樹都是只有各個頂點,我們一直合併樹的方式直到最後只剩一棵樹即為所求 它也是 greedy 策略,每次都挑成本最小的 時間複雜度也是 $O(\left| E \right| \lg \left| V \right|)$ 下面是我大致寫的,應該是對的吧…… 如果我自己沒理解錯 (我對它也不熟) ```cpp! struct Edge { int from, to, weight; }; struct Graph { int num; std::vector<Edge> edges; explicit Graph(int n) : num(n) {} void addEdge(int u, int v, int w) { edges.push_back({u, v, w}); edges.push_back({v, u, w}); } }; std::vector<Edge> boruvka(const Graph &g) { int n = g.num; // number of components DisjointSet<int> ds(g.num); std::vector<Edge> mst; while (n > 1) { std::vector<Edge> cheapest(g.num, {-1, -1, INT_MAX}); for (const auto &edge : g.edges) { const int u = ds.find(edge.from); const int v = ds.find(edge.to); if (u != v) // Edge connects different components { if (edge.weight < cheapest[u].weight) cheapest[u] = edge; if (edge.weight < cheapest[v].weight) cheapest[v] = edge; } } for (const auto &edge : cheapest) { if (edge.from != -1 && edge.to != -1) { const int u = ds.find(edge.from); const int v = ds.find(edge.to); if (u != v) { mst.push_back(edge); ds.unionSets(u, v); --n; } } } } return mst; } ``` 因為它真的很冷門,如果不太會可以選擇跳過 ### MST 常考觀念 MST 選擇題之類的啊,很愛考一些觀念,舉幾個: 1. 如果 $G$ 所有邊的權重皆不相同,則 MST 必定唯一;second best MST 不一定唯一。如果有相同權重 MST 可能不唯一 (所以上述幾種演算法得出的 MST 可能長得不一樣,不過權重和倒是一定一樣,畢竟是最小了) 2. 對 $G$ 任何 cycle $C$ ,如果 $C$ 有一個邊,沒有任何其他 $C$ 中的邊權重比它大,那麼該邊必定不屬於 $G$ 之任何一棵 MST 3. 下列敘述為 False: $G$ 中唯一最大的邊必定不在任何一棵 MST 4. 下列敘述為 False: $C$ 中唯一最小的邊必定屬於 MST 5. 圖之切集中唯一最小的邊必定屬於 MST 6. 圖中唯一最小的邊必定屬於 MST 7. 承上,該圖中唯一第二小的邊亦必在 MST 中 ## Articulation Point 無向連通圖中,如果移除某頂點會圖的分量變多,該點稱為 articulation point (關節點) 去掉某頂點會使圖不連通,則稱為 cut vertex (切點) 兩者基本上是差不多的,我們以下就當一樣 cut vertex 的集合為 vertex cut (點切集) 定義 vertex connectivity (點連通度) $\lambda (G)$ 為最少移除多少頂點可使圖不連通 可想而知,$0\le \lambda (G) \le \left| V \right| - 1$ 連通圖至少三頂點且沒有 cut vertex 時稱該圖為 biconnected 不存在 cut vertex 的無向連通圖為 biconnected graph biconnected component 指為 biconnected graph 的 maximal induced subgraph (極大誘導子圖) 像網路結構之類的,如果有 articulation point ,表示該節點掛掉了會是大災難 所以確保其為 biconnected graph 是重要的 以下介紹找出 articulation point 的方法 ### Naive Approach 最簡單的且直覺的想法,就是把每個頂點都移除看看,看會不會使圖不連通 為此需要遍歷圖 $\left| V \right|$ 次,時間複雜度會是 $O(\left| V \right|^2 + \left| V \right|\left| E \right|)$ 以演算法來說,還有改進空間,然而對人眼判別來說,這樣的方法就相當夠用了,而且通常考試給的圖並不大 只是要你找出 articulation point 的話,用此方法反而更快 ### Tarjan's Algorithm 我們之前有講過[如何找出所有 strongly connected component](https://hackmd.io/@RyoukiWei/graph_1#%E6%89%BE%E5%87%BA%E6%89%80%E6%9C%89-strongly-connected-component) 其中 Kosaraju's algorithm 需要兩次 DFS ,而 Tarjan's algorithm 只需要一次 DFS 即可 現在,雖然問題變成無向圖,但我們依舊能使用其想法: 1. 在 DFS 的過程中,維持陣列 `parent` ,紀錄各頂點之 parent 2. 如果頂點 $u$ 是 DFS spanning tree 的 root (`parent[u] == nullptr`) 並且其子點數量 $\gt 1$ ,則 $u$ 為一個 articulation point 3. 如果 $u$ 不是 DFS spanning tree 的 root 且其子點 $v$ 之子樹 (以 $v$ 為 root 的子樹) 中不存在任何 back edge 能連回 $u$ 所在的 DFS spanning tree 中的 ancestors ,我們需要將這些節點的 discovery time 記錄下來 (存在 `disc` 中) 4. 對每個 $u$ ,找出以 $u$ 為 root 的子樹中可以到達且最早被拜訪的節點 (discovery time 最小的節點),我們定義 $\mathrm{low}\lbrack u \rbrack = \min\left\{ \mathrm{disc}\lbrack u \rbrack, \mathrm{disc}\lbrack w \rbrack \right\}$ ,其中 $w$ 是 $u$ 的某個 ancestor ,且存在一條從 $u$ 的 descendant 連回到 $w$ 的 back edge 這邊 $\mathrm{low}$ 值是演算法核心,如果 $\mathrm{low}\lbrack v \rbrack \ge \mathrm{disc}\lbrack u \rbrack$ ,表示從 $v$ 的子樹沒有 back edge 可以連回 $u$ 或是其 ancestors ,表示 $u$ 是 articulation point ```cpp! enum class Color { WHITE, GRAY, BLACK, }; struct Graph { int num; struct Vertex { int disc; int low; Color color; Vertex *pi; }; std::vector<std::vector<int>> adjList; std::vector<Vertex> vertices; Graph(int n) : num(n), adjList(n), vertices(n) {} void addEdge(int u, int v) { adjList[u].push_back(v); adjList[v].push_back(u); } }; void DFSVisit(Graph &g, int u, int &time, std::unordered_set<int> &arPoints); std::unordered_set<int> findArticulationPoints(Graph &g) { for (auto vertex : g.vertices) { vertex.disc = -1; vertex.low = -1; vertex.color = Color::WHITE; vertex.pi = nullptr; } int time = 0; std::unordered_set<int> arPoints; for (int u = 0; u < g.num; ++u) { if (g.vertices[u].color == Color::WHITE) DFSVisit(g, u, time, arPoints); } return arPoints; } void DFSVisit(Graph &g, int u, int &time, std::unordered_set<int> &arPoints) { auto &vertex = g.vertices[u]; vertex.color = Color::GRAY; vertex.disc = vertex.low = ++time; int children = 0; for (int v : g.adjList[u]) { auto &neighbor = g.vertices[v]; if (neighbor.color == Color::WHITE) { neighbor.pi = &vertex; ++children; DFSVisit(g, v, time, arPoints); vertex.low = std::min(vertex.low, neighbor.low); // Case 1: Root node with multiple children if (!vertex.pi && children > 1) arPoints.insert(u); // Case 2: Non-root node if (vertex.pi && neighbor.low >= vertex.disc) arPoints.insert(u); } else if (&neighbor != vertex.pi) // back edge vertex.low = std::min(vertex.low, neighbor.disc); } vertex.color = Color::BLACK; } ``` 考慮下圖: ![image](https://hackmd.io/_uploads/SyB3BauIyx.png) 我們使用上述演算法來找出 articulation points ```cpp! int main() { Graph g(10); g.addEdge(0, 1); g.addEdge(1, 2); g.addEdge(1, 3); g.addEdge(2, 4); g.addEdge(3, 4); g.addEdge(3, 5); g.addEdge(5, 6); g.addEdge(5, 7); g.addEdge(6, 7); g.addEdge(7, 8); g.addEdge(7, 9); auto articulationPoints = findArticulationPoints(g); for (int vertex : articulationPoints) { std::cout << vertex << ", "; } std::cout << "\n"; return 0; } ``` ![image](https://hackmd.io/_uploads/ryU_bau81g.png) ## Flow Network 給定有向圖 $G = (V,E)$ ,其上有兩點 $s, t \in V$ ,其中 $s$ 稱作 source , $t$ 稱作 sink ,滿足 $\forall v\in V$ , $s$ 可達 $v$ 且 $v$ 可達 $t$ ($s$ 的 in-degree 為 $0$ , $t$ 的 out-degree 為 $0$) $\forall u,v\in V$ 若 $(u,v)\in E$ 則 $(v,u)\notin E$ 令 $c: E \rightarrow \mathbb{R^+}$ 為 capacity function , $f: E \rightarrow \mathbb{R^+}\cup \left\{ 0 \right\}$ 為 flow function 滿足: 1. **cpacity constraint**: $\forall u,v \in V, 0\le f(u,v)\le c(u,v)$ 2. **flow conservation**: $\forall u\in V-\left\{ s,t \right\}, \sum\limits_{v\in V} f(v,u) = \sum\limits_{v\in V} f(u,v)$ (每點流入等於流出) 則稱 $G$ 為一個 flow network, $f$ 為 $G$ 之一 flow 定義 $\left| f \right| = \sum\limits_{v\in V} f(s,v)$ 為 $f$ 之 value Flow network 直覺看來就像是許多管線串連的網路,它有個水源起點 $s$ , $t$ 為終點池 那麼該怎麼求出從起點所能流出的最大總流量呢 (求 $G$ 上具有最大 value 之 flow)?這個問題便是 the maximum flow problem ![image](https://hackmd.io/_uploads/Sk7GeeKUkg.png) ## Residual Network 給定 flow network $G = (V,E)$ ,流量為 $f$ $G$ 之 residual network $G_f = (V,E_f)$ ,其中 $E_f = \left\{ (u,v)\in V\times V | c(u,v) - f(u,v) \gt 0 \right\}$ 而我們會稱 $G_f$ 上 $s$ 到 $t$ 的 simple path 為 augmenting path ### Ford-Fulkerson method 1. 找 augmenting path $P$ 2. $P$ 上最小的權重 $w$ 順邊 $-w$ 反邊 $w$ 3. 重複 1. 跟 2. 直到不存在 $P$ 可找 其策略即 greedy ,我們每次能挑滿就挑滿,然後去更新 residual network 先寫好前置作業 `FlowNetwork` 的部份我覺得其實沒特別需要寫 雖然演算法過程中要兩邊更新 但實際上更新一邊就能看出東西了 所以我就寫個樣子 ```cpp! struct FlowEdge { int from, to, flow, capacity; }; struct FlowNetwork { int num; std::vector<std::vector<int>> adjList; std::vector<FlowEdge> edges; FlowNetwork(size_t n) : num(n), adjList(n) {} void addEdge(int from, int to, int capacity) { edges.push_back({from, to, 0, capacity}); adjList[from].push_back(edges.size() - 1); } }; struct ResidualEdge { int from, to, capacity; }; struct ResidualNetwork { int num; std::vector<std::vector<int>> adjList; std::vector<ResidualEdge> edges; ResidualNetwork(const FlowNetwork &G) : num(G.num), adjList(G.adjList) { for (const auto &edge : G.edges) { edges.push_back({edge.from, edge.to, edge.capacity - edge.flow}); adjList[edge.from].push_back(edges.size() - 1); edges.push_back({edge.to, edge.from, edge.flow}); adjList[edge.to].push_back(edges.size() - 1); } } void augmentFlow(int idx, int amount) { edges[idx].capacity -= amount; edges[idx ^ 1].capacity += amount; } }; // Use DFS to find the augmenting path bool findAugmentingPath(ResidualNetwork &Gf, int s, int t, std::vector<int> &path, std::vector<bool> &visited, int u) { if (u == t) return true; visited[u] = true; for (int idx : Gf.adjList[u]) { const auto &edge = Gf.edges[idx]; if (!visited[edge.to] && Gf.edges[idx].capacity > 0) { path[edge.to] = idx; if (findAugmentingPath(Gf, s, t, path, visited, edge.to)) return true; } } return false; } ``` 再來看演算法的部份就會簡單多了 ```cpp! int fordFulkersonMethod(const FlowNetwork &G, int s, int t) { int maxFlow = 0; ResidualNetwork Gf(G); while (true) { std::vector<int> path(Gf.num, -1); std::vector<bool> visited(Gf.num); if (!findAugmentingPath(Gf, s, t, path, visited, s)) break; int pathFlow = INT_MAX; for (int v = t; v != s;) { const int idx = path[v]; pathFlow = std::min(pathFlow, Gf.edges[idx].capacity); v = Gf.edges[idx].from; } // Update Gf for (int v = t; v != s;) { const int idx = path[v]; Gf.augmentFlow(idx, pathFlow); v = Gf.edges[idx].from; } maxFlow += pathFlow; } return maxFlow; } ``` 時間複雜度為 $O(\left| f^* \right|(\left| V \right| + \left| E \right|)) = O(\left| f^* \right|\left| E \right|)$ 其中 $f^*$ 即 maximum flow 例如下圖: ![image](https://hackmd.io/_uploads/HkqPLbYU1l.png) ```cpp! int main() { FlowNetwork G(6); G.addEdge(0, 1, 10); G.addEdge(0, 2, 5); G.addEdge(1, 2, 4); G.addEdge(1, 3, 4); G.addEdge(1, 4, 5); G.addEdge(2, 4, 9); G.addEdge(4, 3, 6); G.addEdge(3, 5, 10); G.addEdge(4, 5, 8); int maxFlow = fordFulkersonMethod(G, 0, 5); std::cout << "Maximum Flow: " << maxFlow << "\n"; return 0; } ``` ![image](https://hackmd.io/_uploads/B15rv-t8Jl.png) > [!Note] > 依據原始問題的定義,每邊的 capacity 可為任意實數,但 Ford-Fulkerson method 只適用於每邊的 capacity 皆為有理數時 ### Edmonds-Karp Algorithm 看上面 Ford-Fulkerson method 就會發現,這時間複雜度怎麼還跟問題答案 $\left| f^* \right|$ 有關啊? 是的,這牽扯到該方法的大問題,那就是它挑 augmenting path 的方法沒有特別設計過 該方法完全有可能會不斷挑一些很爛的 augmenting path 使得執行效率大幅降低 就算圖本身很小,如果 $\left| f^* \right|$ 很大,那效率還是會很爛,並且就算只是把 $\left| f^* \right|$ 放大,同個網路的效率也會有所不同 考慮下面這個極端情況: ![image](https://hackmd.io/_uploads/HytU5WKIke.png) 如果每次都挑到經過 $(u,v)$ 再 $(v,u)$ 可想而之會多跑超級多回 聽起來就不夠聰明對吧,給你挑,你一眼就知道怎麼挑 Edmonds-Karp 的想法就只差在,它在挑走哪條 augmenting path 時是用 BFS 挑的 所以他的時間複雜度為 $O(\left| V \right|\left| E \right|(\left| V \right| + \left| E \right|)) = O(\left| V \right|\left| E \right|^2)$ ### Maximum Flow and Minimum Cut 給定 $G= (V,E)$ 為 flow network,$s$ 為 source, $t$ 為 sink 1. 若 $(S,T)$ 為 $V$ 之 partition ,即 $S\cup T = V$ 且 $S\cap T = \emptyset$ ,滿足 $s\in S, t\in T$ ,則稱 $(S,T)$ 為 $G$ 之 cut 2. $c(S,T) = \sum\limits_{u\in S}\sum\limits_{v\in T} c(u,v)$ 稱作 $\mathrm{cut} (S,T)$ 之 capacity 若 $f$ 為 $G$ 上的 flow,下列敘述等價: 1. $f$ 為 $G$ 之 maximum flow 2. $G_f$ 中不含 augmenting path 3. $\exists \mathrm{cut} (S',T'), \left| f \right| = c(S',T')$ 也就是說,我們上面求 maximum flow 的同時,也能解出 minimum cut 考試都有可能問,要知道這兩個問題是同一件事 > [!Tip] > $(S,T)$ 為 minimum cut $\Leftrightarrow \sum\limits_{u\in S}\sum\limits_{v\in T} f(u,v) = \sum\limits_{u\in S}\sum\limits_{v\in T} c(u,v)$ 且 $\sum\limits_{u\in S}\sum\limits_{v\in T} f(v,u) = 0$ ### Maximum Matching 給定 $G = (V_1\cup V_2, E)$ 為 biparite graph (離散必考內容,不知道這是什麼的請趕快知道),若 $M\subseteq E$ 滿足 $M$ 中的邊皆不具共同端點,則稱 $M$ 為 $G$ 之 bipartite matching 如果想求 $G$ 上的 maximum bipartite matching $M^*$ ,我們亦可利用解 maximum flow 的技巧 首先要把 $G$ 變成一個 flow network $G'$ 辦法就是在左右兩邊加 source 跟 sink ,source 連到 $V_1$ 中所有點 sink 連到 $V_2$ 中所有點 視所有邊 capacity 都是 $1$ ,利用 Ford-Fulkerson method 求解 $G'$ 之 maximum flow,所求即 $G$ 之 maximum matching 因為 capacity 都 $1$ ,所以使用 Ford-Fulkerson method 的時間複雜度也就 $O(\left| V \right|\left| E \right|)$ 簡單講一下為什麼可以這樣 (~~嘴砲證明法~~): > claim: $G$ 中存在一組 matching $M'$ 使得 $\left| M' \right| = \left| f \right| \Leftrightarrow G'$ 具有整數值的 flow $f$ > ($\Rightarrow$) > $\forall (u,v)\in M, \text{ 若 } (u,v)\in M'$ 則令 $f(s,u) = f(u,v)=f(v,t)=1$ ,否則令 $f(u,v) = 0$ > 則 $f$ 為 $G'$ 之 flow 且可驗證得 $\left| f \right| = \left| M' \right|$ > ($\Leftarrow$) > 給定 $f$ 為 $G'$ 之整數值 flow ,定義 $M' = \left\{ (u,v) | u\in V_1, v\in V_2, f(u,v) \gt 0 \right\}$ > 則 $M'$ 為 $G$ 之一組 matching 且可驗證得 $\left| M' \right| = \left| f \right|$ > > 因此欲求 $G$ 之 maximum matching 相當於求 $G'$ 之 maximum flow > 並且由於 $G'$ 中每邊的 capacity 皆為整數,故可以使用 Ford-Fulkerson method 求 $G'$ 之 maximum flow ,而得 $G$ 之 maximum matching ## 【資工考研】DSA: Graph 全系列 - [【資工考研】DSA: Graph (1)](https://hackmd.io/@RyoukiWei/graph_1) - [【資工考研】DSA: Graph (2)](https://hackmd.io/@RyoukiWei/graph_2)