「圓方樹」學習筆記 (updating...)
圓方樹的定義
圓方樹是由一個無向圖轉化出的樹形結構。轉化方法為:
- 所有原圖的點為“圓點”。
- 對於每個點雙連通分量:
- 刪去點雙內部“圓點”間的連邊。
- 新建點雙的代表點——“方點”。
- 方點向點雙內的所有點連邊。
舉個例子:
觀察圖片,我們可以得到圓方樹的一些性質:
- 不存在相鄰的圓點或方點。
- 一個圓點同時隸屬於它所鄰接的所有方點所代表的點雙。可見所有非葉子圓點都是原圖的割點。
- 從圓點 \(u\) 到圓點 \(v\) 的樹上路徑所經過的圓點是原圖 \(u\) 到 \(v\) 的所有路徑一定經過的點。
圓方樹的構造
既然跟點雙相關,那肯定是 \(\text{Tarjan}\)
圓方樹的構造演算法實質上就是在求點雙的基礎上構建一個新圖。設當前結點為 \(u\),若發現其兒子(DFS 樹上兒子)\(v\) 的 low[v]>=dfn[u]
,那麼就知道 \(u\) 是一個割點或者是 DFS 樹根。我們不必對此加以區分,直接將 \(u\) 和新建的方點 \(w\) 連邊,然後 \(w\) 向所有退棧的點連邊,圓方樹就構造完成了。
實現
inline void Tarjan ( const int u, const int f ) { dfn[u] = low[u] = ++ dfc, stk[++ tp] = u; adj ( src, u, v ) if ( v ^ f ) { // 列舉原圖上u的臨界點,並保證其不是DFS樹上u的父親。 if ( ! dfn[v] ) { Tarjan ( v, u ), chkmin ( low[u], low[v] ); if ( low[v] >= dfn[u] ) { tre.add ( u, ++ snode ); // snode初值為n,用於計數新建結點。 do tre.add ( snode, stk[tp] ); while ( stk[tp --] ^ v ); // 向每個退棧的點連邊。 } } else chkmin ( low[u], dfn[v] ); } }
細節
在運用圓方樹的過程中,我們往往需要在方點維護一些資訊。為了避免重複,我們一般在割點處單獨維護圓點資訊,而在方點中不考慮割點。(即在上文演算法中,結點 \(snode\) 不考慮 \(u\) 的資訊,只記錄 \(v\) 的資訊。)
圓方樹的運用
「BZOJ 3331」壓力
\(\mathcal{Description}\)
Link.
給定一個 \(n\) 個點 \(m\) 條邊的連通無向圖,並給出 \(q\) 個點對 \((u,v)\),令 \(u\) 到 \(v\) 的路徑所必經的結點權值 \(+1\)。求最終每個結點的權值。
\(n\le10^5\),\(m,q\le2\times10^5\)
\(\mathcal{Solution}\)
看到”必經之點“,應該考慮圓方樹。
對於每個點對,直接在圓方樹上作差分,最後一遍 DFS 求每個圓點的子樹和即可。
詳細題解:my solution。
「洛谷 P4320」道路相遇
\(\mathcal{Description}\)
Link.
給定一個 \(n\) 個點 \(m\) 條邊的連通無向圖,並給出 \(q\) 個點對 \((u,v)\),詢問 \(u\) 到 \(v\) 的路徑所必經的結點個數。
\(n,q\le5\times10^5\),\(q\le\min\{\frac{n(n-1)}2,10^6\}\)。
\(\mathcal{Solution}\)
和上一道幾乎一樣。預處理圓方樹上每個點到根經過的圓點個數,然後求 LCA 計算答案。
詳細題解:my solution。
「APIO 2018」「洛谷 P4630」鐵人兩項
\(\mathcal{Description}\)
Link.
給定一個 \(n\) 個點 \(m\) 條邊的無向圖(不保證聯通),求有序三元點對 \((s,c,f)\) 的個數,滿足 \(s,c,f\) 互不相同,且存在一條從 \(s\) 到 \(c\) 再到 \(f\) 的簡單路徑。
\(n\le10^5\),\(m\le2\times10^5\)。
\(\mathcal{Solution}\)
首先考慮這樣一個問題,若 \(s,c,f\) 在同一點雙中,是否一定滿足條件。
答案是肯定的,這裡不細說。
那麼如果固定 \(s,f\),合法的 \(c\) 就可以在圓方樹 \(s\) 到 \(f\) 路徑上的所有圓點和所有方點所代表的圓點(除去 \(s,f\))。這是因為 \(c\) 取在任意點雙內部,由我們的結論,都一定可以從某點進入點雙,經過 \(c\),再從某點走出點雙。
但簡單的計算會導致重複——一個圓點對多個方點有貢獻。
舉個例子,對於 \(u-w-v\),\(w\) 是圓點,\(u,v\) 是方點,如果我們單純地用點雙大小作為方點的權值,\(w\) 就會在 \(u\) 和 \(v\) 中分別計算一次。
解決辦法很巧妙:將圓點的權值設為 \(-1\)。考慮 \(s\) 到 \(f\) 的路徑必然是”圓-方-圓-……-圓-方-圓“,兩個端點的 \(-1\),去除了 \(s\) 和 \(f\) 的貢獻,中間的 \(-1\) 去除了在左右的”方“中重複的貢獻。那麼,合法的 \(c\) 的數量就是 \(s\) 到 \(f\) 的樹上路徑權值之和。
於是,相當於求樹上點對的路徑權值和。反過來,固定 \(c\),維護子樹資訊求出 \((s,f)\) 的方案數,計算 \(c\) 的貢獻即可。
詳細題解:my solution。
「CF 487E」Tourists
\(\mathcal{Description}\)
Link.
維護一個 \(n\) 個點 \(m\) 條邊的簡單無向連通圖,點有點權。\(q\) 次操作:
- 修改單點點權。
- 詢問兩點所有可能路徑上點權的最小值。
\(n,m,q\le10^5\)。
\(\mathcal{Solution}\)
怎麼可能維護圖嘛,肯定是維護圓方樹咯!
一個比較 naive 的想法是,每個方點維護其鄰接圓點的最小值,樹鏈剖分處理詢問。
不過修改的複雜度會由於菊花退化:修改”花蕊“的圓點,四周 \(\mathcal O(n)\) 個方點的資訊都需要修改。
聯想到 array 這道題,我們嘗試”弱化“方點所維護的資訊。每個方點,維護其圓方樹上兒子們的點權最小值。那麼每次修改圓點,至多就只有其父親需要修改資訊了。
於是,每個方點用 std::multiset
或者常見的雙堆 trick 維護最小值資訊(推薦後者,常數較小),再用一樣的樹剖處理詢問即可。
詳細題解:my solution。