其实本人学树剖时还完全没到学树剖的水平,但本着在图论这一块的兴趣就看了看,结果发现树剖还挺简单?
树剖是什么呢?看看它的全称:“树链剖分”。顾名思义,就是把一棵树解剖成一个个链子咯。然后在每条链上维护需要的信息,比如最小值、最大值、权值和什么的。这棵树的红线就是剖出来的链条:特别地,一个点也算一条链.
但是问题来了,假如树上有两个点,我怎么知道他们俩是不是在同一条链子上呢?所以我们还需要用一个东西来给链子上的所有节点染色,这样就可以回答刚才的问题了。怎么染呢?我们想一个问题,如果我要查树上两个点之间节点的权值的最大值,怎么查?这里就有两种情况:
1.这两个点在同一条链上,这样的话就可以直接查出链上两点的区间中的最小值了。
2.这两个点不在同一条链上。我们就可以让两个点不断往上跳,直到跳到他们属于同一条链为止。而树剖的关键之一就是这个跳法。如果朴(bao)素(li)一点,怎么跳?那当然是一个点一个点地往上跳,那么时间复杂度就是O(N),对于m次查询就是O(NM)。那我还要树剖有个屁用。。。所以要换一种优雅点的。现在我们不是把树都剖成链了吗?为什么不一条链一条链地跳呢?我们可以首先记录下当前节点u所在的链的顶端top[u],若要跳出这条链,就让u=fa[top[u]](不是top[u])。就像这样不断地跳,直到两个点的top相同为止。
以上就是树剖的基本思路。那么我们为什么要树剖这种东西呢?“有时可以用来暴力。”——学校一个大佬。树剖确实是暴力,只不过非常地优雅。如果一道题需要查询树上两点的区间中的某些信息,我们可以直接查这个区间,而不是枚举区间的每个点。所以我们需要记录下树上的每一个区间,用线段树来维护,而这些区间就是上面所说的链条了。这样一来,每次查询的复杂度就降为O(logN)了。当然这是最理想的情况。而为了让我们的树剖的复杂度尽量接近这个等级,我们就需要想办法把树剖成logN条链子。所以这里引入一个概念:重链剖分。
所谓重链,给人的感觉就是这条链特别重。也就是这条链的节点数特别多了。而重链剖分的过程就是:对于当前节点u,找出以它的子节点为根的子树中节点最多的一棵子树,假如这颗子树的根为v,那么就给这条链加上节点v,并递归下去,对v也如法炮制。这样构造出的链就很接近logN个。而u的这个子节点v有个定义,叫做u的“重儿子”。
那么,怎么让一条链所表示的区间内的所有节点都是连续的整数呢?想想刚才的操作,我们在建链时是沿着链递归下去的,如果我们这时记下每个节点的时间戳的话,是不是这条链上的时间戳就是一串连续的整数了呢?这样就解决了刚才的问题。所以,我们在剖树时,需要两次DFS,第一次找出每个节点的重儿子,第二次沿着重儿子走下去,记下每个点被遍历到的时间戳:
void dfs_getson(int u){//第一次DFS size[u] = 1; for(int i = head[u]; ~i; i = e[i].next){ int v = e[i].to; if(v == fa[u]) continue;//注意不能往回走 fa[v] = u; dep[v] = dep[u] + 1;//计算深度,等会要讲. dfs_getson(v); size[u] += size[v]; if(size[v] > size[son[u]]) son[u] = v;//记录u的重儿子. } }
void dfs_rewrite(int u, int tp){//第二次DFS top[u] = tp, dfn[u] = ++tot, id[tot] = u;//id为dfn的反函数,由于链上的区间由dfn组成,为了知道链上某个点的编号我们记录下id.top为u所在链的顶端 if(son[u]) dfs_rewrite(son[u], tp);//优先往重儿子走. for(int i = head[u]; ~i; i = e[i].next){ int v = e[i].to; if(v != fa[u] && v != son[u]) dfs_rewrite(v, v);//往不是重儿子的儿子(轻儿子)走 } }
像这样,一棵树就被我们剖成了链。不过这些链现在还没有什么用,我们需要用线段树来维护它们。所以首先是构造。这里用区间最大值为例,其它的类似:
void build(int d, int l, int r){ t[d].l = l, t[d].r = r; if(l == r){ t[d].mmax = val[id[l]]; return; }//要注意这里的lr都是dfn值 int mid = l + r >> 1; build(d << 1, l, mid);//构造左儿子 build(d << 1 | 1, mid + 1, r);//构造右儿子 t[d].mmax = max(t[d << 1].mmax, t[d << 1 | 1].mmax); } //main函数中 build(1, 1, tot);
然后是单点查询:
int getmax_vertex(int d, int x){ if(t[d].l == t[d].r) return t[d].mmax; int mid = t[d].l + t[d].r >> 1; if(x <= mid) return getmax_vertex(d << 1, x); else return getmax_vertex(d << 1 | 1, x); } //main函数中 ans = getmax_vertex(1, dfn[u]);
单点修改:
void change_vertex(int d, int x, int w){ if(t[d].l == t[d].r){ t[d].mmax = w; return; } int mid = t[d].l + t[d].r >> 1; if(x <= mid) change_vertex(d << 1, x, w); else change_vertex(d << 1 | 1, x, w); t[d].mmax = max(t[d << 1].mmax, t[d << 1 | 1].mmax); } //main函数中 change_vertex(1, dfn[u], w);
需要注意的是对区间的操作。其实上面已经提到了,若区间的两个端点u,v在同一条链上,那么直接操作就可以了。而不在一条链上时,只需要跳到同一条链上即可。那么谁跳呢?假如我们让top深度小的点跳,深度只能越跳越小,永远小于另一个点,直到跳到根节点也什么也做不了(可以画个图来理解)。所以我们应该选择top深度大的开始跳,边跳边对跳过的链操作,最后跳到同一条链时再操作一次。
于是就有了区间查询(延迟标记就懒得写了):
int getmax_xtoy(int d, int l, int r){ if(l <= t[d].l && t[d].r <= r) return t[d].mmax; if(t[d].f) down(d); int mid = t[d].l + t[d].r >> 1, ans = 0; if(l <= mid) ans = max(ans, getmax_xtoy(d << 1, l, r)); if(r > mid) ans = max(ans, getmax_xtoy(d << 1 | 1, l, r)); return ans; } //main函数中 int ans = 0; while(top[u] != top[v]){ if(dep[top[u]] > dep[top[v]]) swap(u, v);//从深度大的开始跳 ans = max(ans, getmax_xtoy(1, dfn[top[v]], dfn[v])); v = fa[top[v]];//注意要调到顶端的父亲去 } if(dep[u] > dep[v]) swap(u, v); ans = max(ans, getmax_xtoy(1, dfn[u], dfn[v]));
以及区间修改:
void change_xtoy(int d, int l, int r, int w){ if(l <= t[d].l && t[d].r <= r){ t[d].mmax = w; t[d].f = w; return; } if(t[d].f) down(d); int mid = t[d].l + t[d].r >> 1; if(l <= mid) change_xtoy(d << 1, l, r, w); if(r > mid) change_xtoy(d << 1 | 1, l, r, w); t[d].mmax = max(t[d << 1].mmax, t[d << 1 | 1].mmax); } //main函数中 while(top[u] != top[v]){ if(dep[top[u]] > dep[top[v]]) swap(u, v); change_xtoy(1, dfn[top[v]], dfn[v], w); v = fa[top[v]]; } if(dep[u] > dep[v]) swap(u, v); change_xtoy(1, dfn[u], dfn[v]);
没错,树链剖分就是个码量惊人的东西。
现在我们可以愉快地刷例题了。
P3384 【模板】树链剖分
题目描述
如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z
操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和
操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z
操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和
输入输出格式
输入格式:
第一行包含4个正整数N、M、R、P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。
接下来一行包含N个非负整数,分别依次表示各个节点上初始的数值。
接下来N-1行每行包含两个整数x、y,表示点x和点y之间连有一条边(保证无环且连通)
接下来M行每行包含若干个正整数,每行表示一个操作,格式如下:
操作1: 1 x y z
操作2: 2 x y
操作3: 3 x z
操作4: 4 x
输出格式:
输出包含若干行,分别依次表示每个操作2或操作4所得的结果(对P取模)
这不是模板题。这题涉及了区间修改、区间查询,以及——子树修改和查询???这就是这题的难点了。子树并不只是简单的一条链,这怎么办呢?实际上子树的东西还简单地可怜。想想,我们在对区间操作时的对象等同于一些连续的dfn值,而子树呢?子树的所有点的dfn值不也是连续的一串吗?所以我们在第二次搜索时不仅记录下节点x的dfn值,也记下以x为根的子树的dfn最大值:
void dfs_rewrite(int u, int tp){ top[u] = tp, dfn[u] = ++tot, id[tot] = u; if(son[u]) dfs_rewrite(son[u], tp); for(int i = head[u]; ~i; i = e[i].next){ int v = e[i].to; if(v != fa[u] && v != son[u]) dfs_rewrite(v, v); } cnt[v] = tot;//递归完子树后的tot就是dfn最大值 }
而对子树的操作也就可以写得出来了:
inline void getsum_sontree(){//查询子树和 int u; scanf("%d", &u); ans = getsum(1, dfn[u], cnt[u]); }
inline void change_sontree(){//修改子树值 int u, w; scanf("%d %d", &u, &w); change(1, dfn[u], cnt[u], w); }
最后放上代码(我也不知道用不用long long):
#include <string.h> #include <stdio.h> #define maxn 500010 #define maxm 500010 struct graph{ struct edge{ long long to, next; edge(){} edge(const long long &_to, const long long &_next){ to = _to; next = _next; } }e[maxn << 1]; long long head[maxn], k; inline void init(){ memset(head, -1, sizeof head); k = 0; } inline void add(const long long &u, const long long &v){ e[k] = edge(v, head[u]); head[u] = k++; } }g; struct node{ long long l, r, zuo, you; long long c, f; }t[maxn << 2]; long long fa[maxn], dep[maxn], size[maxn], son[maxn]; long long dfn[maxn], id[maxn], top[maxn], cnt[maxn], tot; long long n, m, root, p, val[maxn], len; inline void swap(long long &x, long long &y){long long t = x; x = y; y = t;} void dfs_getson(long long u){ size[u] = 1; for(long long i = g.head[u]; ~i; i = g.e[i].next){ long long v = g.e[i].to; if(v == fa[u]) continue; fa[v] = u; dep[v] = dep[u] + 1; dfs_getson(v); size[u] += size[v]; if(size[v] > size[son[u]]) son[u] = v; } } void dfs_rewrite(long long u, long long tp){ top[u] = tp; dfn[u] = ++tot; id[tot] = u; if(son[u]) dfs_rewrite(son[u], tp); for(long long i = g.head[u]; ~i; i = g.e[i].next){ long long v = g.e[i].to; if(v != son[u] && v != fa[u]) dfs_rewrite(v, v); } cnt[u] = tot; } void build(long long d, long long l, long long r){ t[d].l = l, t[d].r = r; if(l == r){ t[d].c = val[id[l]]; return; } long long mid = l + r >> 1; build(d << 1, l, mid); build(d << 1 | 1, mid + 1, r); t[d].c = t[d << 1].c + t[d << 1 | 1].c; } inline void down(long long d){ t[d << 1].f += t[d].f; t[d << 1 | 1].f += t[d].f; t[d << 1].c += t[d].f * (t[d << 1].r - t[d << 1].l + 1); t[d << 1 | 1].c += t[d].f * (t[d << 1 | 1].r - t[d << 1 | 1].l + 1); t[d].f = 0; } void change(long long d, long long l, long long r, long long x){ if(l <= t[d].l && t[d].r <= r){ t[d].c += (t[d].r - t[d].l + 1) * x; t[d].f += x; return; } if(t[d].f) down(d); long long mid = t[d].l + t[d].r >> 1; if(l <= mid) change(d << 1, l, r, x); if(r > mid) change(d << 1 | 1, l, r, x); t[d].c = t[d << 1].c + t[d << 1 | 1].c; } long long getsum(long long d, long long l, long long r){ if(l <= t[d].l && t[d].r <= r) return t[d].c; if(t[d].f) down(d); long long mid = t[d].l + t[d].r >> 1; long long ans = 0; if(l <= mid) ans = (ans + getsum(d << 1, l, r)) % p; if(r > mid) ans = (ans + getsum(d << 1 | 1, l, r)) % p; return ans; } inline void change_xtoy(){ long long x, y, z; scanf("%lld%lld%lld",&x, &y, &z); while(top[x] != top[y]) { if(dep[top[x]] > dep[top[y]]) swap(x, y); change(1, dfn[top[y]], dfn[y], z); y = fa[top[y]]; } if(dep[x] > dep[y]) swap(x, y); change(1, dfn[x], dfn[y], z); } inline void getson_xtoy(){ long long x, y; scanf("%lld%lld", &x, &y); long long ans = 0; while(top[x] != top[y]){ if(dep[top[x]] > dep[top[y]]) swap(x, y); ans = (ans + getsum(1, dfn[top[y]], dfn[y])) % p; y = fa[top[y]]; } if(dep[x] > dep[y]) swap(x, y); ans += getsum(1, dfn[x], dfn[y]); printf("%lld\n", ans % p); } inline void change_sontree(){ long long x, y; scanf("%lld%lld", &x, &y); change(1, dfn[x], cnt[x], y); } inline void getsum_sontree(){ long long x; scanf("%lld", &x); printf("%lld\n", getsum(1, dfn[x], cnt[x]) % p); } int main(){ g.init(); scanf("%lld%lld%lld%lld", &n, &m, &root, &p); for(long long i = 1; i <= n; i++) scanf("%lld", &val[i]); for(long long i = 1; i < n; i++){ long long u, v; scanf("%lld%lld", &u, &v); g.add(u, v); g.add(v, u); } dfs_getson(root); dfs_rewrite(root, root); build(1, 1, tot); for(long long i = 1; i <= m; i++){ long long op; scanf("%lld", &op); if(op == 1) change_xtoy(); if(op == 2) getson_xtoy(); if(op == 3) change_sontree(); if(op == 4) getsum_sontree(); } return 0; }
注释应该就不需要了吧,自我感觉代码还是能被看懂的(溜~)
P2146 [NOI2015]软件包管理器
题目描述
Linux用户和OSX用户一定对软件包管理器不会陌生。通过软件包管理器,你可以通过一行命令安装某一个软件包,然后软件包管理器会帮助你从软件源下载软件包,同时自动解决所有的依赖(即下载安装这个软件包的安装所依赖的其它软件包),完成所有的配置。Debian/Ubuntu使用的apt-get,Fedora/CentOS使用的yum,以及OSX下可用的homebrew都是优秀的软件包管理器。
你决定设计你自己的软件包管理器。不可避免地,你要解决软件包之间的依赖问题。如果软件包A依赖软件包B,那么安装软件包A以前,必须先安装软件包B。同时,如果想要卸载软件包B,则必须卸载软件包A。现在你已经获得了所有的软件包之间的依赖关系。而且,由于你之前的工作,除0号软件包以外,在你的管理器当中的软件包都会依赖一个且仅一个软件包,而0号软件包不依赖任何一个软件包。依赖关系不存在环(若有m(m≥2)个软件包A1,A2,A3,⋯,Am,其中A1依赖A2,A2依赖A3,A3依赖A4,……,A[m-1]依赖Am,而Am依赖A1,则称这m个软件包的依赖关系构成环),当然也不会有一个软件包依赖自己。
现在你要为你的软件包管理器写一个依赖解决程序。根据反馈,用户希望在安装和卸载某个软件包时,快速地知道这个操作实际上会改变多少个软件包的安装状态(即安装操作会安装多少个未安装的软件包,或卸载操作会卸载多少个已安装的软件包),你的任务就是实现这个部分。注意,安装一个已安装的软件包,或卸载一个未安装的软件包,都不会改变任何软件包的安装状态,即在此情况下,改变安装状态的软件包数为0。
输入输出格式
输入格式:
从文件manager.in中读入数据。
输入文件的第1行包含1个整数n,表示软件包的总数。软件包从0开始编号。
随后一行包含n−1个整数,相邻整数之间用单个空格隔开,分别表示1,2,3,⋯,n−2,n−1号软件包依赖的软件包的编号。
接下来一行包含1个整数q,表示询问的总数。之后q行,每行1个询问。询问分为两种:
install x:表示安装软件包x
uninstall x:表示卸载软件包x
你需要维护每个软件包的安装状态,一开始所有的软件包都处于未安装状态。
对于每个操作,你需要输出这步操作会改变多少个软件包的安装状态,随后应用这个操作(即改变你维护的安装状态)。
输出格式:
输出到文件manager.out中。
输出文件包括q行。
输出文件的第i行输出1个整数,为第i步操作中改变安装状态的软件包数。
这题有点像上面那道模板题。这题其实已经暗示我们很多了。首先,0号软件包不依赖任何软件包——这不就是说0为根节点吗?其次,依赖关系——这不就是让我们把u向着依赖它的v连一条边吗?于是我们就建好了一颗树。如果我们要安装软件包A,由题意得,我们需要将u到根节点路上所有为安装的软件包都安装上;而删除u呢?又由题意得,我们需要删掉所有依赖u的软件包,而依赖那些软件包的软件包也得被删掉。这不就是上一题写过的对子树的操作吗?所以我们只需要再用线段树维护一下区间上已安装的软件包个数,这个题就也被切掉了。
#include <iostream> #include <cstring> #include <cstdio> #define maxn 100010 #define maxm 100010 using namespace std; struct edge{ int to, next; edge(){} edge(const int &_to, const int &_next){ to = _to; next = _next; } }e[maxn << 1]; int head[maxn], k; struct node{ int l, r, c, f; }t[maxn << 2]; int size[maxn], fa[maxn], dep[maxn], son[maxn]; int dfn[maxn], id[maxn], top[maxn], cnt[maxn], tot; int n, m; inline void add(const int &u, const int &v){ e[k] = edge(v, head[u]); head[u] = k++; } void dfs_getson(int u){ size[u] = 1; for(int i = head[u]; ~i; i = e[i].next){ int v = e[i].to; if(v == fa[u]) continue; fa[v] = u, dep[v] = dep[u] + 1; dfs_getson(v); size[u] += size[v]; if(size[v] > size[son[u]]) son[u] = v; } } inline void dfs_rewrite(int u, int tp){ top[u] = tp, dfn[u] = ++tot, id[tot] = u; if(son[u]) dfs_rewrite(son[u], tp); for(int i = head[u]; ~i; i = e[i].next){ int v = e[i].to; if(v != fa[u] && v != son[u]) dfs_rewrite(v, v); } cnt[u] = tot; } void build(int d, int l, int r){ t[d].l = l, t[d].r = r; if(l == r) return; int mid = l + r >> 1; build(d << 1, l, mid), build(d << 1 | 1, mid + 1, r); } inline void down(int d){ if(t[d].f == 2){ t[d << 1].c = t[d << 1].r - t[d << 1].l + 1; t[d << 1 | 1].c = t[d << 1 | 1].r - t[d << 1 | 1].l + 1; t[d << 1].f = t[d << 1 | 1].f = t[d].f; }else if(t[d].f == 1){ t[d << 1].c = t[d << 1 | 1].c = 0; t[d << 1].f = t[d << 1 | 1].f = t[d].f; } t[d].f = 0; } int change(int d, const int &l, const int &r, const int &op){ if(l <= t[d].l && t[d].r <= r){ int ans = t[d].c; if(op == 2) t[d].c = t[d].r - t[d].l + 1; else t[d].c = 0; t[d].f = op; return ans; } if(op) down(d); int mid = t[d].l + t[d].r >> 1, ans = 0; if(l <= mid) ans += change(d << 1, l, r, op); if(r > mid) ans += change(d << 1 | 1, l, r, op); t[d].c = t[d << 1].c + t[d << 1 | 1].c; return ans; } inline void change_path(); inline void change_sontree(); int main(){ memset(head, -1, sizeof head); scanf("%d", &n); for(int i = 2; i <= n; i++){ int v; scanf("%d", &v);v++; add(i, v), add(v, i); } dfs_getson(1); dfs_rewrite(1, 1); build(1, 1, tot); scanf("%d", &m); while(m--){ string op; cin >> op; if(op == "install") change_path(); else change_sontree(); } return 0; } inline void change_path(){ int u, ans; scanf("%d", &u);u++; ans = dep[u] - dep[1] + 1; while(top[u] != 1){ ans -= change(1, dfn[top[u]], dfn[u], 2);//被删掉的个数就等于节点总数减被安装了的软件包个数 u = fa[top[u]]; } ans -= change(1, 1, dfn[u], 2); printf("%d\n", ans); } inline void change_sontree(){ int u, ans; scanf("%d", &u);u++; ans = change(1, dfn[u], cnt[u], 1); printf("%d\n", ans); }
这里的节点编号要加1。注意这不是习惯!为什么呢?首先,我们的son数组一开始都是0,若不给节点编号加1的话,第二次DFS时就可能出错。
P2486 [SDOI2011]染色
这是一道很好的树剖题(至少不是模板)。很直接地,我们会想到用线段树记下区间内的颜色段的数量。于是我们递归下去,如果到了所要改的区间,我们就把区间的颜色段数量改成1就好了。但回溯时,若左儿子的右端点颜色和右儿子的左端点颜色一样,怎么判断呢?于是我们需要再记下两个信息:区间左端点的颜色和右端点的颜色。若左儿子的右端点颜色和右儿子的左端点颜色一样,将两个儿子的颜色段数加起来后再减1即
可。就是代码有点难调,我的那个BUG代码还没调好(果然太弱了)。
P2590 [ZJOI2008]树的统计
题目描述
一棵树上有n个节点,编号分别为1到n,每个节点都有一个权值w。
我们将以下面的形式来要求你对这棵树完成一些操作:
I. CHANGE u t : 把结点u的权值改为t
II. QMAX u v: 询问从点u到点v的路径上的节点的最大权值
III. QSUM u v: 询问从点u到点v的路径上的节点的权值和
注意:从点u到点v的路径上的节点包括u和v本身
输入输出格式
输入格式:
输入文件的第一行为一个整数n,表示节点的个数。
接下来n – 1行,每行2个整数a和b,表示节点a和节点b之间有一条边相连。
接下来一行n个整数,第i个整数wi表示节点i的权值。
接下来1行,为一个整数q,表示操作的总数。
接下来q行,每行一个操作,以“CHANGE u t”或者“QMAX u v”或者“QSUM u v”的形式给出。
输出格式:
对于每个“QMAX”或者“QSUM”的操作,每行输出一个整数表示要求输出的结果。
真正的模板题。如果你觉得你写树剖还不熟练,可以拿这题练(shui)练(jing)手(yan)。话说树剖的代码真的难调。
P4114 Qtree1
题目描述
给定一棵n个节点的树,有两个操作:
-
CHANGE i ti 把第i条边的边权变成ti
-
QUERY a b 输出从a到b的路径中最大的边权,当a=b的时候,输出0
输入输出格式
输入格式:
第一行输入一个n,表示节点个数
第二行到第n行每行输入三个数,ui,vi,wi,分别表示 ui,vi有一条边,边权是wi
第n+1行开始,一共有不定数量行,每一行分别有以下三种可能
CHANGE,QUERY同题意所述
DONE表示输入结束
输出格式:
对于每个QUERY操作,输出一个数,表示a b之间边权最大值
这道题的技巧在很多树剖题都会用到~我们树剖时,计算的都是点权对不对?可是这题就不同些,给的是边权。这时怎么办呢?就用到了一种技巧,叫.......我也不知道叫什么。就是把边权转化为点权。怎么转呢?假如有边(u,v),如果我们把边权存在深度大的那个节点上去,也就是在节点x存下x连向父亲节点的边的权值。这样有什么用呢?想想,除了根节点,每个节点都唯一地只有一个父亲对吧,所以这样存不就把n-1条边的权值存在n-1个点上了吗?而那个没存的就是根节点了。于是,计算边权的题就可以转化为计算点权的题了。不过这样做还需要注意一个事项,由于每个点存的都是连向父亲边的边权,所以我们在计算树上两点之间的信息时,这两点的lca是不可用的(我不会告诉你我因为这个写爆过)。
例题就讲到这里吧。。。
其实树剖还可以做lca。现有树上两点u,v,若求他们的lca,就树剖后一直跳链,直到top相同为止。此时深度小的那个就是lca了~也就是说现在我们有三种求lca的方法了:树上倍增、Tarjan和树剖(还有向上标记)。我们来对比一下。其实三种算法的思路都是一样,一直往上跳即可。而树上倍增时跳的是2的k次方,虽然很大,但还是没有树剖跳一跳链跳得彻底。而树剖跳的时候,如果链多了,还是会费时的。但Tarjan就异常强大了,在遍历图的过程中遇到了可以回答的询问就能得出答案,你可以理解为现在遍历到了u,而询问里有求lca(u,v)并且v已经遍历过,此时lca就是v在并查集的生成树里的祖先,也就是直接把u,v直接拽到了lca的位置去,跳都不跳了。但毕竟是离线算法,费空间......再对比一下时间复杂度,树上倍增为O((N+M)logN),树剖为(N+MlogN),Tarjan为O(N+M),Tarjan最优。而空间复杂度呢?树上倍增法为O(NlogN),树剖为O(N),Tarjan为O(N+M)(询问与并查集),树剖最优。综上,个人认为树剖还是最好的求lca的算法(虽然有点小题大做并且暴力得一批)。所以你们可以尝试做一下lca的题: