虛樹入門筆記
虛樹學習筆記
前置芝士:LCA,Dfs序
參考內容 部落格園“淺談虛樹”, 洛穀日報“淺談虛樹”(實際上是同一篇/lh)
虛樹的定義
對於一棵給定的 \(n\) 個節點的樹 \(T\),構造新樹 \(T'\) 使其包含指定點集及它們的 LCA, 且節點數最小。這樣的 \(T'\) 被稱為虛樹。
虛樹的應用
虛樹可以解決樹上對於指定點集 \(S\) 的詢問,時間複雜度可以實現 \(\mathrm{O(\left| S\right|log\,n + F(\left|S\right|))}\),其中 F 為樹上解決該問題所需複雜度, 前者則為建樹複雜度。
虛樹的構造
進行 LCA 與 Dfs 序預處理,對於詢問的點集按照 dfs 序排序
維護一個棧,儲存虛樹中從根到當前節點的路徑。
我們考慮對於某一個將要加入虛樹的新點,設其為 \(u\), 此時棧頂為 \(st_{top}\),求出它們的 LCA。
因為根據 dfs 序排過序,新點 u 不可能是 LCA, 於是進行分情況討論:
-
-
LCA 即為 \(st_{top}\),可以直接將 u 壓入棧中。
-
LCA 是兩點的公共祖先且不為 \(st_{top}\) 或 u,說明這樹長這樣:
此時我們開始彈出棧中元素並在 \(st_{top}\) 與 \(st_{top-1}\) 之間連邊真正建虛樹,邊權用深度計算一下就好了。
條件 \(dep_{st[top - 1]} > dep_{LCA}\)
-
執行到最後將棧中點全部彈出並連邊,完成虛樹構建。
建樹程式碼 \(\mathbf{Code:}\)
inline void Inc(int x, int y) { int wei = abs(dep[x] - dep[y]); return e[x].push_back(mp(y, wei)), e[y].push_back(mp(x, wei)); } inline void Tend(int u) { if (tp == 0) return fr.push_back(st[++tp] = u); // 棧中冇點,直接壓入 int LCA = Lca(st[tp], u); while (tp > 1 && dep[st[tp - 1]] > dep[LCA]) Inc(st[tp], st[tp - 1]), --tp; // 舊子樹構建 if (dep[LCA] < dep[st[tp]]) Inc(LCA, st[tp--]); if (!tp || st[tp] != LCA) fr.push_back(st[++tp] = LCA); // LCA 特殊處理 return fr.push_back(st[++tp] = u); // 壓入當前點 u } inline void Build(vector<int> S) { dep[root = 0] = 1e9, sort(S.begin(), S.end(), cmp_dfn); // 點集按照字典序排序 Rep(i, 0, S.size() - 1) Tend(S[i]); //按照 DFS 序逐個壓入點 Rep(i, 0, fr.size() - 1) if (dep[fr[i]] < dep[root]) root = fr[i]; // 定根 while (--tp > 0) Inc(st[tp], st[tp + 1]); // 拿出最後剩下的一條鏈並連邊 }
構造複雜度
瓶頸在於點集排序和樹上 LCA ,加上了個棧,每個元素進棧一次出棧一次,具體為\(\mathrm{O(\sum (|S|\ log\,|S|) + \sum(|S|\ log\,n)+ |S|)}\),常數不計。
瓶頸複雜度 \(\mathrm{O((\sum|S|)\ log\,n)}\)。
注意每次回答完詢問後的清空虛樹操作一定要注意時間複雜度,memset 大多致死。
當然也要注意空間複雜度,如果開了靜態陣列,就要算算是否會爆空間,當然 vector 是一個好選擇。雖說很多虛樹的題空間都會給夠。
建議例題
入門例題首選 CF613D Kingdom and its Cities.
再有 [SDOI2011]消耗戰