<style> .reveal .slides { text-align: left; font-size:30px; } </style> ## Graph Problem Misc - Constraint Difference System - Centroid Decomposition - Tree Hash - Round-square tree --- ## 差分約束系統 ### Constraint Difference System 是求解關於一組不等式之方法。 $n$ 個變數和 $m$ 個約束條件組成 其中每個約束條件形如 ${ x_{j}-x_{i}\leq b_{k}(i,j\in [1,n],k\in [1,m])}$ 則稱其為差分約束系統。 ---- ## 例子 ${x_{1}-x_{2}\leq 8}$ ${x_{1}-x_{3}\leq 2}$ $\to x_{1}=3, x_{2}=-5, x_{3}=1$ ${x_{1}-x_{2}\leq 1}$ ${x_{1}-x_{3}\leq -1}$ ${x_{3}-x_{2}\leq 0}$ $\to$ 無解 <div style="position: absolute; right: 10%; top:0%;"> ![](https://i.imgur.com/uA2FxQR.png =250x) </div> <div style="position: absolute; right: 10%; top:80%;"> ![](https://i.imgur.com/EaMjJwm.png =250x) </div> ---- ## 作法 轉成圖論問題 對於所有 $x_{j}-x_{i}\leq k$ 連接一條邊 $(i, j)$,權重為 $k$ 最後再設置一個起點 $s$,連向所有邊邊權為 $0$ 從起點 $s$,跑 Bellman Ford/SPFA , 若出現負環則代表這組不等式無解 ---- ## 建圖 ${x_{1}-x_{2}\leq 1}$ ${x_{1}-x_{3}\leq -1}$ ${x_{3}-x_{2}\leq 0}$ ![](https://i.imgur.com/LySYtE5.png) ---- ## 結果 跑完 Bellman Ford/SPFA 如果發現有負環,則此組不等式無解 否則所有點到起點的距離為其中一組解 ---- ## 變化 | 限制 | 建邊 | | -------- | -------- | | $x_{j}-x_{i}\leq k$ | addEdge($i, j, k$) | | $x_{j}-x_{i}\geq k$ | addEdge($j, i, -k$) | | $x_{j} = x_{i}$ | addEdge($i, j, 0$), addEdge($j, i, 0) | ---- ## 區間問題轉換 給 $n$ 個區間 $[l_i, r_i]$ 以及 $c_i$ 要構造出一個序列 $\forall i\in [1, n]$ 滿足區間 $[l_i, r_i]$ 內的總和至少為 $c_i$, 構造出總和最小的序列。 ---- ## 轉換為前綴和處理 $[l_i, r_i]$ 至少為 $c_i$ 等價於前綴和 $pre_r$ - $pre_{l-1}$ 至少為 $c_i$ ---- 等價於前綴和 $pre_r - pre_{l-1}$ 至少為 $c_i$ $\to pre_r - pre_{l-1}\ge c_i$ 把序列上每個位置當成節點,即可轉換為差分約束問題 ! --- ## Centroid Decomposition 重心剖分 (樹分治),透過每次不斷找到當前子圖的重心,重心拔掉再遞迴下去分割的子樹找各自的重心 重心的定義為一棵樹把節點 $G$ 移除之後,其最大的連通塊會是最小的,而我們會稱節點 $G$ 是樹重心。 ---- ## 如何找到樹重心 先選一個點為根節點,暴力 $O(n)$ 跑過所有節點紀錄每個節點的子樹大小 $s_i$。 假設子樹大小分別為 $s_1, s_2,... s_k$,則父節點方向的子樹大小為 $n - 1 - \sum{s_i}$ 樹重心為 max(n-1-$\sum{s_i}, s_1, s_2...s_k)$ 中最小的節點 ---- ## 重心的性質 - 把重心拔掉之後, 所有子樹的大小會 $\le\frac{n}{2}$ - 一棵樹最多兩個樹重心 ---- ## 重心剖分 1. 找到樹重心 2. 把樹重心從圖上移除 3. 遞迴下去連到的子圖 4. 如果子圖大小 >1 回到步驟 1 ![](https://i.imgur.com/scl8ghu.png) ---- 找到<span style = "color:red;">樹重心 </span> ![](https://i.imgur.com/9eqXdSp.png) ---- 把樹重心從塗上移除,剩下的子圖分別去找各自的樹重心 ![](https://i.imgur.com/FSKOW60.png) ---- 找到子圖各自的<span style = "color:orange;">樹重心 </span> ![](https://i.imgur.com/c47NCd7.png) ---- 把樹重心移除,剩下的子圖分別去找個字的樹重心 ![](https://i.imgur.com/LXrxUzN.png) ---- 找到各自的<span style = "color:green;">樹重心 </span> ![](https://i.imgur.com/cp4eK8v.png) ---- ## 複雜度 每次把圖從重心分割,每個子圖最大為原本的一半 最多遞迴 $\log N$ 層 $T(n)=\sum T(s_i) + O(n)$ 複雜度 $O(N\log{N})$ ---- ## 重心樹 對於當前這層的重心與下一層子圖的重心連接可以構出一顆重心樹 由於重心剖分的性質,樹高度不超過 $\log n$ ![](https://i.imgur.com/cp4eK8v.png) $\to$ ![](https://i.imgur.com/IWJ2yGG.png) ---- ![](https://i.imgur.com/cp4eK8v.png =x300) $\to$ ![](https://i.imgur.com/i87ocvA.png =x300) ---- ## 引入例題 CF 342 E. Xenia and Tree 給你 $n$ 個節點的樹,一開始每個點都是藍色,會有 $m$ 次操作 - 把某個藍點塗成紅色 - 查詢某個點最近的紅點有多遠。 $n, m\le 10^5$ ---- $n, m\le 10^5$ 如果暴力做,直接用一個陣列紀錄每個節點的顏色 - 把某個藍點塗成紅色 $\to$ O(1) 更新某個節點的顏色 - 查詢某個點最近的紅點 $\to$ BFS 找最近的紅色節點 $O(n)$ 很明顯如果每次都是第二種操作,總複雜度為 $O(nm)\to$ TLE ---- ### 換一種作法 先選一個節點為根 每個節點紀錄到子樹中最近的紅色節點距離,沒有則視為無限大 1. 把某個藍點 $v$ 塗成紅色 - 從當前節點 $v$ 開始往根節點方向更新所有祖先的距離 ![](https://i.imgur.com/4we3zrp.png =x300) ---- 2. 查詢某個點 $v$ 最近的紅點 - 從當前節點 $v$ 開始往上走 - 每次判斷到某個祖先 $u$ 的距離 + 祖先 $u$ 到他最近的子樹紅色節點距離取最短的 ![](https://i.imgur.com/QxTPRRw.png =x300) ---- 1. 把某個藍點 $v$ 塗成紅色 - 從當前節點 $v$ 開始往根節點方向更新所有祖先的距離 2. 查詢某個點 $v$ 最近的紅點 - 從當前節點 $v$ 開始往上走 - 每次判斷到某個祖先 $u$ 的距離 + 祖先 $u$ 到他最近的子樹紅色節點距離取最短的 以上兩種 case 只要到根節點距離很遠複雜度就變 $O(n)$ 一樣最差會變成 $O(nm)$ ---- ### 但如果樹是平衡的 ? 如果樹高最高為 $\log n$ 1. 把某個藍點 $v$ 塗成紅色 - 從當前節點 $v$ 開始往根節點方向更新所有祖先的距離 2. 查詢某個點 $v$ 最近的紅點 - 從當前節點 $v$ 開始往上走 - 每次判斷到某個祖先 $u$ 的距離 + 祖先 $u$ 到他最近的子樹紅色節點距離取最短的 則複雜度變成 $O(m \log n)$ ---- ## 用剛剛的重心樹 ? 會發現重心剖分建出來的重心樹樹高最差為 $\log n$ ![](https://i.imgur.com/cp4eK8v.png =x300) $\to$ ![](https://i.imgur.com/i87ocvA.png =x300) ---- ### 把節點 $v$ 塗成紅色 用 mn 陣列紀錄子樹中最近的紅色節點距離,沒有則視為無限大 對於每一次塗色,從當前節點往祖先方向走到重心樹根節點 更新從塗色節點 $v$ 到當前走訪到的節點 $u$ 距離是否為更近的紅色節點 mn[u] = min(mn[u], dis(u, v)) ---- 以塗色節點 5 為例 ![](https://i.imgur.com/4we3zrp.png =x300) ![](https://i.imgur.com/cp4eK8v.png =x300) 會更新節點 5, 4, 1 到紅色的最短 ---- ### 詢問節點 $v$ 到最近紅色節點距離 從當前節點往重心樹根節點方向走 每次問走訪到的節點 $u$ 與詢問節點 $v$ 的距離+走訪到的節點 $u$ 到子樹中最近的紅色節點距離 ans = min(ans, dis(u, v) + mn[u]) ---- 詢問節點 7 到紅色最短 ![](https://i.imgur.com/QxTPRRw.png =x300) ![](https://i.imgur.com/cp4eK8v.png =x300) 答案會是 ```txt min( 節點 7 到最近紅色點距離, 節點 1 到最近紅色點距離 + 節點 1 到節點 7 的距離) = min(∞, 2+2) = 4 ``` ---- ### 為甚麼這樣是好的 ? 只更新從當前節點到重心樹根結點路徑上的距離,其他節點呢 ? 可以分成兩種 case : 1. 最近的紅色節點在重心樹的子樹中 2. 最近的紅色節點在往根節點方向 ---- #### 1. 最近的紅色節點 $u$ 在重心樹的子樹中 如果節點 $u$ 在詢問節點 $v$ 的子樹中 則在塗色時已經更新過詢問節點 $v$ 到最近紅色節點的距離 因此答案為 mn[v] = dis(u, v) ---- #### 2. 最近的紅色節點 $u$ 在往根節點方向 則到最近的紅色節點必定會先經過詢問節點 $v$ 的某個祖先 $a$ 因此答案為祖先 $a$ 的子樹中最近的節點 + 自己到祖先 $a$ 的距離 mn[a] + dis(a, v) ---- ### 複雜度 每次會從當前節點走到重心樹的根節點 每次會計算樹上兩點之間的距離更新最近距離 由於樹高不超過 $\log n$ 算樹上兩點之間的距離可以 $O(\log n)$ 找 lca, 再透過深度 dep[u]+dep[v]-2*dep[lca] 得到 因此每次操作的複雜度為 $O(\log^2n)$ 總複雜度為 $O(m\log ^2n)$ ---- ### 細節 注意計算距離是算原本那棵樹上的距離 ---- ### 另外一個例題 CSES. Fixed-Length Paths I 給一棵 $N$ $(1\le N\le 2\cdot 10^5)$ 個節點的無根樹,樹上有幾個點對的距離為 $k$ 也就是有幾個長度為 $k$ 的簡單路徑 ---- ### 考慮分治 選一個點 $v$ 計算所有經過他且長度為 $k$ 的路徑 把點 $v$ 拔掉再分成各自的連通子圖遞迴下去計算長度為 $k$ 的路徑 ---- ![](https://i.imgur.com/6IXgNYL.png =400x) 經過當前點 $v$ 的且長度為 $k$ 的路徑分成兩種 1. 以當前點 $v$ 為起點往外的路徑 2. 經過當前點 $v$ 的且不為起始點的路徑 ---- 1. **以當前點 $v$ 為起點往外的路徑** 第一種我們可以從點 $v$ 開始 DFS 紀錄從點 $v$ 到當前節點的長度,如果長度為 $k$ 則答案 + 1 ```cpp= auto dfs = [&](int x, int f, int d) -> void{ if(d == k) ++ans; for(int i : edge[x]){ if(i == f || vis[i]) continue; dfs(i, x, d+1); } }; for(int i : edge[v]){ dfs(i, v, 1); // 遞迴從 v 連出去的節點 i } ``` ---- 2. **經過當前點的且不為起始點的路徑** 第二種等價於從點 $v$ 開始往兩棵相異子樹方向長度分別為 $a, b$ 且 $a+b = k$ 的路徑 我們可以算往某個子樹長度為 $a$ 的路徑有幾個,乘上往其他子樹路徑長度為 $k-b$ 有幾個。 每個子樹都去計算即為答案 ---- 每次從點 $v$ 開始往不同的子樹去遞迴,計算每個子樹當前走到的距離 $d$ 與前面計算過有幾個距離為 $k-d$ (儲存在 cnt 陣列裡) 算完之後再把當前子樹的距離加進去 cnt 陣列裡 ```cpp= auto dfs = [&](int x, int f, int d, bool add) -> void{ if(d == k){ //case 1 ans++; return; } if(add) ans += cnt[k - d]; else cnt[d]++; for(int i : edge[x]){ if(i == f || vis[i]) continue; dfs(i, x, d+1, add); } }; fill(cnt, cnt+sz, 0); for(int i : edge[v]){ dfs(i, v, 0, 0); // 計算完當前子樹的距離與前面窮舉過的距離加進答案 dfs(i, v, 0, 1); // 把當前子樹的距離存進去 cnt } ``` ---- 每次選一個節點 $v$ 計算經過他且長度為 $k$ 的路徑 再把節點 $v$ 要怎麼選會是最好的 ? 每次選節點 $v$ 計算長度為 $k$ 的路徑複雜度最差為 $O(n)$ 如果選重心? ---- 回想一下重心剖分的複雜度計算 $T(n)=\sum T(s_i) + O(n)$ 每次會跑過當前連通塊所有節點,找到重心之後把重心拔掉 再繼續遞迴下去 --- ## Tree Hash 根據樹的形狀把整棵樹 Hash 成一個值。 進而可以 `O(1)` 判斷兩棵樹是否為同構 ---- ## isomorphism 同構圖的定義為給定兩張圖其節點數量相同,且在兩張圖重新編號後能夠使得任兩個點對其連邊狀況相同 ![](https://i.imgur.com/53NG3In.png =300x) ![](https://i.imgur.com/ULch3qY.png =300x) 上兩張圖為同構圖,把左邊那張編號為 3, 4 的節點交換編號後,其任一點對連邊狀況相同 ---- ## Rooted Tree Isomorphism 給定兩棵有根樹,判斷是否為同構的有根樹 ![](https://i.imgur.com/gQJvJTt.png =x300) ![](https://i.imgur.com/dTLX5We.png =x300) 以上為同構有根樹,每個節點的子節點可以重新排列順序後相同 ---- ## radix-sort style compression 給每種有根樹一個 unique id, 把每個兒子的 id 排序用 vector 存進 map 中維護 ```cpp map<vector<int>, int> id; int dfs(int x, int f){ vector<int> sub; for (int v : edge[x]){ if (v != f) sub.push_back(dfs(v, x)); } sort(sub.begin(), sub.end()); if (!id.count(sub)) id[sub] = id.size(); return id[sub]; } ``` ---- ## 複雜度 每個節點只遍歷一次, 每次只排序自己的子節點 id 值並存進 map 中 $O(N\log N)$ --- ## 圓方樹 ### Block forest, Round-square tree 一種將圖變成樹的方法,使得一些圖上問題能夠在 $O(\log n)$ 解決 ---- ## 例題 --- CSES 1705. Forbidden Cities 給定一張無向圖,$q$ 筆詢問 每筆詢問為是否存在一條路徑使得點 $a$ 到點 $b$ 不經過點 $c$ $1\le n, m, q\le 10^5$ <span>會發現題目即為判斷點 c 是否為 a-b 路徑上的關節點<!-- .element: class="fragment" data-fragment-index="1" --></span> ---- ## 建樹 而要判斷路徑上的問題,我們可以用 BCC 縮點,把每個 BCC 變成一個點 對於每個 BCC 與相連的關節點建邊 ![image](https://hackmd.io/_uploads/Skm9uzMSp.png) ![image](https://hackmd.io/_uploads/BkI4FzGH6.png) 如此一來,建出來的圖會變成一顆樹,總共 $b+c$ 個點 $b$ 為 BCC 數量,$c$ 為關節點數量 ---- ## 實作 找出所有關節點,以及把非關節點的點都縮點成 BCC 的編號 實作上方便,我們可以把 BCC 的編號從 n+1 開始 ```cpp= int num[MXN]; vector<int> edge[MXN]; // new graph vector<vector<int>> bcc = BCC.solve(); for(auto i : bcc){ for(int j : i) num[j]++; } for(int i = 0; i < bcc.size(); i++){ auto b = bcc[i]; if(b.size() == 2){//bridge edge[b[0]].push_back(edge[b[1]]); edge[b[1]].push_back(edge[b[2]]); bln[b[0]] = b[0]; bln[b[1]] = b[1]; } else{ for(int j : b){// set bcc index to n+1+i if(num[j] > 1){// articulation points edge[num[j]].push_back(n+1+i); edge[n+1+i].push_back(num[j]); bln[num[j]] = num[j]; } else{ bln[j] = n+1+i; // set node to bcc index } } } } ``` ---- 建出樹之後,即可進行樹上操作 如這題是判斷點 $c$ 是否在點 $a-b$ 的路徑上 可以使用 LCA 維護 或者其他需要修改的題目也可以搭配樹鏈剖分進行動態操作
{"title":"Graph Problem Misc","description":"重心剖分 (樹分治),透過每次不斷找到當前子圖的重心,重心拔掉再遞迴下去分割的子樹找各自的重心","contributors":"[{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":10385,\"del\":1405}]"}
    458 views
   Owned this note