哈哈哈学会的新东西啊,暴搓312行
在此鸣谢机房大蛇QEDQEDQED&&zxkqwq进行一个讲解
前置知识
en。没有。
(可能要理解下线段树的结构,前缀和,二分)
定义
Top Tree是什么?
就是对于一个给定的树,通过compress和rake操作,将点和边以及它们维护的信息进行脱水缩合成簇后构成的一棵树。
你可以类比成在树上建一个线段树,及其对应父亲节点维护左右儿子的总信息,这对于单点修改,整体查询问题,是极其方便的。

考虑对于左图建一颗Top Tree,每个点就是用来维护每次对应脱水缩合后簇的信息的点。
右图中14这个点维护的是节点5,8以及它们的返祖边(即连接父节点的那条边)形成的簇的信息(也对应下文的compress操作)
右图中12这个点维护的是节点6,7以及它们的返祖边(即连接父节点的那条边)形成的簇的信息(也对应下文的rake操作)

即Top Tree 中的一个节点有两个儿子(都分别代表一个簇),这个节点代表的簇是这两个簇通过 Compress 或 Rake 操作合并得到的新簇。
我们发现,如果对一个点进行修改,只需要\(O(树高)\)(top tree)的时间复杂度,查询是\(O(1)\)的。
这简直是棒完了!
脱水缩合
en其实人家叫树收缩。(可以类比成线段树的pushup,即将子节点的信息传递到父节点上,方便整体查询)
即上面我们提到的两种妙妙操作
妙妙操作

哦其实就是解释一下(原因是肝硬化在我码的时候问询我妙妙操作是什么)
compress
将一条长链缩成一条短链
严格来说,即指定一个度数为 \(2\) 的点 \(x\),与点 \(x\) 相邻的那两个点记为 \(y\)、\(z\)。
我们连一条新边 \(yz\),将点 \(x\)、边 \(xz\)、边 \(xy\) 的信息放到 \(yz\) 中储存,并删去它们

rake
将一个二叉树缩成链
严格来说,即将一个度为 \(1\) 的点 \(x\),而且与点 \(x\) 相邻的点 \(y\) 的度数需大于 \(1\),设点 \(y\) 的另一个邻点为 \(z\)。
我们将点 \(x\)、边 \(xy\) 的信息放入边 \(yz\) 中储存,并删去它们。

我们发现,对于一次树收缩操作,只需要维护对应的合并信息即可(根据例题不同进行灵活调整即可)
我们总是可以通过不断地 rake 一度节点为界点的簇,Compress 一对二度节点为界点的簇,来把整棵树合并成一个大簇。
Tips: Top Tree和普通的二叉树一样拥有相似的结构,所以在进行compress和rake操作时要记录父节点,左右儿子节点的标号,方便之后的信息修改。
因为我们把它比作一颗线段树(其结构和维护信息同样也和线段树相仿)
所以我们类比线段树,同样也要明确以下三种操作
建树
建树是保证其时间复杂度正确的关键
以下我们提供一种时间复杂度介于\(O(n)\)和\(O(n \log n)\)之间的方法。
暴力建树
只要找到一组满足compress或rake操作的点就进行收缩。(这东西树高大抵是\(n\)的,没有人这样建树)
朴素建树
对于一棵树,我们可以将它重链剖分后的所有轻儿子链形成的簇通过rake操作合并到重儿子链上,然后将重儿子链通过compress操作整合成一个簇(对应Top Tree上的某个点)。
我们发现,这种方法的树高有可能被菊花图状物卡成\(n\),则之后的修改操作的时间复杂度就会变成 \(O(n)\),劣完了。
然后考虑优化
优化建树1
我们考虑分治
rake
对于一个有着\(n\)个轻儿子的节点,我们将其进行分治rake;
即像二分一样,将\(n\)个儿子分为两组,左组和右组的个数均约为\(n/2\)个
一直递归分治,直到某次只有两个儿子,然后进行rake操作,获得新生成的节点信息,然后返回到上一次分组的状态(此时两个儿子的信息已经被储存在了一个节点上),继续上述操作。

如图,对于节点\(1\)来说,重儿子链是\(2\)那条链,有\(8\)个轻儿子。
数字对应rake操作的序数,颜色即组内个数(同时发现同种颜色在Top Tree上的层数是相同的)
compress
对于一条有\(n\)个点的链,我们将其进行分治compress;
还是如同rake操作一样,将链上的\(n\)个点均分为两组,递归分治进行compress操作。

如图,对于\(1\)这条链来说,其中有\(8\)个节点。
数字对应compress操作的序数,颜色即组内个数(同时发现同种颜色在Top Tree上的层数是相同的)
我们发现,这样建图树高最劣是\(O(n \log ^2 n)\)的(咦?怎么和树剖一样啊,我Top Tree 学了个"____"吗?)
然后我们还要优化!
优化建树2
我们通过观察,发现簇的大小是影响操作次数(即树高)的重要因素。当其分治时分成的两组簇的大小越相近,这颗Top Tree就越平均(树高的值就越小)
所以我们在递归分组的时候就不按照\(节点个数/2\)的方式进行分组,而是让每次分成两组的簇大小尽量相等。
rake
即记录子节点的子树大小,找到一个位置使两边子树大小的加和尽量相等。(这里有一个转化,簇的大小为子树大小的加和)

如图,黑色边为重儿子链,红色和蓝色的簇就是我们的第一次分组。
compress
还是像rake一样,按照子树大小进行分治。

如图,对于重儿子链,红色和蓝色的簇就是我们的第一次分组。
这样,我们就可以建出一颗树高为\(\log n\)的Top Tree
总体操作
- 预处理操作
- 将原树进行重链剖分,记录子树大小,重儿子链,并将原树上的一对父子节点的信息用簇记录下来。
- 轻儿子链形成的簇通过rake操作合并到重儿子链上
-
对于其原树上的一个点\(num\),遍历其重儿子链上的所有节点,对于其中一个重儿子节点\(x\),遍历\(x\)的所有轻儿子\(y\),并递归建树。
-
递归返回,将轻儿子\(y\)加入到\(x\)的节点集合(此时是轻儿子节点集合)。
-
遍历\(x\)节点的轻儿子集合,将其子树大小以前缀和的方式存储(方便之后找到总子树大小的分界点)。
-
递归进行分治rake操作,对于一次递归,对于其左端点\(l\)和右端点\(r\),使用二分查找找到分界点 \(mid\)(满足两边子树大小的加和尽量相等)。继续递归并回退然后执行rake即可。
- 将重儿子链通过compress操作整合成一个簇
-
回到我们的\(num\)节点,遍历其重儿子\(x\),并加入到\(num\)的节点集合(此时是重儿子节点集合)。
-
遍历\(num\)节点的重儿子集合,将其子树大小以前缀和的方式存储(方便之后找到总子树大小的分界点)。
-
递归进行分治compress操作,对于一次递归,对于其左端点\(l\)和右端点\(r\),使用二分查找找到分界点 \(mid\)(满足两边子树大小的加和尽量相等)。继续递归并回退然后执行compress即可。
- end
- 清空\(num\)的连边,只记录与其父亲的连边。(为之后的建树做处理)
修改
我们在建树预处理的时候记录了有关一对父子节点信息的簇,同时,我们记录对于该子节点在Top Tree上对应的点的标号。
为什么是子节点?
因为我们维护的这个簇上界点是父亲节点,下界点是儿子节点;
而簇维护的信息“包下不包上”,这样我们才能进行合理的信息转移以及维护。
所以该簇对应的节点是儿子节点。
然后直接在对应的Top Tree上的点进行修改。
若该点的类型是C,即这个节点是由它的左右儿子节点维护的簇通过compress操作构成的,就再次进行compress操作更新这个节点维护的信息。(和线段树的pushup类似)
反之,若该点的类型是R,即这个节点是由它的左右儿子节点维护的簇通过rake操作构成的,就再次进行rake操作更新这个节点维护的信息。(和线段树的pushup类似)
然后找到这个节点在Top Tree上的父节点,递归更新即可。
查询
整体查询就直接查询Top Tree根节点所维护的信息即可。
(是的就是这样)
啊终于终于写完了!!
接下来让我们看一道例题
例题
luogu P4115 Qtree4
P4115 Qtree4
题目描述
给出一棵边带权的节点数量为 \(n\) 的树,初始树上所有节点都是白色。有两种操作:
-
C x,改变节点 \(x\) 的颜色,即白变黑,黑变白。 -
A,询问树中最远的两个白色节点的距离,这两个白色节点可以重合(此时距离为 \(0\))。
输入格式
第一行,输入一个正整数 \(n\ (n \le {10}^5\))。
接下来 \(n-1\) 行,每行有 \(3\) 个整数 \(a,b,c\),代表节点 \(a\) 和节点 \(b\) 之间连一条边权为 \(c\ (|c|\le{10}^3)\) 的边。
接下来一行,一个正整数 \(q\ (q\le 2\times 10^5)\),表示操作的数量。
接下来 \(q\) 行,每行一次操作。
输出格式
对于每次 A 操作,如果树上不存在白点,输出一行一个字符串 They have disappeared.,否则输出一行一个整数代表树上最远的两个白色节点的距离。
输入输出样例 #1
输入 #1
3
1 2 1
1 3 1
7
A
C 1
A
C 2
A
C 3
A
输出 #1
2
2
0
They have disappeared.
分析题目
分析题目,我们发现是树上问题+单点修改+整体查询,十分鱼块啊!直接上\(Top Tree\)!
建树,修改,查询是十分板板的(甚至可以Ctrl+C Ctrl+V的)
然后考虑我们两个妙妙操作要维护什么信息
这题让我们求对于这颗原树中两个白点的最远距离
对于一个簇来说,我们要附加维护四个值(除簇的类型,父节点,左右儿子节点,上界点,下界点,节点标号外)
-
\(mx\)
簇内两个白点之间的最远距离 -
\(mu\)
上界点到簇内一个白点的最远距离 -
\(md\)
下界点到簇内一个白点的最远距离 -
\(dis\)
上界点到下界点的距离
- 初始化信息
因为是求最大值,所以
而\(dis\)的定义即上界点和下界点之间的距离
对于原树上一对父子节点形成的簇来说
而对于其他的簇来说:
compress操作
rake操作
考虑转移
当前节点维护的是\(x\)和\(y\)两个簇合并的信息。
对于compress操作来说
此时左儿子\(x\)是包含当前节点的上界点的簇,右儿子\(y\)是包含当前节点的下界点的簇。
即
\(x\)的上界点是当前节点的上界点。
\(x\)的下界点是\(y\)的上界点。
\(y\)的下界点是当前节点的下界点。
对于rake操作来说
此时左儿子\(x\)是要合并在\(y\)上的一个簇,右儿子\(y\)是包含当前节点的上下界点的簇。
\(y\)的上界点是当前节点的上界点。
\(x\)的下界点和上界点被合并在簇内。
\(y\)的下界点是当前节点的下界点。
对于\(x\)这个簇的下界点是白点的情况,有一些附加转移。
可以自己先思考一下,实在不会转移可以参考代码
代码实现
#include<bits/stdc++.h>
using namespace std;
const long long inf=1e14;
struct jade
{long long x,y;//上,下界点 long long mx,mu,md;//簇中答案,上界点到簇中白点的最大距离,下界点到簇中白点的最大距离 long long id;//标号 long long dis;//上下界点距离 char type;//该点的类型
}clus[200010];//top tree
long long n,m;
long long col[200010];//点的颜色
long long pos[200010],fa[200010],ls[200010],rs[200010];//(新建节点时)原树上的点对应toptree的点标号,toptree上的父节点,左子节点,右子节点标号
long long root=1;//toptree的根
long long h[200010],to[400010],nxt[400010],v[400010],tot;//链式前向星存图
long long posfa[200010];//原树上的点对应toptree的点标号
long long da[200010],son[200010],siz[200010],cnt;// 原树上的父节点,重儿子节点,子树大小,toptree点的个数
long long sum;//黑点的个数(判无解
vector <long long> heavy[200010];//对于i节点的重儿子链
vector <long long> node[200010];//i节点的儿子集
vector <long long> sizsum[200010];//i节点儿子的子树前缀和
void add(long long x,long long y,long long val)//建边
{tot++;to[tot]=y;nxt[tot]=h[x];h[x]=tot;v[tot]=val;
}
void compress(jade x,jade y,jade &res)//将一条链缩短
{res.mx=res.mu=res.md=-inf;res.x=x.x;res.y=y.y;res.dis=x.dis+y.dis;res.mx=max({x.md+y.mu,x.mx,y.mx});res.mu=max(x.mu,y.mu+x.dis);res.md=max(x.md+y.dis,y.md);if(col[x.y]==0){res.mx=max({res.mx,x.md,y.mu,0*1ll});res.mu=max(res.mu,x.dis);res.md=max(res.md,y.dis);}pos[x.y]=res.id;fa[x.id]=fa[y.id]=res.id;ls[res.id]=x.id;rs[res.id]=y.id;res.type='C';root=res.id;
}
void rake(jade x,jade y,jade &res)//将叉数减一
{res.mx=res.mu=res.md=-inf;res.x=y.x;res.y=y.y;res.dis=y.dis;res.mx=max({x.mx,y.mx,x.mu+y.mu});res.mu=max(x.mu,y.mu);res.md=max(x.mu+y.dis,y.md);if(col[x.y]==0){res.mx=max({res.mx,x.md,y.mu+x.dis,0*1ll});res.mu=max(res.mu,x.dis);res.md=max(res.md,x.dis+y.dis);}pos[x.y]=res.id;fa[x.id]=fa[y.id]=res.id;ls[res.id]=x.id;rs[res.id]=y.id;res.type='R';root=res.id;
}
void update(long long x)//修改
{if(x==0)//边界 {return ;}if(clus[x].type=='C'){compress(clus[ls[x]],clus[rs[x]],clus[x]);update(fa[x]);//递归修改 }else{rake(clus[ls[x]],clus[rs[x]],clus[x]);update(fa[x]);}
}
void dfs1(long long x)
{siz[x]=1;for(long long i=h[x];i;i=nxt[i]){long long y=to[i];if(y==da[x]){continue;}da[y]=x;cnt++;//初始化 posfa[y]=cnt;clus[cnt].x=x;clus[cnt].y=y;clus[cnt].id=cnt;clus[cnt].dis=v[i];clus[cnt].mx=clus[cnt].mu=clus[cnt].md=-inf;dfs1(y);if(siz[y]>siz[son[x]]){son[x]=y;}siz[x]+=siz[y];}
}
void dfs2(long long x,long long top)
{heavy[top].push_back(x);if(son[x]!=0){dfs2(son[x],top);}for(long long i=h[x];i;i=nxt[i]){long long y=to[i];if(y==da[x]||y==son[x]){continue;}dfs2(y,y);}
}
long long solve_compress(long long l,long long r,long long x)
{//compress递归缩树 if(l>r){return 0;}if(l==r){return posfa[node[x][l]];}long long ll=l,rr=r;while(ll+1<rr)//二分查找分界点 {long long mid=(ll+rr)>>1;if((sizsum[x][mid]-sizsum[x][l-1])*2<=(sizsum[x][r]-sizsum[x][l-1])){ll=mid;}else{rr=mid;}}long long mid=ll;long long lson=solve_compress(l,mid,x);long long rson=solve_compress(mid+1,r,x);cnt++;long long res=cnt;clus[cnt].id=cnt;compress(clus[lson],clus[rson],clus[res]);//合并左右儿子信息 return res;
}
long long solve_rake(long long l,long long r,long long x)
{//rake递归缩树if(l>r){return 0;}if(l==r){return posfa[node[x][l]];}long long ll=l,rr=r;while(ll+1<rr)//二分查找分界点{long long mid=(ll+rr)>>1;if((sizsum[x][mid]-sizsum[x][l-1])*2<=(sizsum[x][r]-sizsum[x][l-1])){ll=mid;}else{rr=mid;}}long long mid=ll;long long lson=solve_rake(l,mid,x);long long rson=solve_rake(mid+1,r,x);cnt++;long long res=cnt;clus[cnt].id=cnt;rake(clus[lson],clus[rson],clus[res]);//合并左右儿子信息 return res;
}
void build(long long num)//建树
{for(long long x:heavy[num])//遍历重儿子链 {if(son[x]==0){continue;}sizsum[x].push_back(0);node[x].push_back(0);for(long long i=h[x];i;i=nxt[i])//遍历重儿子上的轻儿子 {long long y=to[i];if(y!=son[x]&&y!=da[x]){build(y);node[x].push_back(y);//此时是轻儿子集 }}for(long long i=1;i<node[x].size();i++)//求前缀和 {sizsum[x].push_back(sizsum[x][i-1]+siz[node[x][i]]);}long long ro=solve_rake(1,node[x].size()-1,x);//递归建树 if(ro!=0)//有新点 {cnt++;clus[cnt].id=cnt;rake(clus[ro],clus[posfa[son[x]]],clus[cnt]);posfa[son[x]]=cnt;}}sizsum[num].clear();//清除历史遗留问题 node[num].clear();sizsum[num].push_back(0);node[num].push_back(0);for(long long x:heavy[num]){node[num].push_back(x);//此时变成重儿子集 }for(long long i=1;i<node[num].size();i++)//求前缀和 {sizsum[num].push_back(sizsum[num][i-1]+siz[da[node[num][i]]]-siz[node[num][i]]);}if(num!=1)//不是原树上的根节点 {posfa[num]=solve_compress(1,node[num].size()-1,num);}else{posfa[num]=solve_compress(2,node[num].size()-1,num);}h[num]=0;//清空 add(num,da[num],0);return ;
}
int main()
{ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n;for(long long i=1;i<n;i++){long long x,y,val;cin>>x>>y>>val;add(x,y,val);add(y,x,val);}cin>>m;dfs1(1);//预处理一对父子节点簇,子树大小,重儿子 dfs2(1,1);//预处理重儿子链 build(1);//递归建树 while(m--){char op;cin>>op;if(op=='C'){long long x;cin>>x;sum-=col[x];col[x]^=1;sum+=col[x];update(pos[x]);//修改 }else{//查询 if(sum<n){long long ans=clus[root].mx;if(col[clus[root].x]==0){ans=max({ans,0*1ll,clus[root].mu});}if(col[clus[root].y]==0){ans=max({ans,0*1ll,clus[root].md});}if(col[clus[root].x]==0&&col[clus[root].y]==0){ans=max({ans,0*1ll,clus[root].dis});}cout<<ans<<'\n';}else//无解 {cout<<"They have disappeared.\n";}}}return 0;
}