动态点分治入门 ZJOI2007 捉迷藏
这道题好神奇啊……如果要是不带修改的话那就是普通的点分治了,每次维护子树中距离次大值和最大值去更新。
不过这题要修改,而且还改500000次,总不能每改一次都点分治一次吧。
所以我们来认识一个新东西:带修改的点分治,动态点分治!
它可以强势解决带修改点分治问题(但是这玩意真的太难了我这个菜鸡只能学到入门)
首先我们要建立一棵树(点分树),这棵树是由点分治每次所分治的所有子树的重心串起来的。为什么要这么做呢?因为对于每次的修改,其实并没有影响到特别多的结果,它只会影响它自己所在的子树的重心,以及这个所对应重心在点分树上的祖先。
为什么可以这样做?因为我们考虑到,点分治中,每一个重心其实只会维护自己的子树中的情况,其他的情况于这个重心是不相干的。所以其实对于一次修改,它能影响的就是它所在子树重心的答案(这个是显然的),然后对于这个重心,它所在的树必然是它自己在点分树上的父亲(也就是一棵更大的树的重心,当前树是其子集)的一棵小子树,所以也会对之产生影响,然后再往上递归也是同理,这样的话,我们使用点分树维护,每次就只需要更改log个节点。
建立点分树怎么建?听起来或许很难但实际上特别简单,因为我们本身就是递归访问的,每次在递归求完子树重心的时候只要记录一下它当前的父亲是谁(就是当前的重心)就可以了。
先看一下代码。
void solve(int x) { vis[x] = 1; for(int i = head[x];i;i = e[i].next) { int t = e[i].to; if(vis[t]) continue; sum = sz[t],G = 0; getG(t,x),fq[G] = x;solve(G); } }
其中建树的就是fq[G] = x那一行……是不是非常好做?
然后假设我们现在建完了这棵点分树(也就是我们遍历了整棵树),之后对于无穷无尽的修改操作我们该咋办呢……
我们还是想刚才那个事,如果要是不修改的话,我们只要求出来当前重心所有子树中的最长和次长距离来更新答案就可以,虽然现在带上修改了,但是我们计算答案的方法是并不会变的!
所以我们发现了这几件事:
1.我们可以对于每一个重心(点分树上的每一个节点)都开一个堆来维护当前子树中的最大值(C堆)
2.对于每一个重心,再开一个堆,记录它所有子树中的距离最大值和次大值(也就是上面C堆中两个最大的堆顶元素)(B堆)
3.开一个全局的堆,记录所有重心的距离最大值和次大值(也就是B堆中两个最大的堆顶元素)(A堆)
这样我们就可以进行维护了!每次修改是log的,用堆维护也是log的,总复杂度是log^2的。
说的如此轻描淡写该咋做呀……(这玩意简直不是一般难写,而且还贼难理解)
我们不选择使用set而是开一个结构体,里面有两个堆,其中一个用来存有用的状态一个存无用状态。(啥叫有用无用状态?)我们直接用set查找元素进行删除其实比较慢,我们可以这样做,每次把要删除的状态存到另一个堆里面,等真正要进行删除操作的时候,我们再将其删除。
是不是感觉没听懂?对其实我也不大懂。
大致意思就是,因为一些修改操作使得一些状态变得不合法,我们不用什么find函数之类的直接给他删了,而是加到无用状态里,只有在堆顶是无用状态的时候我们把其删除即可。
好。之后我们先考虑把白点变黑点的操作(我们姑且称之为“关灯操作”)
我们每次求的时候,首先更新一下当前节点的B堆,把一个0的情况加进去(因为你相当于没走嘛),之后如果当前的B的大小是2,也就说明有了最大和次大值,我们就向A堆里面添加一下这个值。
之后我们先计算一下当前重心的在点分树上的父亲和这点的距离。因为你是把当前点变成了黑点,所以这个点的答案必然是合法的,我们把其压入当前的C堆中。之后,如果这个值大于当前C堆中的最大值,那说明我们这次修改对这个范围的答案是有影响的,肯定是要进行一次修改了。那我们先统计这个重心的父亲的B堆中的最大和次大值,之后把堆顶元素弹出(因为当前这个值已经更大了说明它没用了),把这个更大的答案加进去。之后再次计算当前重心的父亲的B堆中的最大和次大值,如果要是比原来大,并且B中有两个以上的元素(说明有最大值和次大值,只有一个是不能删除的,因为旧的答案可以成为次大值),那么我们在A堆中把这个答案删除,再把新答案添加进去即可。注意添加答案的前提是,B堆中至少也有两个元素,这样才保证有了最大值和次大值。才可以更新。
之后我们重复上述操作,向上递归更新。这样关灯操作就完成了。
与之对应的是开灯操作。
开灯操作大部分都是相对应的,因为开灯之后,我们的答案将变得不合法,所以我们需要删去这些答案。
这里的操作只有在你当前的答案就是堆顶元素的时候才会去更新。更新的方法和上面都是对应且相反的,直接看代码即可。
然后最后一个问题是,如何O(1)求出树上两点之间的距离。这个可以使用dfs序,st表转化为RMQ问题解决(这个要好好复习了)
所以我们总结一下做这题的步骤。
1.先手点分治,把点分树建出来同时遍历每棵树,确定初始的最长距离。
2.初始化关于RMQ的一些数组和函数
3.建立三个堆,每个堆里面再用两个堆去模拟set进行维护
4.把所有的点全部关一次灯。
5.开始修改,每次修改对应上面的开,关灯操作,每次输出结果即可。
最后还有啥不懂的看一下代码,要是还不懂我们慢慢来(其实我也没完全理解,还是慢慢来)
// luogu-judger-enable-o2//这题不开O2的话会T…… #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #include<iostream> #include<queue> #include<set> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar('\n') using namespace std; typedef long long ll; const int M = 200005; const int INF = 1e9+7; int read() { int ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } int n,m,G,ecnt,sum,fq[M],dep[M],maxs[M],sz[M],head[M],x,y; int anc[20][M<<1],tot1,bin[20],Log[M],dfn,num[M]; bool col[M],vis[M]; struct edge { int next,to; }e[M<<1]; void add(int x,int y) { e[++ecnt].to = y; e[ecnt].next = head[x]; head[x] = ecnt; } struct heap { priority_queue<int> A,B;//优先队列模拟堆 void push(int x)//添加一个状态 { A.push(x); } void erase(int x) //删除状态 { B.push(x); } void pop()//把A堆的堆顶元素弹出 { while(B.size() && (A.top() == B.top())) A.pop(),B.pop(); A.pop(); } int top()//求A堆堆顶元素 { while(B.size() && (A.top() == B.top())) A.pop(),B.pop(); if(!A.size()) return 0; return A.top(); } int size()//返回当前状态数 = 总状态-无用状态 { return A.size() - B.size(); } int stop()//求A堆次大元素 { if(size() < 2) return 0; int x = top();pop(); int y = top();push(x); return y; } }A,B[150000],C[150000]; void init()//这个是处理2的幂和每个数对应的log值,RMQ初始化 { bin[0] = 1;rep(i,1,19) bin[i] = bin[i-1] << 1; Log[0] = -1;rep(i,1,200000) Log[i] = Log[i>>1] + 1; } void dfs(int x,int fa)//同样是RMQ初始化,记录dfs序 { anc[0][++dfn] = dep[x],num[x] = dfn; for(int i = head[x];i;i = e[i].next) { int t = e[i].to; if(t == fa) continue; dep[t] = dep[x] + 1; dfs(t,x); anc[0][++dfn] = dep[x]; } } void ST()//ST表操作 { rep(i,1,Log[dfn]) rep(j,1,dfn) if(j + bin[i] - 1 <= dfn) anc[i][j] = min(anc[i-1][j],anc[i-1][j + bin[i-1]]); } int RMQ(int x,int y)//真实RMQ { x = num[x],y = num[y]; if(x > y) swap(x,y); int t = Log[y-x+1]; return min(anc[t][x],anc[t][y-bin[t]+1]); } int dis(int x,int y)//计算两点之间距离! { return dep[x] + dep[y] - 2 * RMQ(x,y); } void getG(int x,int fa)//找重心 { sz[x] = 1,maxs[x] = 0; for(int i = head[x];i;i = e[i].next) { int t = e[i].to; if(t == fa || vis[t]) continue; getG(t,x); sz[x] += sz[t]; maxs[x] = max(maxs[x],sz[t]); } maxs[x] = max(maxs[x],sum - sz[x]); if(maxs[x] < maxs[G]) G = x; } void solve(int x)//点分治+建立点分树 { vis[x] = 1; for(int i = head[x];i;i = e[i].next) { int t = e[i].to; if(vis[t]) continue; sum = sz[t],G = 0; getG(t,x),fq[G] = x;solve(G);//在这里建立点分树 } } void turnoff(int x,int v)//关灯 { if(x == v)//第一个节点 { B[x].push(0); if(B[x].size() == 2) A.push(B[x].top());//有最大和次大即更新 } if(!fq[x]) return; int f = fq[x],D = dis(f,v),tmp = C[x].top();//计算当前两点间距离和这个点C堆的最大值 C[x].push(D);//把合法答案压入 if(D > tmp)//如果这个值更优,说明修改产生了影响,要更新 { int maxn = B[f].top() + B[f].stop(),size = B[f].size();//计算当前最大值(最大和次大更新)和B的大小 if(tmp) B[f].erase(tmp);//把这个无用的删了 B[f].push(D);//把有用的加进来 int cur = B[f].top() + B[f].stop();//重计算一下答案 if(cur > maxn)//如果新答案更优 { if(size >= 2) A.erase(maxn);//把这个无用的删了 if(B[f].size() >= 2) A.push(cur);//把这个新的加进来 } } turnoff(f,v);//继续向上递归关灯 } void turnon(int x,int v)//开灯 { if(x == v) { if(B[x].size() == 2) A.erase(B[x].top()); B[x].erase(0);//和上面是正好相反的 } if(!fq[x]) return; int f = fq[x],D = dis(f,v),tmp = C[x].top(); C[x].erase(D);//把这个答案给删了(不合法) if(D == tmp)//如果这个答案=堆顶元素,说明这次修改产生了影响 { int maxn = B[f].top() + B[f].stop(),size = B[f].size(); B[f].erase(D); if(C[x].top()) B[x].push(C[x].top()); int cur = B[f].top() + B[f].stop(); if(cur < maxn)//这些和上面都是相同的操作了,注意这次变成了小于 { if(size >= 2) A.erase(maxn); if(B[f].size() >= 2) A.push(cur); } } turnon(f,v);//递归向上开灯 } int main() { init();n = read(); rep(i,1,n-1) x = read(),y = read(),add(x,y),add(y,x); dfs(1,0),ST();//前面都预处理出来 sum = n;maxs[G] = INF; getG(1,0); fq[G] = 0,solve(G);//找到重心开始建立点分树 rep(i,1,n) col[i] = 1,turnoff(i,i),tot1++;//把每个点都关灯 m = read(); while(m--)//开始修改 { char c = getchar(); if(c == 'G') { if(tot1 <= 1) printf("%d\n",tot1-1);//要是只有一个点那就是0,要是没有直接输出-1,tot1记录当前黑点数 else printf("%d\n",A.top());//否则输出最大值 } else { x = read(); if(col[x]) turnon(x,x),tot1--;//开灯 else turnoff(x,x),tot1++;//关灯 col[x] ^= 1;//转变开关灯情况 } } return 0; }