[ZJOI2007]捉迷藏 解题报告 (动态点分治)

[ZJOI2007]捉迷藏

近期做过的码量最大的一题 (当然也是我写丑了....)

题意

有一个 \(n\) 个节点的树 (\(n \le 10^5\)), 每个节点为黑色或白色.

有 \(m\) 个操作 (\(m \le 5 \times 10^5\)), 操作有两种,

  1. 将点 \(x\) 的的颜色翻转.
  2. 查询树上距离最远的黑色点对之间的距离.

思路

首先, 如果没有修改操作的话, 就是一个裸的点分治 (点分治学习笔记).

有修改操作, 那就 动态点分治.

动态点分治的基本思路是 (个人总结的) : 由于树的结构不会变化, 所以可以先找出重心, 并构建点分树 (一个连接每一层重心的树), 然后维护点分治中需要知道的数据, 避免了进行递归查找重心带来的时间浪费.

基本思路是简单的, 困难的是归纳出需要知道的数据并对它们进行维护.

(注 : 下文中如果没有特别说明, 则 "儿子", "子树", "父亲", "距离" 等名词都是指在原树上的.)

这道题, 我们在点分治过程中, 对每一个点需要知道的数据为 :

  1. 以它为重心时, 在它 管辖范围内的黑色节点 到它的 距离最大值和次大值, 称为数据 1.
  2. 以它为重心时, 它的管辖范围内的所有点到上一层重心 (即它在点分树上的父亲) 的距离, 称为数据 2.

这两个数据的关系是 :

一个点把自己的 数据 2 中的最大值 贡献给 点分树上的父亲, 点分树上的父亲 根据每个儿子贡献出来的最大值, 得到自己 数据 1 的最大值和次大值.

显然, 在得到 最大值和次大值 的过程中, 我们需要一个支持排序的数据结构, 这里我们选择使用 .

刚才我们提到的 点分树, 有一个比较显然的性质 : 它的树高是 \(\log n\) 级别的.

那么我们就可以在修改一个节点时, 依次维护它的每一个祖先的数据, 并且只有 \(O(\log n)\) 的时间复杂度.

但是, 在维护过程中, 我们需要用到删除操作, 而堆本身是不支持删除操作的.

我们可以使用一个小技巧 : 把一个堆 一分为二, 一个是 排序堆, 一个是 删除堆.

当我们需要删除某个值的时候, 只需要把这个值 加入删除堆里,

在取最大值时, 若 排序堆 和 删除堆 的 堆顶元素相同, 就意味着这个元素已经被删除了, 把它们双双弹出.

这样, 我们就得到了一个时间复杂度为 \(O(n \log^2 n)\) 的算法, 并且由于堆 (优先队列) 的使用, 会有比较大的常数.

作者的代码在洛谷上会 \(TLE\) 一个点, 在 bzoj 上可以通过. 并且代码比较丑陋, 不建议看代码理解. 实在想看代码的话可以去看洛谷的第一篇题解.

代码

#include<bits/stdc++.h>
using namespace std;
const int _=1e5+7;
const int __=2e5+7;
const int L=20;
const int inf=0x3f3f3f3f;
struct hep{
  priority_queue<int> A,B;
  void push(int x){ A.push(x); }
  void del(int x){ B.push(x); }
  int top(){
    while(!A.empty()&&!B.empty()&&A.top()==B.top()){ A.pop(); B.pop(); }
    return A.empty() ?-1 :A.top();
  }
  int sec(){
    int x=top();
    if(x==-1) return x;
    A.pop();
    int y=top();
    A.push(x);
    return y;
  }
}h1[_],h2[_],ans;
int n,m,dep[_],f[_][L+7],dis[_],sz[_],rt,minx=inf,ft[_],lt[_];
int lst[_],nxt[__],to[__],tot;
bool vis[_],sta[_];
int gi(){
  char c=getchar(); int x=0,f=1;
  while(c<'0'||c>'9'){ f=c=='-'?-1:1; c=getchar(); }
  while(c>='0'&&c<='9'){ x=(x<<3)+(x<<1)+c-'0'; c=getchar(); }
  return x*f;
}
void add(int x,int y){ nxt[++tot]=lst[x]; to[tot]=y; lst[x]=tot; }
int Lca(int x,int y){
  if(dep[x]<dep[y]) swap(x,y);
  for(int i=L;i>=0;i--)
    if(dep[f[x][i]]>=dep[y])
      x=f[x][i];
  if(x==y) return x;
  for(int i=L;i>=0;i--)
    if(f[x][i]!=f[y][i]){
      x=f[x][i];
      y=f[y][i];
    }
  return f[x][0];
}
int dist(int x,int y){ return dep[x]+dep[y]-2*dep[Lca(x,y)]; }
void idk(int u,int fa){
  dep[u]=dep[fa]+1;
  f[u][0]=fa;
  for(int i=1;i<=L;i++)
    f[u][i]=f[f[u][i-1]][i-1];
  for(int i=lst[u];i;i=nxt[i]){
    int v=to[i];
    if(v==fa) continue;
    idk(v,u);
  }
}
void pre(int u,int fa,bool id){
  if(id) h1[rt].push(dis[u]+1);
  sz[u]=1; dis[u]=dis[fa]+1;
  for(int i=lst[u];i;i=nxt[i]){
    int v=to[i];
    if(v==fa||vis[v]) continue;
    pre(v,u,id);
    sz[u]+=sz[v];
  }
}
void g_rt(int u,int fa,int sum){
  int maxn=sum-sz[u];
  for(int i=lst[u];i;i=nxt[i]){
    int v=to[i];
    if(v==fa||vis[v]) continue;
    g_rt(v,u,sum);
    maxn=max(maxn,sz[v]);
  }
  if(maxn<minx){ minx=maxn; rt=u; }
}
void upd(int u,int id){
  int t1=h2[u].top(),t2=h2[u].sec();
  if(id){
    if(t1==-1);
    else if(t2==-1) ans.push(0);
    else ans.push(t1+t2);
  }
  else{
    if(t1==-1);
    else if(t2==-1) ans.del(0);
    else ans.del(t1+t2);
  }
}
void init(int u,int lrt){
  minx=inf;
  pre(u,0,0);
  g_rt(u,0,sz[u]);
  //printf("u: %d rt: %d dis[rt]: %d\n",u,rt,dis[rt]);
  dis[rt]= lrt ?dis[rt] :-1;
  ft[rt]=lrt;
  lt[rt]=dis[rt]+1;    // 因为此时的 dis 是到 lrt 的子节点的距离, 所以要加上 1
  pre(rt,0,1);
  h2[lrt].push(h1[rt].top());
  vis[rt]=1; u=rt;
  for(int i=lst[u];i;i=nxt[i]){
    int v=to[i];
    if(vis[v]) continue;
    init(v,u);
  }
  vis[u]=0;
  h2[u].push(0);
  upd(u,1);
}
void modify(int u){
  sta[u]^=1;
  int len=lt[u],fa=ft[u],t=u;
  upd(u,0);
  //printf("%d %d\n",h2[u].top(),h2[u].sec());
  if(sta[u]){ h2[u].del(0); }
  else h2[u].push(0);
  upd(u,1);
  while(fa){
    int top=h1[u].top(),sec=h2[fa].sec();
    bool f1= len>=top,f2= len>=sec;
    if(f1){
      if(f2) upd(fa,0);
      h2[fa].del(top);
      if(sta[t]){ h1[u].del(len); h2[fa].push(h1[u].top()); }
      else{ h1[u].push(len); h2[fa].push(len); }
      if(f2) upd(fa,1);
    }
    else{
      if(sta[t]) h1[u].del(len);
      else h1[u].push(len);
    }
    u=ft[u]; fa=ft[u];
    len=dist(t,fa);
  }
}
char ss[_];
void run(){
  m=gi(); char c; int x;
  for(int i=1;i<=m;i++){
    //gets(ss);
    c=getchar();
    //printf("%s %c\n",ss,c);
    if(c=='G') printf("%d\n",ans.top());
    else{
      x=gi();
      //printf("x: %d\n",x);
      modify(x);
      //puts("!!!");
    }
  }
}
int main(){
  //freopen("x.in","r",stdin);
  //freopen("x.out","w",stdout);
  n=gi(); int x,y;
  for(int i=1;i<n;i++){
    x=gi(); y=gi();
    //printf("%d %d\n",x,y);
    add(x,y);
    add(y,x);
  }
  dis[0]=-1;
  idk(1,0);
  init(1,0);
  run();
  return 0;
}

原文地址:https://www.cnblogs.com/brucew-07/p/12121940.html

时间: 2024-07-29 14:44:37

[ZJOI2007]捉迷藏 解题报告 (动态点分治)的相关文章

[ZJOI2015]幻想乡战略游戏 解题报告 (动态点分治)

[ZJOI2015]幻想乡战略游戏 题意 有一棵大小为 \(n\) 的带权树, 每个点有一个权值, 权值可以修改 \(q\) 次, 找出一个补给点 \(x\) , 使得 \(\sum_{u \in V} val[u] \times dis(x,u)\) 最小, 并求出这个最小值. 一句话 : 求带权重心 (zsy说的) 附加条件 : 树中所有点的度数不超过 \(20\). 思路 一道你以为复杂度过不了, 但其实是过得了的题. 首先, 我们假定先选了一个点 \(u\) 作为补给点, 我们可以算出它

【BZOJ1095】[ZJOI2007]Hide 捉迷藏 动态树分治+堆

[BZOJ1095][ZJOI2007]Hide 捉迷藏 Description 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩捉迷藏游戏.他们的家很大且构造很奇特,由N个屋子和N-1条双向走廊组成,这N-1条走廊的分布使得任意两个屋子都互相可达.游戏是这样进行的,孩子们负责躲藏,Jiajia负责找,而Wind负责操纵这N个屋子的灯.在起初的时候,所有的灯都没有被打开.每一次,孩子们只会躲藏在没有开灯的房间中,但是为了增加刺激

[bzoj1095][ZJOI2007]Hide 捉迷藏 点分树,动态点分治

[bzoj1095][ZJOI2007]Hide 捉迷藏 2015年4月20日7,8876 Description 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩捉迷藏游戏.他们的家很大且构造很奇特,由N个屋子和N-1条双向走廊组成,这N-1条走廊的分布使得任意两个屋子都互相可达.游戏是这样进行的,孩子们负责躲藏,Jiajia负责找,而Wind负责操纵这N个屋子的灯.在起初的时候,所有的灯都没有被打开.每一次,孩子们只会躲藏在

【BZOJ1095】【ZJOI2007】捉迷藏 [动态点分治]

捉迷藏 Time Limit: 40 Sec  Memory Limit: 256 MB[Submit][Status][Discuss] Description 捉迷藏 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩捉迷藏游戏. 他们的家很大且构造很奇特,由N个屋子和N-1条双向走廊组成,这N-1条走廊的分布使得任意两个屋子都互相可达. 游戏是这样进行的,孩子们负责躲藏,Jiajia负责找,而Wind负责操纵这N个屋子的灯. 在起初

[ZJOI2007]捉迷藏(动态点分治)

题目描述 Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子.某天,Jiajia.Wind和孩子们决定在家里玩捉迷藏游戏.他们的家很大且构造很奇特,由N个屋子和N-1条双向走廊组成,这N-1条走廊的分布使得任意两个屋子都互相可达. 游戏是这样进行的,孩子们负责躲藏,Jiajia负责找,而Wind负责操纵这N个屋子的灯.在起初的时候,所有的灯都没有被打开.每一次,孩子们只会躲藏在没有开灯的房间中,但是为了增加刺激性,孩子们会要求打开某个房间的电灯或者关闭某个房间的电灯.为了评估某一次游戏的

bzoj1095: [ZJOI2007]Hide 捉迷藏 动态点分治学习

好迷啊...感觉动态点分治就是个玄学,蜜汁把树的深度缩到logn (静态)点分治大概是递归的时候分类讨论: 1.答案经过当前点,暴力(雾)算 2.答案不经过当前点,继续递归 由于原树可以长的奇形怪状(菊花啊..链啊..扫把啊..)这就导致各种方法都会被卡 于是通过每次找重心保证最大深度 动态怎么解决呢? 不妨考虑线段树是二分的固态版本(只可意会),那么我们把每次找到的重心固定下来长成一棵树就可以把点分治凝固(不可言传) 原来点分治该维护什么现在就维护什么... (事实上我并没有写过静态点分治..

BZOJ 1095 ZJOI2007 Hide 捉迷藏 动态树分治+堆

题目大意:给定一棵树,一开始每个点都是黑点,多次改变某个点的状态或询问距离最远的两个黑点的距离 <珍爱生命远离STL可是我还是可耻地用了STL系列> 传说中的动态树分治...其实并没有那么神嘛= = ↑别听这傻瓜瞎说这货被STL卡了一天QAQ 我们把分治过程中遍历过的重心都连起来 上一层的重心链接下一层的重心 可以得到一棵新的树 下面我们开始讨论这棵新树 显然这棵树的高度不会超过O(logn) 然后我们每个节点开两个堆 第一个堆记录子树中所有节点到父亲节点的距离 第二个堆记录所有子节点的堆顶

bzoj 1095 [ZJOI2007]Hide 捉迷藏 动态点分治+堆

题面 题目传送门 解法 挺恶心的题 考虑动态点分治,先建出点分树 然后每一个点开两个堆,分别为\(a,b\) \(a_i\)表示点分树上\(i\)子树中所有节点在原树上和点分树中\(i\)父亲的距离,\(b_i\)表示点分树中\(i\)所有儿子的堆顶 再开一个堆\(ans\),存每一个\(b_i\)最大和次大值的和 在修改的时候,分两种情况考虑 但是本质都是一样的,就是在点分树上不断爬,爬到根为止,然后对当前点和父亲的\(a,b\)进行删除和加入 细节比较多,需要注意 重点:传入一个结构体参数的

动态点分治入门 ZJOI2007 捉迷藏

传送门 这道题好神奇啊--如果要是不带修改的话那就是普通的点分治了,每次维护子树中距离次大值和最大值去更新. 不过这题要修改,而且还改500000次,总不能每改一次都点分治一次吧. 所以我们来认识一个新东西:带修改的点分治,动态点分治! 它可以强势解决带修改点分治问题(但是这玩意真的太难了我这个菜鸡只能学到入门) 首先我们要建立一棵树(点分树),这棵树是由点分治每次所分治的所有子树的重心串起来的.为什么要这么做呢?因为对于每次的修改,其实并没有影响到特别多的结果,它只会影响它自己所在的子树的重心