动态点分治入门 ZJOI2007 捉迷藏
这道题好神奇啊……如果要是不带修改的话那就是普通的点分治了,每次维护子树中距离次大值和最大值去更新。
不过这题要修改,而且还改500000次,总不能每改一次都点分治一次吧。
所以我们来认识一个新东西:带修改的点分治,动态点分治!
它可以强势解决带修改点分治问题(但是这玩意真的太难了我这个菜鸡只能学到入门)
首先我们要建立一棵树(点分树),这棵树是由点分治每次所分治的所有子树的重心串起来的。为什么要这么做呢?因为对于每次的修改,其实并没有影响到特别多的结果,它只会影响它自己所在的子树的重心,以及这个所对应重心在点分树上的祖先。
为什么可以这样做?因为我们考虑到,点分治中,每一个重心其实只会维护自己的子树中的情况,其他的情况于这个重心是不相干的。所以其实对于一次修改,它能影响的就是它所在子树重心的答案(这个是显然的),然后对于这个重心,它所在的树必然是它自己在点分树上的父亲(也就是一棵更大的树的重心,当前树是其子集)的一棵小子树,所以也会对之产生影响,然后再往上递归也是同理,这样的话,我们使用点分树维护,每次就只需要更改log个节点。
建立点分树怎么建?听起来或许很难但实际上特别简单,因为我们本身就是递归访问的,每次在递归求完子树重心的时候只要记录一下它当前的父亲是谁(就是当前的重心)就可以了。
先看一下代码。
void solve(int x)
{
vis[x] = ;
for(int i = head[x];i;i = e[i].next)
{
int t = e[i].to;
if(vis[t]) continue;
sum = sz[t],G = ;
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 = ;
const int INF = 1e9+; int read()
{
int ans = ,op = ;
char ch = getchar();
while(ch < '' || ch > '')
{
if(ch == '-') op = -;
ch = getchar();
}
while(ch >= '' && ch <= '')
{
ans *= ;
ans += ch - '';
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[][M<<],tot1,bin[],Log[M],dfn,num[M];
bool col[M],vis[M]; struct edge
{
int next,to;
}e[M<<]; 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 ;
return A.top();
}
int size()//返回当前状态数 = 总状态-无用状态
{
return A.size() - B.size();
}
int stop()//求A堆次大元素
{
if(size() < ) return ;
int x = top();pop();
int y = top();push(x);
return y;
}
}A,B[],C[]; void init()//这个是处理2的幂和每个数对应的log值,RMQ初始化
{
bin[] = ;rep(i,,) bin[i] = bin[i-] << ;
Log[] = -;rep(i,,) Log[i] = Log[i>>] + ;
} void dfs(int x,int fa)//同样是RMQ初始化,记录dfs序
{
anc[][++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] + ;
dfs(t,x);
anc[][++dfn] = dep[x];
}
} void ST()//ST表操作
{
rep(i,,Log[dfn])
rep(j,,dfn)
if(j + bin[i] - <= dfn)
anc[i][j] = min(anc[i-][j],anc[i-][j + bin[i-]]);
} int RMQ(int x,int y)//真实RMQ
{
x = num[x],y = num[y];
if(x > y) swap(x,y);
int t = Log[y-x+];
return min(anc[t][x],anc[t][y-bin[t]+]);
} int dis(int x,int y)//计算两点之间距离!
{
return dep[x] + dep[y] - * RMQ(x,y);
} void getG(int x,int fa)//找重心
{
sz[x] = ,maxs[x] = ;
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] = ;
for(int i = head[x];i;i = e[i].next)
{
int t = e[i].to;
if(vis[t]) continue;
sum = sz[t],G = ;
getG(t,x),fq[G] = x;solve(G);//在这里建立点分树
}
} void turnoff(int x,int v)//关灯
{
if(x == v)//第一个节点
{
B[x].push();
if(B[x].size() == ) 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 >= ) A.erase(maxn);//把这个无用的删了
if(B[f].size() >= ) A.push(cur);//把这个新的加进来
}
}
turnoff(f,v);//继续向上递归关灯
} void turnon(int x,int v)//开灯
{
if(x == v)
{
if(B[x].size() == ) A.erase(B[x].top());
B[x].erase();//和上面是正好相反的
}
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 >= ) A.erase(maxn);
if(B[f].size() >= ) A.push(cur);
}
}
turnon(f,v);//递归向上开灯
} int main()
{
init();n = read();
rep(i,,n-) x = read(),y = read(),add(x,y),add(y,x);
dfs(,),ST();//前面都预处理出来
sum = n;maxs[G] = INF;
getG(,);
fq[G] = ,solve(G);//找到重心开始建立点分树
rep(i,,n) col[i] = ,turnoff(i,i),tot1++;//把每个点都关灯
m = read();
while(m--)//开始修改
{
char c = getchar();
if(c == 'G')
{
if(tot1 <= ) printf("%d\n",tot1-);//要是只有一个点那就是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] ^= ;//转变开关灯情况
}
}
return ;
}
动态点分治入门 ZJOI2007 捉迷藏的更多相关文章
- 动态点分治:Bzoj1095: [ZJOI2007]Hide 捉迷藏
简介 这是我自己的一点理解,可能写的不好 点分治都学过吧.. 点分治每次找重心把树重新按重心的深度重建成了一棵新的树,称为分治树 这个树最多有log层... 动态点分治:记录下每个重心的上一层重心,这 ...
- 【BZOJ1095】[ZJOI2007]Hide 捉迷藏 动态树分治+堆
[BZOJ1095][ZJOI2007]Hide 捉迷藏 Description 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩捉 ...
- [bzoj1095][ZJOI2007]Hide 捉迷藏 点分树,动态点分治
[bzoj1095][ZJOI2007]Hide 捉迷藏 2015年4月20日7,8876 Description 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiaji ...
- BZOJ_1095_[ZJOI2007]Hide 捉迷藏_动态点分治+堆
BZOJ_1095_[ZJOI2007]Hide 捉迷藏_动态点分治+堆 Description 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子 ...
- BZOJ1095: [ZJOI2007]Hide 捉迷藏【动态点分治】
Description 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩 捉迷藏游戏.他们的家很大且构造很奇特,由N个屋子和N-1条 ...
- 【bzoj1095】[ZJOI2007]Hide 捉迷藏 动态点分治+堆
题目描述 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩捉迷藏游戏.他们的家很大且构造很奇特,由N个屋子和N-1条双向走廊组成,这 ...
- [ZJOI2007]捉迷藏(动态点分治/(括号序列)(线段树))
题目描述 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩捉迷藏游戏.他们的家很大且构造很奇特,由N个屋子和N-1条双向走廊组成,这N-1条 ...
- BZOJ1095 [ZJOI2007]Hide 捉迷藏 动态点分治 堆
原文链接https://www.cnblogs.com/zhouzhendong/p/BZOJ1095.html 题目传送门 - BZOJ1095 题意 有 N 个点,每一个点是黑色或者白色,一开始所 ...
- 2019.01.10 bzoj1095: [ZJOI2007]Hide 捉迷藏(动态点分治)
传送门 蒟蒻真正意义上做的第一道动态点分治! 题意:给一棵最开始所有点都是黑点的树,支持把点的颜色变成从黑/白色变成白/黑色,问当前状态树上两个最远黑点的距离. 思路: 首先考虑不带修改一次点分治怎么 ...
随机推荐
- bzoj 2721[Violet 5]樱花 数论
[Violet 5]樱花 Time Limit: 5 Sec Memory Limit: 128 MBSubmit: 671 Solved: 395[Submit][Status][Discuss ...
- [转]SQL SERVER数据库还原的方法
SQL SERVER数据库还原的方法 在SQL SERVER 2005下还原数据库 1.新建数据库A,右键还原数据库,此时目标数据库为A,选择备份 文件B_db_201311040200.BAK,还原 ...
- 反编译sencha toucha打包的apk文件,修改应用名称支持中文以及去除应用标题栏
一.去除安卓应用标题栏 sencha touch打包android安装包,去掉标题栏titlebar的简单方法 (有更复杂更好的方法,参看"二.利用反编译修改apk的应用名称为中文" ...
- hdu2157:How many ways??
n<=20个点m<=100条边有向图不带权,t个询问问Ai到Bi的经过k<=20条边方案数多少. f[i][j]--i到j的方案数,,初始化成初邻接矩阵,这样做一次就得到2条路最短路 ...
- msp430项目编程25
msp430中项目---带有断电保护的电子密码锁 1.I2C工作原理 2.I2C通信协议 3.代码(显示部分) 4.代码(功能实现) 5.项目总结 msp430项目编程 msp430入门学习
- poj2689素数问题
打算重新刷一下数论题,忘了很多了,水平也很差,此题入手就不顺了,刷了一个早上,只是一个简单 的素数应用罢了.题意:找出区间长度不超过10^6的最近的素数和最远的素数(相邻的), 算法:数在int范围内 ...
- 洛谷 P3811 【模板】乘法逆元
P3811 [模板]乘法逆元 题目背景 这是一道模板题 题目描述 给定n,p求1~n中所有整数在模p意义下的乘法逆元. 输入输出格式 输入格式: 一行n,p 输出格式: n行,第i行表示i在模p意义下 ...
- InfluxDB useful commands
InfluxDB 配置文件地址:/etc/influxdb/influxdb.conf 通过curl写数据 curl -i -XPOST 'http://localhost:8086/write?db ...
- Android实战简易教程-第三十九枪(第三方短信验证平台Mob和验证码自己主动填入功能结合实例)
用户注冊或者找回password时通常会用到短信验证功能.这里我们使用第三方的短信平台进行验证实例. 我们用到第三方短信验证平台是Mob,地址为:http://mob.com/ 一.注冊用户.获取SD ...
- 【c++】动态内存
静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.它主要存放静态数据.全局数据和常量.注意:const常量在定义时必须初始化 栈区:在执行函数时,函数内局部变量的存储单 ...