Contest
A 消耗戰(弱化版)
題意:只有一組詢問的消耗戰(B 題)。
這個題跟虛樹沒有半點關係。只是為 B 題做準備。
令 \(f_u\) 為切斷 \(u\) 與其子樹內所有關鍵點的最小代價(不需要考慮 \(u\) 是關鍵點的情況)。答案為 \(f_1\)。
令 \(mi_u\) 為 \(1 \rightsquigarrow u\) 的最小邊權(特別地,\(mi_1 = \infty\))。一遍 DFS 預處理。
列舉 \(u\) 的兒子 \(v\):
- 若 \(v\) 是關鍵點,則 \(ans \gets ans + mi_v\)。
- 若 \(v\) 不是關鍵點,則 \(ans \gets ans + f_v\)。
還有一種可能:直接切 \(mi_u\) 會更好。所以答案為 \(\min(ans, mi_u)\)。
時間複雜度 \(\Theta(n)\)。
B 消耗戰
P2495 [SDOI2011] 消耗戰
虛樹是什麼
給定樹上的若干個關鍵點 \(S\)。往 \(S\) 新增最少的點,使得 \(S\) 對 LCA 運算封閉。
把新的點集叫做 \(S'\)。如果把 \(S'\) 中的點按照原樹的祖先後代關係連邊,則可以形成一棵小樹。這棵樹被稱為 \(S\) 對應的虛樹。
可以證明,\(|S| \le |S'| \le 2 |S| - 1\),即 \(|S'| = \Theta(|S|)\)。
因此 \(\sum |S'| = \Theta(\sum |S|)\)。在類似本題(限制關鍵點數目之和)的題目當中,保證了時間複雜度。
虛樹的構造方法
虛樹的構造方法有兩種:單調棧和二次排序。其中單調棧用的似乎比較多。但是二次排序好寫好背,所以下文介紹二次排序。
- 預處理 \(dfn\) 和 LCA。
- 把 \(S\) 按 \(dfn\) 排序。
- 列舉 \(S\) 的每對相鄰點對(\(S_{i-1}\) 和 \(S_i\)),將 \(\text{LCA}(S_{i-1}, S_i)\) 加入 \(S'\)。當然 \(S\) 中的所有元素也要加入。
- 把 \(S'\) 按 \(dfn\) 排序,並去重。此時 \(S'\) 已經構造完畢。
- 新建一個空圖 \(g\)。列舉 \(S'\) 的每對相鄰點對(\(S'_{i-1}\) 和 \(S'_i\)),在 \(g\) 上連邊 \(\text{LCA}(S'_{i-1}, S'_i) \rightarrow S'_i\)。虛樹即為 \(g\)。
時間複雜度 \(O(|S| \log |S|)\)。
(備註:單調棧做法的好處在於,在點集已經排序的情況下,藉助 \(O(1)\) 查詢的 LCA 可以做到 \(\Theta(|S|)\) 構造虛樹)
虛樹的構造:正確性
Part 1:為什麼這樣構造的 \(S'\) 對 LCA 封閉
- 引理 1 \(\text{LCA}(S_{i-1}, S_{i+1}) \in \{\text{LCA}(S_{i-1}, S_i), \text{LCA}(S_i, S_{i+1})\}\)。
證明:\(\text{LCA}(S_{i-1}, S_i)\) 和 \(\text{LCA}(S_i, S_{i+1})\) 是祖先後代關係,因為它們都是 \(S_2\) 的祖先。那麼他倆中 dep 小的那個就是 \(\text{LCA}(S_{i-1}, S_{i+1})\)。\(\square\)
- 引理 2 \(\text{LCA}(S_i, S_j) \in \{\text{LCA}(S_{k-1}, S_k) \mid i < k \le j\}\)。即 \(\text{LCA}(S_i, S_j) \in S'\)。
證明:用引理 1 數學歸納法即可。\(\square\)
- 引理 3 \(\operatorname{LCA}\limits_{u \in T} u\) 為 \(T\) 中 \(dfn\) 最小和最大兩個點的 LCA。
(這個好像是個常用結論,但是我不會)
證明:先證明這個點確實是所有點的 CA,然後證明它是 LCA。
設 \(dfn\) 最小和最大的點為 \(a,b\)。
反證:設 \(dfn_a < dfn_c < dfn_b\),且 \(\text{LCA}(a,b)\) 不是 \(c\) 的祖先。根據 \(dfn\) 的性質,\(c\) 不在 \(\text{LCA}(a,b)\) 的子樹內,因此 \(dfn_c < dfn_a\) 或 \(dfn_c > dfn_b\)。矛盾。因此這個點確實是所有點的 CA。
如果這個點不是 LCA(即還有點比它更深),那麼和它是 \(\text{LCA}(a,b)\) 這一點矛盾。
\(\square\)
- 定理 \(S'\) 對 \(\text{LCA}\) 封閉。
證明:對於 \(S'\) 中任意兩個元素,他們的 LCA 必定是 \(S\) 的一個子集的 LCA。透過引理 3 可以轉化為 \(S\) 的一個點對的 LCA,透過引理 2 可知必定在 \(S'\) 中。\(\square\)
Part 2:為什麼對 \(S'\) 這樣連邊是正確的
根據 \(S'\) 對 \(\text{LCA}\) 的封閉性,\(\text{LCA}(S'_{i-1}, S'_i)\) 是 \(S'\) 中的元素,且確實是 \(S_i'\) 的祖先。
因為 \(S'_{i-1}\) 與 \(S'_i\) 按 \(dfn\) 排序後是連續的,所以 \(\text{LCA}(S'_{i-1}, S'_i) \rightsquigarrow S'_i\) 之間沒別的 \(S'\) 中的點了,確實就是 \(\text{LCA}(S'_{i-1}, S'_i)\) 應該連 \(S'_i\)。
虛樹的構造:程式碼
vector<int> g[MAXN];
vector<int> build_virtual_tree(vector<int> S) {
sort(S.begin(), S.end(), [&](int u, int v) {
return dfn[u] < dfn[v];
});
vector<int> t;
t.push_back(S[0]);
for (int i = 1; i < S.size(); i++) {
t.push_back(LCA(S[i-1], S[i]));
t.push_back(S[i]);
}
sort(t.begin(), t.end(), [&](int u, int v) {
return dfn[u] < dfn[v];
});
t.erase(unique(t.begin(), t.end()), t.end());
for (int i = 1; i < t.size(); i++) {
int u = LCA(t[i-1], t[i]), v = t[i];
g[u].push_back(v);
}
return t;
}
虛樹的小結論
\(S = \{S_0, S_1, \cdots, S_{m-1}\}\)(按 \(dfn\) 排序)的虛樹邊權和為:
例題:P3320 [SDOI2015] 尋寶遊戲 能看出 SDOI 挺喜歡虛樹的
C 世界樹
P3233 [HNOI2014] 世界樹
TODO: