1. 程式人生 > 實用技巧 >虛樹入門筆記

虛樹入門筆記

虛樹學習筆記

前置芝士: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, 於是進行分情況討論:

    1. LCA 即為 \(st_{top}\),可以直接將 u 壓入棧中。

    2. LCA 是兩點的公共祖先且不為 \(st_{top}\) 或 u,說明這樹長這樣:

    此時我們開始彈出棧中元素並\(st_{top}\)\(st_{top-1}\) 之間連邊真正建虛樹,邊權用深度計算一下就好了。

    條件 \(dep_{st[top - 1]} > dep_{LCA}\)

    時不斷執行, 最後棧頂與 LCA 連邊,彈出棧頂,壓入 u。

執行到最後將棧中點全部彈出並連邊,完成虛樹構建。

建樹程式碼 \(\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]消耗戰