[SDOI2017] 树点涂色
一、题目
三操作是到根的路径哦,不要以为是到子树根的路径。
二、解法
一定要仔细观察题目中的修改有没有什么特殊性质:点 \(x\) 到根节点的路径上所有的点染上一种没有用过的新颜色
我感到了一种神秘的熟悉感,这个东西不是 \(\tt lct\) 的那个 \(\tt access\) 操作吗?也就是我们把虚边看成连边的两个点之间颜色不同,实边看成颜色相同,那么一个点到根的路径权值就是虚边数量\(+1\)
所以我们在 \(\tt access\) 的时候要考虑虚实边变化带来的权值影响,其实每次就是修改一个子树的权值,有 \(dfn\) 序加线段树维护即可。具体的写法中我们要寻找这条边在原树上代表的边对应的点,直接一直往左儿子下边找即可。
\(\tt lct\) 中一个点转到根后,它的祖先要么在拉上去的虚边上面,要么在自己左儿子方向。注意 \(\tt lct\) 上的边并不是原树的结构(虚边指向指向该 \(Splay\) 中中序遍历最靠前的点在原树中的父亲),但是深度关系往往能帮上忙。
这一部分可以看一下魔改之后的 \(\tt access\) 代码:
void access(int x)
{
for(int y=0;x;x=par[y=x])
{
splay(x);//先把x转到根
if(ch[x][1])//原来的右儿子要变虚了
{
int p=find(ch[x][1]);//找到最左的祖先
upd(1,1,n,dfn[p],dfo[p],1);//在线段树上改
}
if(y)//这个点又要变实了
{
int p=find(y);
upd(1,1,n,dfn[p],dfo[p],-1);
}
ch[x][1]=y;
}
}
但是有人说直接找左儿子好像时间复杂度是错的,但是由于我不会 \(\tt lct\) 的时间复杂度分析所以我也不知道,据说更好的写法是 \(\tt splay\) 里面就维护深度最小的点。
现在看第一个询问操作,其实问的就是 \((x,y)\) 路径之间的虚边数量,直接差分就可以了,设 \(dis[i]\) 等于虚边数量加\(1\),一开始的值就是深度,那么答案是 \(dis[x]+dis[y]-2\cdot dis[lca]+1\),第二个询问操作就是子树内最大值,没什么好说的。
时间复杂度 \(O(n\log^2 n)\)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,tot,f[M],ch[M][2],par[M],fa[M][20];
int Ind,dep[M],dfn[M],dfo[M],mx[4*M],tag[4*M];
struct edge
{
int v,next;
edge(int V=0,int N=0) : v(V) , next(N) {}
}e[2*M];
//预处理部分
void dfs(int u,int p)
{
fa[u][0]=p;
dfn[u]=++Ind;
dep[u]=dep[p]+1;
par[u]=p;//一开始都连虚边
for(int i=1;i<20;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(v==p) continue;
dfs(v,u);
}
dfo[u]=Ind;
}
int lca(int u,int v)
{
if(dep[u]<dep[v]) swap(u,v);
for(int i=19;i>=0;i--)
if(dep[fa[u][i]]>=dep[v])
u=fa[u][i];
if(u==v) return u;
for(int i=19;i>=0;i--)
if(fa[u][i]^fa[v][i])
u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
//线段树部分
void down(int i)
{
mx[i<<1]+=tag[i];
tag[i<<1]+=tag[i];
mx[i<<1|1]+=tag[i];
tag[i<<1|1]+=tag[i];
tag[i]=0;
}
void upd(int i,int l,int r,int L,int R,int f)
{
if(L>r || l>R) return ;
if(L<=l && r<=R)
{
mx[i]+=f;
tag[i]+=f;
return ;
}
int mid=(l+r)>>1;
down(i);
upd(i<<1,l,mid,L,R,f);
upd(i<<1|1,mid+1,r,L,R,f);
mx[i]=max(mx[i<<1],mx[i<<1|1]);
}
int ask(int i,int l,int r,int L,int R)
{
if(L>r || l>R) return 0;
if(L<=l && r<=R) return mx[i];
int mid=(l+r)>>1;
down(i);
return max(ask(i<<1,l,mid,L,R),ask(i<<1|1,mid+1,r,L,R));
}
//lct部分
int nrt(int x)
{
return ch[par[x]][0]==x || ch[par[x]][1]==x;
}
int chk(int x)
{
return ch[par[x]][1]==x;
}
void rotate(int x)
{
int y=par[x],z=par[y],k=chk(x),w=ch[x][k^1];
ch[y][k]=w;par[w]=y;
if(nrt(y)) ch[z][chk(y)]=x;par[x]=z;
ch[x][k^1]=y;par[y]=x;
}
void splay(int x)
{
while(nrt(x))
{
int y=par[x],z=par[y];
if(nrt(y))
{
if(chk(y)==chk(x)) rotate(y);
else rotate(x);
}
rotate(x);
}
}
int find(int x)//找到最左儿子
{
while(ch[x][0]) x=ch[x][0];
return x;
}
void access(int x)
{
for(int y=0;x;x=par[y=x])
{
splay(x);
if(ch[x][1])
{
int p=find(ch[x][1]);
upd(1,1,n,dfn[p],dfo[p],1);
}
if(y)
{
int p=find(y);
upd(1,1,n,dfn[p],dfo[p],-1);
}
ch[x][1]=y;
}
}
signed main()
{
n=read();m=read();
for(int i=1;i<n;i++)
{
int u=read(),v=read();
e[++tot]=edge(v,f[u]),f[u]=tot;
e[++tot]=edge(u,f[v]),f[v]=tot;
}
dfs(1,0);
for(int i=1;i<=n;i++)
upd(1,1,n,dfn[i],dfn[i],dep[i]);//初始化
while(m--)
{
int op=read(),x=read();
if(op==1) access(x);
if(op==2)
{
int y=read(),t=lca(x,y);
printf("%d\n",ask(1,1,n,dfn[x],dfn[x])+
ask(1,1,n,dfn[y],dfn[y])-2*ask(1,1,n,dfn[t],dfn[t])+1);
}
if(op==3)
printf("%d\n",ask(1,1,n,dfn[x],dfo[x]));
}
}