[总结] LCT学习笔记
\(emmm\)学\(lct\)有几天了,大概整理一下这东西的题单吧 (部分参考flashhu的博客)
基础操作
[洛谷P1501Tree II]
题意 给定一棵树,要求支持 链加,删边加边,链乘,询问链权值 四种操作。
Sol: 大概是 \(lct\) 上维护加和乘标记的板子题
[SHOI2014 三叉神经树]
**题意 **给定一棵 $ 3\times n$ 个节点的树,编号在 \(1\sim n\) 的节点有且仅有三个儿子,编号在\(n+1\sim 3\times n\)的节点没有儿子。节点的值只可能为 \(0\) 或 \(1\) 。编号在 \(n+1\sim 3\times n\) 的节点的值由输入确定,编号在 \(1\sim n\) 的节点的值为其三个儿子中 \(0\) 和 \(1\) 较多的那种。现在有 \(m\) 个操作,每次操作会改变某个编号在 \(n+1\sim 3\times n\) 的值,请你在每次操作结束后输出根节点的值。
Sol: \(emmm\)让我调到凌晨两点的一道题 设题目中要求的值为 \(a\) ,每个节点可以根据子节点值为 \(0\) 的个数分为 \(0\sim 3\) 四种情况,设当前这个新值为 \(val\) 。观察到每次将某个叶子结点从 $0 $ 变成 \(1\) 实际上就是将从这个叶子结点向上连续一段 \(val\) 为 \(2\) 的点区间修改 \(val\) 为 \(1\)。从 \(1\) 变成 \(0\) 实际上就是将一段 \(val\) 为 \(1\) 的修改为 \(2\) 。我们需要维护一个数据结构查询一条链上值不为 \(1/2\) 的最深的点是哪个,可以在 \(lct\) 上二分深度,然后打 \(tag\) 修改就好了。时间复杂度 \(O(n\log ^2n)\)。
维护连通性&双联通分量
[SDOI2008 洞穴勘测]
题意 要求维护一个森林,支持动态加边,删边,询问两点是否联通。
Sol: 如果没有删边,可以直接并查集做一下,加上删边就 \(findroot\) 随便判一下就好了。大概是个板子题。貌似可以并查集做但是复杂度不太对?
[AHOI2005 航线规划]
题意 给定 \(n\) 个点 \(m\) 条边的无向图。要求支持动态删边,询问两点间桥边个数。
Sol: 大概是 \(lct\) 维护边双的板子题,还是挺重要的。动态删边不好做,可以离线转化为动态加边。如果两点 \(u,v\) 已经联通,那么如果再加一条连接 \(u,v\) 的边就需要将 \((u,v)\) 这一条链缩成一个双联通分量了。具体是 维护一个并查集 \(f[i]\) 表示点 \(i\) 所在的双联通分量的代表点,所有对点 \(i\) 的操作实际上都应该对点 \(f[i]\) 进行操作。如果要缩 \((u,v)\) 这条链上的所有点,那么就先把这条路径 \(split\) 出来,然后给这个分量选一个代表点,不妨选作点 \(v\),然后暴力 \(dfs\) 当前 \(splay\),将所有点的 \(f[i]\) 指向 \(v\),这样就成功缩成了一个点。还有一点要注意的就是每次使用 \(fa[i]\) 实际上都应该使用 \(find(fa[i])\),\(fa[i]\) 表示 \(splay\) 上 \(i\) 的父亲,\(find(i)\) 表示查询点 \(i\) 所在连通分量的代表点即 \(f[i]\)。
暴力 \(dfs\) 就长这样:
void dfs(int a,int b){
father[a]=b;
if(ch[a][0]) dfs(ch[a][0],b);
if(ch[a][1]) dfs(ch[a][1],b);
}
维护边权&生成树
[WC2006 水管局长]
题意 给定 \(n\) 个点 \(m\) 条边的有向图,要求支持动态删边,询问点 \(a\) 到点 \(b\) 的所有路径上,路径上最大边权的最小值。
Sol: 话说交这题的时候卡着某人了2333 维护边权的套路是将每条边拆成一个点,这个点的权值是边的权值,然后这个点分别连接这条边的两个端点。然后视情况将其他点的点权赋为 \(-inf/inf\) 。还是套路离线,删边转化成加边。不难发现询问最大边权最小值就是让动态维护一个最小生成树,否则一定能找到不劣解。问题就转变成了动态维护最小生成树。对于 \(splay\) 上每个点 \(x\) 维护 \(mx[x]\) 和 \(id[x]\) 表示 \(x\) 的实儿子中最大的点权和最大的点权是哪个点。尝试加边 \((x,y)\) 时,如果两点不连通那么直接加上,否则将 \((x,y)\) \(split\) 出来。一个显然的性质是点权最大的点一定是原图中的边。如果这颗 \(splay\) 中点权最大的点都要小于当前边的边权,那么跳过当前边,否则让点权最大的点所代表的边与这条边的两个端点 \(cut\) 出来,再将待加入边和两个端点 \(link\) 起来就好了。
[NOI2014 魔法森林]
题意 每条边有两个权值 \(a_i,b_i\),要求一条从 \(1\) 到 \(n\) 的路径,使得 \(\max \left\{a\right\}+\max\left\{b\right\}\) 最小。求出这个最小值。
Sol: 做这道题的上午的数学课上 \(van\) 老师 (数学老师) 刚好讲到了求无限制二元函数最值时的思想,就是"固定一个求另一个"。然后自然而然的想到了这道题上,我们可以"固定" \(a\) 求 \(b\) 的最小生成树,再详细点就是枚举路径上 \(a\) 的最大值 \(x\) ,然后将所有 \(a_i<x\) 的边 \(i\) 以 \(b_i\) 为关键字做最小生成树,就可以用当前的 \(x+\max\left\{b\right\}\) 去更新答案了。观察到不用每次都重新求一遍最小生成树,只需要用 \(lct\) 维护以 \(b\) 为关键字的最小生成树,每次尝试加边就行了。
维护子树信息
\(lct\) 不是很擅长维护子树信息,因为 \(splay\) 上的儿子不一定就是原图上的儿子但是也不是不能做。
以维护子树 \(size\) 举例。我们设 \(s[x]\) 表示 \(x\) 的所有虚子树(通过虚边指向\(x\))的 \(size\) 和,\(sze[x]\) 表示 \(lct\) 上点 \(x\) 的所有儿子的 \(size\) 和(包括 \(Splay\) 中相对的左右儿子的总和与被轻边所指的虚子树的总和)。那么 \(pushup\) 就是这么写:
void pushup(int x){
sze[x]=sze[ch[x][0]]+sze[ch[x][1]]+s[x]+1
}
考虑如何维护 \(s[x]\)
假设当前已经维护好了 \(s[x]\),考虑 \(lct\) 的哪些操作会改变 \(s[x]\) 的值
\(\dots\)
经过前人分析,只有 \(access\) 和 \(link\) 操作会改变 \(s[x]\) 的值。
具体是:
inline void access(int x){
for(int y=0;x;y=x,x=fa[x]){
//x将要失去一个右儿子再得到一个右儿子,所以要加上失去的减去得到的(因为是虚子树)
splay(x);
sze[x]+=s[ch[x][1]];
sze[x]-=s[ch[x][1]=y];
pushup(x);
}
}
//保证连边合法
inline void link(int x,int y){
split(x,y);//这里不是提取x-y的路径的意思,是makeroot+access+splay的偷懒写法
fa[x]=y;s[y]+=sze[x];
pushup(y);
}
尤其要注意 \(link\) 里加上的是 \(sze\) 而不是 \(s\)。
[BJOI2014 大融合]
**题意 ** 要求支持动态连边,询问有多少点对间的路径经过边 \((x,y)\)。
Sol: 维护子树信息的板子题,按上边讲的直接做就好了。
[洛谷U19464 山村游历]
题意 有点复杂...还是去看原题吧
Sol: 懒得写...去看这篇题解吧 戳我戳我戳我戳我戳我!!! 不过这题姿势是真的高
[洛谷P4299 首都]
题意 要求维护一个森林,支持动态加边(保证合法),询问某点所在树的重心,询问所有树的重心的异或和。
Sol: 对着题解调的一道题...
首先有两个关于树的重心的性质:
- 如果两棵树连在了一起,新的重心一定是原来两个重心路径上的某点
- 如果一棵树增加一个叶子结点,那么重心最多移动一次(即重心最多只会移动到相邻的点上)
有了这两个性质就可以做这题了。每次连边 \((x,y)\) 的时候启发式合并一下,将 \(size\) 小的那个树上的每个节点拎出来扔进 \(size\) 多的树上,然后维护重心的移动就行了。每个点最多被拎出来 \(\log n\) 次,总复杂度 \(O(n\log ^2n)\)。
维护重心的移动说的有点简单,再详细说说。因为性质 \(2\) ,所以每次加一个点时判断重心是否会在重心与新加的点的路径上移动一步就行了。再具体点假设原来的重心为 \(rt\),新加入的点为 \(y\),首先要先求出重心可能会变成哪个点。先 \(makeroot(rt),access(y),splay(rt)\),这就保证了 \(rt\) 到 \(y\) 的路径都在一棵 \(splay\) 里。然后一直找 \(ch[rt][1]\) 的左儿子,即 \(splay\) 上 \(rt\) 的后继 \(now\) ,这就是有可能成为新的重心的点。然后由重心的性质:不存在某个儿子的 \(size\) 的 \(2\) 倍大于整棵树的总点数,判断一下 \(sze[now]\times 2\) 和 \(sze[rt]\) 的大小就好了。
[SPOJ2939 QTREEV]
题意 给定一棵 \(n\) 个点的树,每个点有可能是黑色或白色,一开始都是黑的。要求支持 反转某点颜色 询问离点 \(x\) 到最近的白点的距离
Sol: 一群神仙网友都觉得这是道傻逼题于是写的很不详细只有我傻逼地想了整个上午下午 动态点分治似乎可以轻松切掉?为了练 \(lct\) 来写这道题没想太多。
维护子树信息的 \(lct\),但是不是简单的维护子树大小而是维护一个类似子树最值的东西,这个可以在每个点开个 \(multiset\) 随便维护下。
难的是状态的定义。我们定义 \(lmn[x],rmn[x]\) 分别代表在 \(splay\) 中 \(x\) 的子树里深度最浅的点能够到达最近的白点的距离和深度最深的点能够到达最近的白点的距离。这里说是“最浅”和“最深”是因为 \(splay\) 中每棵子树实际上都是原图(实际上是原树)中一条深度单调递增的链。
有了这个状态就可以方便的 \(pushup\) 更新父节点的信息了,这里建议在纸上画下一条链然后把它建成 \(splay\) ,对着这棵 \(splay\) 可以很直观地写出 \(pushup\) 函数。
我的代码里全程没有用到 \(makeroot\),因为如果 \(makeroot\) 的话就需要翻转左右子树,而点 \(x\) 的信息是跟左右子树的顺序有关的(这个跟平常那些维护子树大小的有很大区别),所以大概需要在 \(pushup\) 里面套上 \(pushdown\), \(pushdown\) 里面再套上 \(pushup\) 很是麻烦还会TLE索性就不去写了。
做完这题还可以去做 \(Qtree\;4\) 是这题的升级版 然而我做这题时网上一个像样的题解都没有于是乎心力交瘁痛不欲生就不想再去刚那题了
维护树上同色联通块
维护方法视情况而定
可以通过一个 \(lct\) 将所有的点串起来,其中一个 \(splay\) 里维护同色的点
还可以有多少颜色开多少 \(lct\),然后两点同色就在对应 \(lct\) 中连上边
[SDOI2017树点涂色]
题意 给定一棵以 \(1\) 号点为根的有根树,每个点都有一个颜色,最开始点上的颜色两两不同。定义一条路径的权值为这条路径上的点不同的颜色个数。要求支持以下三个操作:
- 将点 \(x\) 到根节点的路径上所有的点染上一种没有用过的新颜色
- 询问 \(x\) 到 \(y\) 路径权值
- 询问从 \(x\) 的子树中选一个点到根节点的路径最大的权值
Sol: 感觉 \(1\) 操作特别像 \(lct\) 的 \(access\),就是把 \(1\) 到 \(x\) 这条链放进一个 \(splay\) 里,然后就想到了维护一个 \(lct\),每个 \(splay\) 都保存着颜色相同的一条链(因为颜色相同的所有点必然在一条深度递增的链上)。
考虑如何回答 \(2\) 操作。可以弄一个 \(f[i]\) 表示从 \(1\) 到 \(i\) 的路径的权值。然后答案就是 \(f[x]+f[y]-2*f[lca(x,y)]+1\) 了。这个可以自己画一下图还是比较显然的。
怎么维护好这个 \(f\) 呢?
考虑 \(access\) 的过程,就是不断将一条实边断为虚边,再将一条虚边连成实边。而这个 \(f\) 数组本质上就是从 \(1\) 到 \(i\) 经过的虚边个数(或许要加上 \(1\) ),那我们在 \(access\) 每次断边的时候动态维护好这个 \(f\) 就好了。
具体是因为要断开 \(x\) 和 \(ch[x][1]\) 之间的实边将其变为虚边,设在 \(splay\) 中 \(ch[x][1]\) 的子树中最靠左(即在原树中深度最小的点)的点为 \(z\),那么我们就需要将 \(z\) 的子树中所有点的 \(f\) 值 \(+1\) (因为多了一条从 \(x\) 连向 \(z\) 的虚边),然后还要连上 \(x\) 到 \(y\) 的实边,就需要将 \(splay\) 中 \(y\) 的子树中最靠左的点的子树中所有点的 \(f\) 值 \(-1\)。
用 \(dfs\) 序+线段树维护就资瓷区间加减区间查询了。
[SPOJ16549 QTREEVI]
题意 给定一棵 \(n\) 个点的树,每个点的颜色可以为白色或黑色 。要求支持两个操作:
- 改变点 \(x\) 的颜色
- 询问包含点 \(x\) 的同色联通块的大小
Sol: 首先如果熟练的话会想到一个做法就是对于两个颜色分别维护一个 \(lct\),修改颜色的时候暴力 \(link/cut\) 与这个点相连的所有点。显然可以被菊花图卡成 \(n^2\) 。
许多与树有关的题目,边权不好处理时,常将边权下放为点权。这题我们可将点权上推到边权。
同样对于两个颜色分别维护 \(lct\),分别叫做黑 \(lct\) 和白 \(lct\)。
把每个点的父边赋予该点的颜色,如果一条边 \(<fa,x>\) 在黑 \(lct\) 中出现,那么点 \(x\) 的颜色一定是黑色的,白色同理。
发现这样做就可以快速修改了。
那能不能支持查询呢? 废话
如果要查询点 \(x\),那我们可以在对应颜色的 \(lct\) 中找到 \(x\) 所在的树的根节点 \(rt\) ,即 \(rt=findroot(x)\)。然后答案就是 \(sze[ch[rt][1]]\)。不是 \(sze[rt]\) 的原因是 \(<fa[rt],rt>\) 这条边一定没有在当前 \(lct\) 中出现不然根节点就是 \(fa[rt]\) 了。既然没有出现那就代表 \(rt\) 的颜色和 \(x\) 的颜色一定不一样,所以答案是 \(rt\) 下面那个点的子树大小,不然会通过 \(rt\) 连接到别的子树导致答案变大。
[SPOJ16580 QTREEVII]
题意 给定一棵 \(n\) 个点的树,每个点的颜色可以为白色或黑色,同时每个点还有点权 。要求支持两个操作:
- 询问包含点 \(x\) 的同色联通块中最大的点权
- 改变点 \(x\) 的颜色
- 修改点 \(x\) 的点权
Sol: 会了上面那题的话这题的解法就能很自然地出来了。就是加了一个子树最值,随便拿 \(multiset\) 维护下就好。
涉及 \(lct\) 基础套路的东西大概就这么多吧,一些特别好的题会单独拎出来写。