P2056 [ZJOI2007]捉迷藏
如果没有修改显然就直接点分治
有修改那就动态点分治
动态点分治就是在点分树上维护一些东西,查询时也在点分树上查
因为点分树深度是$log$的所以可以保证时间复杂度
此题我们需要在点分树上维护 $c$ 和 $f$
$f[x]$ 维护节点 $x$ 的子树中传给它父亲 $Fa$ 的所有路径长度
$c[x]$ 维护节点 $x$ 的每一个儿子 $v$ 的 $f[v]$ 的最大值
那么询问的答案就是 max(每个节点的 $c[x]$ 的最大值与次大值之和)
那么对于维护每一个 $c[x]$ ,我们枚举 $x$ 的所有儿子 $v$,然后把每个 $f[v]$ 的最大值加入到 $c[x]$
至于维护 $f[x]$ 就是点分治的基本操作了
对于每一个修改操作,我们要把点分树上一条链的数据都进行修改,具体来说
如果是关灯,那么对于点 $x$ 这条链往上的每一个节点 $now$,要往 $f[now]$ 里插入一个 $dis[x][\ dep[now]\ ]$
($dep$存每个节点的深度,$dis[x][i]$ 表示节点 $x$ 这条链上深度为 $i$ 的点与 $x$ 的距离)
反之如果是开灯就要删除相应的距离
修改 $f$ 后相应的 $c$ 也要改变
实现 $f,c$ 的数据结构需要查询最大值,次大值和支持删除,容易想到用平衡树或者 $multiset$ 来维护
但是题解里有一种更骚的"数据结构",维护两个大根堆 $hp$ 和 $rub$,分别存值和删除的数
查询最大值和次大值就直接在 $hp$ 里面查,删除数的话就把要删除的数加到 $rub$ 里面
在 $hp$ 里取出堆顶的值时如果第一个数和 $rub$ 里的第一个数相同就把两个堆的堆顶同时弹掉直到出现不同或有一个堆空了
正确性十分的显然,如果数 $val$ 有被删除(即加入了 $rub$ 里),并且处在 $hp$ 的堆顶,那么 $val$ 也一定在 $rub$ 的堆顶(因为 $rub$ 里的数显然是 $hp$ 的子集)
全局的答案也直接用这个数据结构来维护就好了
代码有注释
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> #include<queue> using namespace std; typedef long long ll; inline int read() { int x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*f; } const int N=2e5+7,INF=1e9+7; int fir[N],from[N<<1],to[N<<1],cntt; inline void add(int &a,int &b) { from[++cntt]=fir[a]; fir[a]=cntt; to[cntt]=b; } struct Heap {//c和f的数据结构 priority_queue <int> hp,rub; inline void ins(int x) { hp.push(x); } //插入 inline void del(int x) { hp.top()==x ? hp.pop() : rub.push(x); }//删除 inline int fir()//求最大值 { while(!hp.empty()&&!rub.empty()&&hp.top()==rub.top()) hp.pop(),rub.pop(); return hp.empty() ? -INF : hp.top();//注意判断堆是否为空 } inline int sec()//求次大值 { int t=fir(),res; if(t==-INF) return t; hp.pop(); res=fir(); hp.push(t); return res; } }Ans,c[N],f[N];//Ans维护全局的答案 int n,m,tot,rt,cnt; int sz[N],mx[N]; bool vis[N],p[N];//p是房间状态 void find_rt(int x,int fa)//找重心 { sz[x]=1; mx[x]=0; for(int i=fir[x];i;i=from[i]) { int &v=to[i]; if(vis[v]||v==fa) continue; find_rt(v,x); sz[x]+=sz[v]; mx[x]=max(mx[x],sz[v]); } mx[x]=max(mx[x],tot-sz[x]); if(mx[x]<mx[rt]) rt=x; } int Fa[N],dis[N][21],dep[N],ans[N];//Fa是点分树上的父亲,ans[x]是点x的答案,用来维护Ans int st[N],Top,Dep;//当前深度 void dfs(int x,int fa) { st[++Top]=x; dis[x][Dep]=dis[fa][Dep]+1;//维护dis for(int i=fir[x];i;i=from[i]) { int &v=to[i]; if(v==fa||vis[v]) continue; dfs(v,x); } } void build(int x)//建树顺便预处理初始c,f { vis[x]=1; dep[x]=dep[Fa[x]]+1;//维护点分树的dep for(int i=fir[x];i;i=from[i]) { int &v=to[i]; if(vis[v]) continue; tot=sz[v]; rt=0; find_rt(v,0); Dep=dep[x]+1; Top=0; dfs(v,0);//dfs从v开始 for(int j=1;j<=Top;j++) f[rt].ins(dis[st[j]][Dep]);//把子树的每个距离插入f[rt] c[x].ins(f[rt].fir());//用点分树儿子更新当前节点 Fa[rt]=x; build(rt);//继续向下建树 } c[x].ins(0);/*别忘了插一个0,本身到自己的距离也算*/ Ans.ins( ans[x]=c[x].fir()+c[x].sec() );//维护Ans并更新ans[x] } inline void change(int x)//修改操作,在点分树上维护数据 { int now=x,d,t1,t2; if(p[x]) cnt++,c[x].ins(0);//如果x为开灯,那么现在要关灯 else cnt--,c[x].del(0);//反之要开灯 while(1) { t1=c[now].fir()+c[now].sec(); if(t1!=ans[now]) Ans.del(ans[now]),Ans.ins(ans[now]=t1);//用c[now]更新Ans和ans[now] if(!Fa[now]) break; d=dep[now]; t1=f[now].fir(); p[x] ? f[now].ins(dis[x][d]) : f[now].del(dis[x][d]);//根据房间的状态判断是插入还是删除 t2=f[now].fir(); if(t1!=t2) c[Fa[now]].del(t1),c[Fa[now]].ins(t2);//如果状态有更新就用f[now]更新c[Fa[now]] now=Fa[now];//往父亲跳 } p[x]^=1;//改变房间状态 } int main() { int a,b; char ch[5]; cnt=n=read(); for(int i=1;i<n;i++) { a=read(),b=read(); add(a,b); add(b,a); } tot=n; mx[0]=INF; find_rt(1,0); build(rt); m=read(); while(m--) { scanf("%s",ch); if(ch[0]=='G') { if(cnt==1) printf("0\n"); else if(cnt==0) printf("-1\n"); else printf("%d\n",Ans.fir()); } else change(read()); } return 0; }