======问题概述====== 不知道从什么时候开始很多序列问题都可以“上树”,“上树”的意思就是在树上进行询问。比如把非常常见的区间求和\最值或者带修改区间求和\最值放到树上,变成树上两个点之间的路径上的权值求和\最值。而树链剖分可以在不错的时间内实现序列问题上树的求解,其具体方式就是把树按照某种方式分成多条链,然后把这些链排成一个一维区间,只要我们能够保证不跨链操作,问题就又变回了序列问题。 ======重链剖分====== 首先,我们定义点$i$的父亲是$f_i$,深度是$dep_i$,子树大小为$size_i$。在重链剖分中,我们定义**重边**和**轻边**,对于一个节点$i$,它连向节点个数最大的子树的边是**重边**,其余都是**轻边**。**重边**连成的链称为重链。比如下图中,实线代表**重边**,虚线代表**轻边**。 {{:2020-2021:teams:hotpot:24.png?400|}} 可以证明,从一个点到根,最多经过$O(\log n)$条轻边。简单证明:首先,从父亲往儿子走,假设从$x$走到了$y$,如果经过了一条轻边,那么一定有$size_y \le \frac{1}{2} \times size_x$,否则这条边一定是重边。那么,我们最多经过$O(\log n)$条轻边就会到达叶子。又因为对于一个点到根的路径,轻链和重链一定是交替存在,所以**重链**的数量一定不会超过**轻边**的数量,所以重链的数量也是$O(\log n)$的,我们只需要记录每个重链上深度最小的点$top_i$和这条重链起末点的dfs序,然后把这些重链一次放到一个序列里,就可以构成一个在一维序列上查询修改的问题,一般用线段树来维护这个序列。 现在考虑给出树上两个点,如何找到它们之间的路径,首先,如果两个点在同一个重链上,那么我们直接在这条链上操作即可,否则,我们把**链顶深度大**的链上的元素处理完后,从链顶跳到链顶的父亲,从一个重链跳到一个链顶深度更小的重链并重复这一步骤,显然我们最后一定可以把这两个点都跳到它们的LCA所在的重链上,这时我们把剩下的元素进行操作,就完成了对两个点之间路径上的元素的操作。我们都知道线段树的操作复杂度是$O(\log n)$,而由我们之前的证明,我们最多操作$O(\log n)$个重链,所以树链剖分一次操作的复杂度是$O(\log^2 n)$。 =====例题——洛谷P2590[ZJOI2008]树的统计===== ====题目大意==== 给出一棵有点权的树,要求实现单点修改,查询路径权值和以及查询路径最大权值 ====解题思路==== 利用树链剖分解决即可,细节可以看代码注释 ====代码实现==== #include #include #include using namespace std; #define rep(x, y) for(int y = F[x];y;y = Next[y]) //遍历边表的定义 const int maxn = 60100; int n, X[maxn / 2], deep[maxn / 2], size[maxn / 2], fa[maxn / 2]; int F[maxn], v[maxn], Next[maxn], EID = 1; int tot = 0, pos[maxn], cc[maxn]; int x, y, ww; struct Tree{ int l, r, mx, s; Tree() { mx = -2147483647; s = 0; } }node[100005];//线段树节点定义 inline int read()//读入优化 { int a = 0; bool flag = false; char ch; while(!((((ch = getchar()) >= '0') && (ch <= '9')) || (ch == '-'))); if(ch == '-') flag = true; else { a = a * 10; a += ch - '0'; } while((((ch = getchar()) >= '0') && (ch <= '9')) || (ch == '-')) { a = a * 10; a += ch - '0'; } if(flag) a = -a; return a; } inline void add(int f, int t) { Next[EID] = F[f]; v[EID] = t; F[f] = EID++; } inline void scanff() { n = read(); for(int i = 1;i <= n - 1;++i) { x = read(), y = read(); add(x, y); add(y, x); } for(int i = 1;i <= n;++i) X[i] = read(); } inline void dfs(int x)//处理出一个点所有儿子的大小以及点的深度 { size[x] = 1; rep(x, i) { if(v[i] == fa[x]) continue; deep[v[i]] = deep[x] + 1; fa[v[i]] = x; dfs(v[i]); size[x] += size[v[i]]; } } inline void dfsc(int x, int chain)//开始划分重链 { int k = 0;++tot; pos[x] = tot; cc[x] = chain; rep(x, i) if(deep[v[i]] > deep[x] && size[v[i]] > size[k])//找到一个大小最大的儿子 k = v[i]; if(k == 0) return; dfsc(k, chain);//优先把最大的儿子划分到重链里 rep(x, i) if(deep[v[i]] > deep[x] && k != v[i])//给剩余的儿子新划分一条重链 dfsc(v[i], v[i]); } inline void build(int k, int l, int r) { node[k].l = l, node[k].r = r; if(l == r) return; int mid = (l + r) >> 1; build(k << 1, l, mid); build(k << 1 | 1, mid + 1, r); } inline void change(int k, int x, int y) { int l = node[k].l, r = node[k].r, mid = (l + r) >> 1; if(l == r) { node[k].s = node[k].mx = y; return; } if(x <= mid) change(k << 1, x, y); else change(k << 1 | 1, x, y); node[k].s = node[k << 1].s + node[k << 1 | 1].s; node[k].mx = max(node[k << 1].mx, node[k << 1 | 1].mx); } inline int qrmx(int k, int x, int y) { int l = node[k].l, r = node[k].r, mid = (l + r) >> 1; if(l == x && y == r) return node[k].mx; if(y <= mid) return qrmx(k << 1, x, y); else if(x > mid) return qrmx(k << 1 | 1, x, y); else return max(qrmx(k << 1, x, mid), qrmx(k << 1 | 1, mid + 1, y)); } inline int qrs(int k, int x, int y) { int l = node[k].l, r = node[k].r, mid = (l + r) >> 1; if(l == x && y == r) return node[k].s; if(y <= mid) return qrs(k << 1, x, y); else if(x > mid) return qrs(k << 1 | 1, x, y); else return qrs(k << 1, x, mid) + qrs(k << 1 | 1, mid + 1, y); } inline int ss(int x, int y)//查询路径权值和 { int sum = +0; while(cc[x] != cc[y]) { if(deep[cc[x]] < deep[cc[y]]) swap(x,y); sum += qrs(1, pos[cc[x]], pos[x]); x=fa[cc[x]]; } if(pos[x] > pos[y]) swap(x,y); sum += qrs(1, pos[x], pos[y]); return sum; } inline int smx(int x, int y)//查询路径最大值 { int ret = -2147483647; while(cc[x] != cc[y]) { if(deep[cc[x]] < deep[cc[y]]) swap(x,y); ret = max(ret, qrmx(1, pos[cc[x]], pos[x])); x=fa[cc[x]]; } if(pos[x] > pos[y]) swap(x,y); ret = max(ret, qrmx(1, pos[x], pos[y])); return ret; } inline void solve() { build(1, 1, n); for(int i = 1;i <= n;++i) change(1, pos[i], X[i]); int q, a, b; char opt[20]; q = read(); for(int i = 1;i <= q;++i) { scanf("%s%d%d", opt, &a, &b); if(opt[0] == 'C') change(1, pos[a], b); else { if(opt[1] == 'M') printf("%d\n", smx(a, b)); else printf("%d\n", ss(a, b)); } } } int main() { scanff(); dfs(1); dfsc(1, 1); solve(); return 0; } =====树链剖分求LCA===== 在上面我们已经提到,按照跳链的策略,能够把两个点都跳到它们LCA所在的重链上,那么当这两个点都跳到同一个重链上时,我们只需要找其中深度更小的那个就能找到两个点的LCA了,时间复杂度$O(\log n)$。 ======长链剖分====== 与树链剖分不同,我们还可以按照子树中深度最大的叶子深度最大的点进行长链剖分,例如下图 {{:2020-2021:teams:hotpot:25.png?400|}} 我们仿照重链剖分的方式分析,假设我们现在从$i$走到了$f_i$并且经过了一条轻边,那么显然$f_i$至少还有一个儿子,所以根节点至少还有一个深度不小于$dep_i$的子树,那么通过计算我们会发现从一个点到根最多经过$O(\sqrt{n})$条轻边和长链。所以,一般情况下,长链剖分的复杂度是不如重链剖分的。但是特定情况下,长链剖分会比重链剖分更优,比如这个问题——在线查询某个点的第$k$个祖先。 首先,一个点的第$k$个祖先所在的长链的长度一定不小于$k$,因为如果小于,那么显然我们把那个祖先所在的长链改成连到这个点的链,一定比之前的长链剖分要长,说明之前的剖分是错误的。有了这个结论,我们先长链剖分并求出倍增数组,然后暴力求出每条长链的链顶的第$k$个祖先以及它向下的$k$个点,由于长链长度和一定是$n$,所以复杂度为$O(n)$。接下来考虑如何在线求出一个节点$x$的第$k$个祖先。首先假设不大于$k$的最大二进制数为$k_b$,则先通过倍增LCA数组从$x$跳至第$k_b$个祖先$x′$处。由于$k − k_b < \frac{k}{2}$,根据我们之前证明的结论,$x′$所在的长链长度应不小于$k_b$,也就严格大于$k−k_b$。而我们已经预处理出了$x′$所在长链的顶端向上或向下的不少于$k−k_b$个节点,这一定覆盖了$x′$的$k−k_b$倍祖先,所以直接利用这个额外信息向上跳即可。