# 虛樹與樹鏈剖分 --- ## 虛樹 ---- 例題: https://www.luogu.com.cn/problem/P2495 https://atcoder.jp/contests/abc359/tasks/abc359_g ---- - 維護樹上性質時,如果會進行計算的點數不多(以下稱為關鍵點),可以幫那些點建一棵虛樹,把事情改成在虛樹上面做 - 虛樹的結構為包含所有關鍵點及關鍵點中任兩點的 $LCA$ 且滿足原樹祖孫關係的最小子樹 - 對於一棵大小為 $n$ 的樹,若其中含有 $k$ 個關鍵點,可以在 $\mathcal{O}(k \log n)$ 的時間內建好一顆 $\mathcal{O}(k)$ 大小的虛樹 ---- 建構方法如下: ```cpp Graph constructVirtualTree(Graph &originalTree, vector<int> &keyPoints) { // Sort the key points by DFS order sort(keyPoints.begin(), keyPoints.end(), [&](int u, int v) { return originalTree.dfsOrder(u) < originalTree.dfsOrder(v); }); // Add the LCA of adjacent key points int k = keyPoints.size(); keyPoints.reserve(2 * k - 1); for (int i = 0; i < k - 1; i++) keyPoints.push_back(originalTree.getLCA(keyPoints[i], keyPoints[i + 1])); // Sort again and remove duplicates sort(keyPoints.begin(), keyPoints.end(), [&](int u, int v) { return originalTree.dfsOrder(u) < originalTree.dfsOrder(v); }); k = unique(keyPoints.begin(), keyPoints.end()) - keyPoints.begin(); keyPoints.resize(k); // Initialize the virtual tree Graph virtualTree; virtualTree.initializeSize(originalTree.size()); // Connect LCA(u, v) to v for each adjacent key points u and v for (int i = 0; i < k - 1; i++) virtualTree.addEdge(originalTree.getLCA(keyPoints[i], keyPoints[i + 1]), keyPoints[i + 1]); return virtualTree; } ``` ---- 證明如下: - 分為「包含任兩點 $LCA$」「滿足祖孫關係」「為最小子樹」三個步驟 - 取 $DFS$ 序相鄰兩點的 $LCA$ 可以涵蓋任意兩點的 $LCA$,因為對於任兩個關鍵點,其 $LCA$ 至少有兩個以子節點為根的子樹包含關鍵點,取到橫跨這些子樹的相鄰關鍵點時就會算到這個 $LCA$ ---- - 建邊時 $LCA(u, v)$ 一定在虛樹中(已加入所有點的 $LCA$)且在原樹中一定是 $v$ 的祖先,故滿足原樹中的祖孫關係 - $LCA(u, v)$ 至 $v$ 的鏈中必定沒有其他關鍵點,否則其與 $u$ 的 $DFS$ 序距離較 $v$ 小,與 $u, v$ 為相鄰兩點矛盾,而此方法建出 $k - 1$ 條邊,為最小子樹 --- ## 樹鏈剖分 ---- 例題: https://www.luogu.com.cn/problem/P3379 https://www.luogu.com.cn/problem/P3384 ---- - 樹鏈剖分的思想是將樹劃分為一些鏈,然後以線性的資料結構維護樹上的性質 - 定義點的重子節點為子樹最大的子節點,連接重子節點的邊稱為重邊,其他均稱為輕邊 - 樹鏈剖分的結構是將所有連續的重邊當成鏈,稱為重鏈,重鏈間以輕邊相連(包含落單的節點) ---- 這樣可以有一些好性質:($n$ 為樹的大小) - 樹上任意路徑最多包含 $\mathcal{O}(\log n)$ 條重鏈 - 同一條重鏈上的點 $DFS$ 序連續 ---- 建構方法如下: ```cpp struct Decomposition { vector<vector<int> > tree; vector<int> depth, father, size, heavy, idx, top; int cur; Decomposition(const vector<vector<int> > &tree, int size, int root) : tree(tree), depth(size, -1), father(size, -1), size(size, 1), heavy(size, -1), idx(size), top(size) { depth[root] = cur = 0; build(root); decompose(root, root); } void build(int u) { for (int v: tree[u]) if (depth[v] == -1) { depth[v] = depth[u] + 1, father[v] = u; size[u] += (build(v), size[v]); if (heavy[u] == -1 || size[v] > size[heavy[u]]) heavy[u] = v; } } void decompose(int u, int t) { idx[u] = cur++, top[u] = t; if (heavy[u] == -1) return; decompose(heavy[u], t); for (int v: tree[u]) if (v != father[u] && v != heavy[u]) decompose(v, v); } int lca(int u, int v) { while (top[u] != top[v]) depth[top[u]] > depth[top[v]] ? u = father[top[u]] : v = father[top[v]]; return depth[u] < depth[v] ? u : v; } }; ``` ---- 證明如下: - 由定義可知,每向下經過一條輕邊所在的子樹大小至少會除以二,把任意路徑根據最高點分為兩半,每半向下走最多經過 $\mathcal{O}(\log n)$ 條輕邊,即最多包含 $\mathcal{O}(\log n)$ 條重鏈 - 第二次 $DFS$ 的時候先走重邊可以保證重邊上下端的 $DFS$ 序差一,故重鏈上 $DFS$ 序連續
{"title":"虛樹與樹鏈剖分","description":"可愛的虛樹與樹鏈剖分:D","contributors":"[{\"id\":\"40538268-17e3-4dd2-91c0-0d5f3e29922e\",\"add\":30458,\"del\":24543}]"}
    469 views