根据杨哲先生的论文(QTREE),可以得知,动态树问题是一类问题的统称,而解决这种问题最常用到的数据结构就是LCT(Link-Cut-Tree)。
LCT的大体思想类似于树链剖分中的轻重链剖分,轻重链剖分是处理出重链来,由于重链的定义和树链剖分是处理静态树所限,重链不会变化,变化的只是重链上的边或点的权值。由于这个性质,我们用线段树来维护树链剖分中的重链,但是LCT解决的是动态树问题(包含静态树),所以需要用更灵活的splay来维护这里的“重链”。
根据个人理解,动态树问题就是要:
很容易想到,如果没有第一点,那么第二点可以用树链剖分轻松解决。所以我们要学习动态树。
Preferred child(偏爱子节点):如果最后被访问的点在X的儿子P节点的子树中,那么称P为X的Preferred child,如果一个点被访问,他的Preferred child为null(即没有)。
Preferred edge(偏爱边):每个点到自己的Preferred child的边被称为Preferred edge。
Preferred path(偏爱路径):由Preferred edge组成的不可延伸的路径称为Preferred path。
access(u):访问u点,既把u到根的路径打通成为实边,并且u的孩子节点的实边变成虚边。也就是这条Preferred Path一端是根,一端是u。
这样我们可以发现一些比较显然的性质,每个点在且仅在一条Preferred path上,也就是所有的Preferred path包含了这棵树上的所有的点,这样一颗树就可以由一些Preferred path来表示(类似于轻重链剖分中的重链),我们用splay来维护每个条Preferred path,关键字为深度,也就是每棵splay中的点左子树的深度都比当前点小,右节点的深度都比当前节点的深度大。这样的每棵splay我们称为Auxiliary tree(辅助树),每个Auxiliary tree的根节点保存这个Auxiliary tree与上一棵Auxiliary tree中的哪个点相连。这个点称作他的Path parent。
大致上讲就是把某棵Splay的根节点指向这棵Splay所在实边最上方的点的父亲,也就是说,这是一条虚边。注意到这条虚边是单向的,由某棵Splay的根指向某一节点,但是那个节点的孩子里面并没有它。LCT的精髓就是通过这一条条的虚边将整棵大树串起来。
网络上有很多神犇大牛写的LCT论文的确很不错,但是毕竟我只是蒟蒻,还是看得云里雾里。所以在这里从结构体到建树方法,再到标记的上传下传都会详细介绍。留给刚刚接触LCT的初学者,方便大家学习,加深自己对于LCT的理解。
在讲解所有的操作之前,请各位读者好好想清splay(既LCT中的Auxiliary tree)的性质,如果遇到了看不明白的地方,也请回来看看这几个性质再仔细想一想为什么:
struct Node
{
int key,siz;
int add,minu,sum;
bool flip;
Node *ch[2],*fa;
void push_mul(const int m) {
minu=minu*m;
sum=sum*m;
add=add*m;
key=key*m;
}
void push_add(const int a) {
sum=sum+a*siz;
add=add+a;
key=key+a;
}
}_memory[maxn],*null=_memory;
/***下传结点信息***/
void clear_mark(Node* const x)
{
if(x==null) return;
if(x->flip)
{
x->ch[0]->flip^=1;
x->ch[1]->flip^=1;
std::swap(x->ch[0],x->ch[1]);
x->flip=false;
}
if(x->minu!=1)
{
if(x->ch[0]!=null)
x->ch[0]->push_mul(x->minu);
if(x->ch[1]!=null)
x->ch[1]->push_mul(x->minu);
x->minu=1;
}
if(x->add)
{
if(x->ch[0]!=null)
x->ch[0]->push_add(x->add);
if(x->ch[1]!=null)
x->ch[1]->push_add(x->add);
x->add=0;
}
}
/***更新结点信息***/
void update(Node* const x)
{
x->siz=x->ch[0]->siz+x->ch[1]->siz+1;
x->sum=x->key+x->ch[0]->sum+x->ch[1]->sum;
}
Make_tree():顾名思义就是建树了,在这里我们用*node[i]表示指向第i个结点的指针,递归建树。这里要注意,建树的时候我们只处理了子结点与父节点的关系,换句话说就是:孩子认了爸,但是爸还没认孩子。因为在进行Access操作之前,图中所有的边还都是虚边(不是Preferred edge的边)。
说明:neigh是个vector邻接表,用于存边,wei[i]表示i点的点权值。
/***建树***/
void Make_tree(int u,int fa)
{
Node* const node=_memory+u;
node->fa=_memory+fa;
node->key=node->sum=node->siz=node->minu=1;
node->ch[0]=node->ch[1]=null;
for(int i=0;i<(int)neigh[u].size();i++) if(neigh[u][i]!=fa)
Make_tree(neigh[u][i],u);
}
Rotate(Node* const,const int):最基础的操作,就是将传入的cur结点旋转至其父结点的位置。代码从上到下的步骤依次为:把指向父亲位置的指针提取到tmp中;将父亲位置原本连着cur的边连到旋转时cur让出的儿子;cur让出的儿子与新父亲“确定关系”,但前提是这个让出的儿子不是空指针;tmp的父亲变成cur的父亲,cur上位(这时需要画个图自行YY);cur继承tmp与父节点之间的关系;既然cur已上位,那么确定cur与tmp之间的关系(画图很重要,脑补);更新tmp的信息。
在这里画图理解很重要,虽然挺简单的。
/***将cur结点旋转到父结点位置***/
void Rotate(Node* const cur,const int dir)
{
Node* const tmp=cur->fa;
tmp->ch[dir^1]=cur->ch[dir];
if(cur->ch[dir]!=null) cur->ch[dir]->fa=tmp;
cur->fa=tmp->fa;
if(tmp->fa->ch[0]==tmp) cur->fa->ch[0]=cur;
else if(tmp->fa->ch[1]==tmp) cur->fa->ch[1]=cur;
tmp->fa=cur;
cur->ch[dir]=tmp; //只有在这里连接实边(Preferred edge)
tmp->maintain();
}
Splay_parent(Node*,Node*&) 和 Splay(Node* const):这个是继Rotate之后第二基础的操作了,所有的操作一定会用到splay。有的人说会写平衡树的那个splay就会写这个splay,但是我的几乎所有同学都是按照刘汝佳刘老师的模版写的splay,既从根节点开始向下找然后向上旋到根节点。不过在LCT中,并不是每两个有父子关系的结点都能通过“找儿子” 的方式去遍历,因为一开始的时候图中的边都是虚边,既只能通过找爸爸的方式向上旋到根节点。所以说只能从某个结点结点向上推。
这里我还加入了一个函数Splay_parent(Node*,Node*&),就是为了保证在旋x结点的时候不会被旋出splay,也就是说在旋的时候不会走虚边。同时也把y指针处理为x指针当前的父亲结点。
在splay函数里面只有一个循环,我们用*y和*z分别表示*x的父亲结点和爷爷结点,保证在同一棵splay的情况才会向上旋。而且务必要注意的是,旋转之前,旋谁就先传一下谁的标记。如果有爷爷结点的话,就一套双旋带上去;否则直接一个单旋,结束战斗。
最后别忘了随手maintain一下。
/***看x的父亲y是否是x所在Splay的父亲***/
bool Splay_parent(Node* x,Node* (&y))
{
return (y=x->fa)!=null && (y->ch[0]==x || y->ch[1]==x);
}
/***将x结点旋到splay的根***/
void Splay(Node* const x)
{
clear_mark(x);
for(Node *y,*z;Splay_parent(x,y);)
if(Splay_parent(y,z))
{
clear_mark(z);
clear_mark(y);
clear_mark(x);
const int c=y==z->ch[0];
if(x==y->ch[c]) Rotate(x,c^1),Rotate(x,c);
else Rotate(y,c),Rotate(x,c);
}
else
{
clear_mark(y);
clear_mark(x);
Rotate(x,x==y->ch[0]);
break;
}
update(x);
return;
}
一开始第一次写的时候我就崩溃在了bool Splay_parent()里面,原因很简单,因为我建树的时候默认node[1]结点的父亲是node[0],而node[0]指针因为在建树的时候没有遍历过,也就是说node[0]没有被new Node()赋初值,导致node[0]一直都是0x0,在调用node[0]->fa时崩溃,所以提醒大家在第一次写的时候一定要注意。
Access(u):访问操作是核心操作,我们保证做完之后,把u到根的路径打通成为实边,并且u的孩子节点的实边变成虚边。也就是这条Prefer Path一端是根,一端是u。
/***访问u结点***/
Node* Access(Node* u)
{
Node* v=null;
for(;u!=null;u=u->fa)
{
Splay(u);
u->ch[1]=v;
update(v=u);
}
return v;
}
大致的意思就是说,每次把一个节点旋到Splay的根,然后把上一次的Splay的根节点当作当前根的右孩子(也就是原树中的下方)。第一次初始 v=null是为了清除u原来的孩子。 因为不是每次access都需要把最后节点旋到Splay的根,所以我就不在最后splay(v)了。 返回值是最后访问到的节点,也就是原树的根。
具体原理如图所示:
所以在这里再说一遍,反复理解,我们保证做完之后,把u到根的路径打通成为实边,并且u的孩子节点的实边变成虚边。也就是这条Prefer Path一端是根,一端是u。返回的指针Node*是以根节点为根,从跟到u结点的Preferred path。
以上是LCT的核心操作,如果你看懂了上面的核心操作,那么请继续往下看。如果没有看懂上面的核心操作的话,那么再看几遍直到看懂为止。因为以下的实际应用的操作都是基于核心操作进行的。
Make_root(Node* const):换根操作。首先将从根到x结点的链提出来(Access(x)),将其翻转,这里所说的翻转是splay意义上的左右翻转,而在树的形态的意义上是上下翻转,既改变了父子关系,更准确地说是调换了父子关系。(这段话没看懂的话,翻上去看看性质里面是怎么说的)
翻转了父子关系之后,传一下标记,顺带把x提到根节点就好了。
/***使x结点变成根***/
void Make_root(Node* const x)
{
Access(x)->flip=true;
Splay(x);
return;
}
Node* Get_root(Node*):找到x所在结点的根,返回值就是指向根的位置的指针。其实现的原理就是找到一条连接根和x的链,找到链之后不断向左子节点推。如果不知道为什么向左子结点推的话就再看看性质,因为在向splay里面向左子结点推就相当于在树上向根推啊!
/***得到x所在的树的树根***/
Node* Get_root(Node* x)
{
for(x=Access(x);clear_mark(x),x->ch[0]!=null;x=x->ch[0]);
return x;
}
Link(Node* const,Node* const) 和 Cut(Node* const,Node* const):这个就是LCT最经常使用的操作了。不过理解了上面的部分之后都比较好理解。
Link就是连接两棵子树,先让x变成x所在的子树的根,然后从向y连一条虚边即可,最后那个Access可以不用,好像没有什么影响的样子。
Cut就是将一棵子树分为两个,也就可以理解为以x为树根,把y到x的路径分离出来。(要是看不懂代码的话就回去翻翻性质)
/***连接两棵树***/
void Link(Node* const x,Node* const y)
{
Make_root(x);
x->fa=y;
Access(x);
return;
}
/***割开两棵树***/
void Cut(Node* const x,Node* const y)
{
Make_root(x);
Access(y);
Splay(y);
y->ch[0]->fa=null;
y->ch[0]=null;
update(y);
return;
}
Query(Node*,Node* ) 和 Modify(Node*,Node* ,const int):就是问题中经常出现的查询和修改。以从x结点到y结点的路径上的点权权值和为例。
/***查询x和y路径上的相关值***/
int Query(Node* x,Node* y)
{
Make_root(x);
Access(y),Splay(y);
return y->sum;
}
/***将x到y路径上的值加上val***/
void Modify_add(Node* x,Node* y,const int val)
{
Make_root(x);
Access(y),Splay(y);
y->push_add(val);
return;
}
Description
一棵n个点的树,每个点的初始权值为1。对于这棵树有q个操作,每个操作为以下四种操作之一:
+ u v c:将u到v的路径上的点的权值都加上自然数c;
- u1 v1 u2 v2:将树中原有的边(u1,v1)删除,加入一条新边(u2,v2),保证操作完之后仍然是一棵树;
* u v c:将u到v的路径上的点的权值都乘上自然数c;
/ u v:询问u到v的路径上的点的权值和,求出答案对于51061的余数。Input
第一行两个整数n,q
接下来n-1行每行两个正整数u,v,描述这棵树
接下来q行,每行描述一个操作Output
对于每个/对应的答案输出一行Sample Input
3 2
1 2
2 3
* 1 3 4
/ 1 1Sample Output
4100%的数据保证,1<=n,q<=10^5,0<=c<=10^4
LCT裸题,连边的时候Link,断边的时候Cut。
对于覆盖类标记不用想太多,如果是像区间赋值这样会“覆盖”已有的非覆盖类标记的标记,那就清除已有标记,然后直接打上新的标记;如果是区间翻转这种和其他标记独立开来的标记,就单独处理。处理这类标记时不需要先clear_mark。
对于非覆盖类标记就要仔细思考了。因为标记之间有可能互相影响,所以处理标记的顺序是很重要的。就拿这道题来说,有两类非覆盖类标记:加法标记和乘法标记。应该先下传乘法标记,再下传加法标记。同时下传乘法标记时,要给儿子的加法增量也乘上乘法标记,写成公式就是(x + add) * c = x * c + add* c。有同样标记的线段树题有一道BZOJ1798。Splay和线段树中的标记下传类似(可以说完全相同)。
还有就是标记下传(clear_mark)与信息更新(update)的时机。由于下传的时候会直接修改子节点的key和sum等信息,也就是说,打了标记的节点本身的信息已经是最新了的,所以不要update,update反而错了。个人总结的Splay需要clear_mark的地方分别是rotate过程中对x进行下传、splay过程中每次旋转前对x的祖父节点(如果有)和父节点依次下传,最后在splay过程结束之前先下传再更新。
代码:
/**************************************************************
Problem: 2631
User: CHN
Language: C++
Result: Accepted
Time:15236 ms
Memory:9220 kb
****************************************************************/
#include
using namespace std;
#define pb push_back
#define mp make_pair
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
/***读入输出优化***/
inline int read()
{
char ch;
bool flag=false;
int a=0;
while(!(((ch=getchar())>='0' && ch<='9') || ch=='-'));
if(ch!='-') a=a*10+ch-'0';
else flag = true;
while((ch=getchar())>='0' && ch<='9')
a=a*10+ch-'0';
if(flag) a=-a;
return a;
}
void write(int a)
{
if(a<0)
{
putchar('-');
a=-a;
}
if(a>=10) write(a / 10);
putchar(a%10+'0');
}
const int maxn=int(1e5)+10;
const int moder=51061;
int n,m;
vector <int> neigh[maxn];
int wei[maxn];
unsigned int lop=1;
struct Node
{
int key,siz;
int add,minu,sum;
bool flip;
Node *ch[2],*fa;
void push_mul(const long long m) {
minu=minu*m%moder;
sum=sum*m%moder;
add=add*m%moder;
key=key*m%moder;
}
void push_add(const int a) {
sum=(sum+1LL*a*siz)%moder;
add=add+a;
key=key+a;
}
}_memory[maxn],*null=_memory;
inline void clear_mark(Node* const x)
{
if(x==null) return;
if(x->flip)
{
x->ch[0]->flip^=1;
x->ch[1]->flip^=1;
std::swap(x->ch[0],x->ch[1]);
x->flip=false;
}
if(x->minu!=1)
{
if(x->ch[0]!=null)
x->ch[0]->push_mul(x->minu);
if(x->ch[1]!=null)
x->ch[1]->push_mul(x->minu);
x->minu=1;
}
if(x->add)
{
if(x->ch[0]!=null)
x->ch[0]->push_add(x->add);
if(x->ch[1]!=null)
x->ch[1]->push_add(x->add);
x->add=0;
}
}
inline void update(Node* const x)
{
x->siz=x->ch[0]->siz+x->ch[1]->siz+1;
x->sum=x->key+x->ch[0]->sum+x->ch[1]->sum;
}
/***将cur结点旋转到父结点位置***/
inline void Rotate(Node* const cur,const int dir)
{
Node* const tmp=cur->fa;
tmp->ch[dir^1]=cur->ch[dir];
if(cur->ch[dir]!=null) cur->ch[dir]->fa=tmp;
cur->fa=tmp->fa;
if(tmp->fa->ch[0]==tmp) cur->fa->ch[0]=cur;
else if(tmp->fa->ch[1]==tmp) cur->fa->ch[1]=cur;
tmp->fa=cur;
cur->ch[dir]=tmp;
update(tmp);
}
/***看x的父亲y是否是x所在Splay的父亲***/
inline bool Splay_parent(Node* x,Node* (&y))
{
return (y=x->fa)!=null && (y->ch[0]==x || y->ch[1]==x);
}
/***将x结点旋到根***/
inline void Splay(Node* const x)
{
clear_mark(x);
for(Node *y,*z;Splay_parent(x,y);)
if(Splay_parent(y,z))
{
clear_mark(z);
clear_mark(y);
clear_mark(x);
const int c=y==z->ch[0];
if(x==y->ch[c]) Rotate(x,c^1),Rotate(x,c);
else Rotate(y,c),Rotate(x,c);
}
else
{
clear_mark(y);
clear_mark(x);
Rotate(x,x==y->ch[0]);
break;
}
update(x);
return;
}
/***访问u结点***/
inline Node* Access(Node* u)
{
Node* v=null;
for(;u!=null;u=u->fa)
{
Splay(u);
u->ch[1]=v;
update(v=u);
}
return v;
}
/***使x结点变成根***/
inline void Make_root(Node* const x)
{
Access(x)->flip=true;
Splay(x);
return;
}
/***得到x所在的树的树根***/
inline Node* Get_root(Node* x)
{
for(x=Access(x);clear_mark(x),x->ch[0]!=null;x=x->ch[0]);
return x;
}
/***连接两棵树***/
inline void Link(Node* const x,Node* const y)
{
Make_root(x);
x->fa=y;
Access(x);
return;
}
/***割开两棵树***/
inline void Cut(Node* const x,Node* const y)
{
Make_root(x);
Access(y);
Splay(y);
y->ch[0]->fa=null;
y->ch[0]=null;
update(y);
return;
}
/***查询x和y路径上的相关值***/
inline int Query(Node* x,Node* y)
{
Make_root(x);
Access(y),Splay(y);
return y->sum%moder;
}
/***将x到y路径上的值加上val***/
inline void Modify_add(Node* x,Node* y,const int val)
{
Make_root(x);
Access(y),Splay(y);
y->push_add(val);
return;
}
/***将x到y路径上的值乘上val***/
inline void Modify_minu(Node* x,Node* y,const int val)
{
Make_root(x);
Access(y),Splay(y);
y->push_mul(val);
return;
}
/***建树***/
void Make_tree(int u,int fa)
{
Node* const node=_memory+u;
node->fa=_memory+fa;
node->key=node->sum=node->siz=node->minu=1;
node->ch[0]=node->ch[1]=null;
for(int i=0;i<(int)neigh[u].size();i++) if(neigh[u][i]!=fa)
Make_tree(neigh[u][i],u);
}
int main()
{
null->fa=null->ch[0]=null->ch[1]=null;
n=read(),m=read();
for(int x,y,i=1;i1,0);
for(int i=1;i<=m;i++)
{
char op=getchar();
int u=read(),v=read(),c,u1,u2;
if(op=='+')
c=read(),
Modify_add(_memory+u,_memory+v,c);
else if(op=='*')
c=read(),
Modify_minu(_memory+u,_memory+v,c);
else if(op=='-')
u1=read(),u2=read(),
Cut(_memory+u,_memory+v),
Link(_memory+u1,_memory+u2);
else if(op=='/')
write(Query(_memory+u,_memory+v)),
putchar('\n');
}
return 0;
}