数据规模

从别的oj网址上找的
对于20%的数据, N ≤50, M≤100;
对于60%的数据, N ≤3000, M ≤10000;
对于100%的数据, N ≤100000, M≤500000。


解题思路

这题我做了两种解法,第一种思路不好想,但是很快,复杂度为O(nlogn),另一种比较好想(但实际上并不好写),但是复杂度略高,为O(n logn logn)


思路一:括号序列

用到的知识:线段树

括号序列的定义

括号序列是可以理解为把一颗树拍扁,变成一个序列,这样我们就可以用各种数据结构来维护这颗树了
具体的方法如下:
这是一颗树:
图片说明
我们从根节点开始遍历,每经过一个点,就加入一个"(",每当一个点的子树遍历完,就加入一个")",然后我们就得到了这颗树的括号序列:
(1(2)(3(4)(5)))

关于括号序列的结论

如果我们有了一颗树的括号序列,我们可以得到以下结论:
i,j之间的距离等于i与j之间的失配括号总数
什么意思?比如我们要找1,5之间的距离,我们列出1,5之间的括号:()(()(,其中有两组括号是匹配的,即有两组"("在")"前面,删去匹配的括号后,还剩下((,两个括号,所有1,5之间的距离为2
为什么有这个结论?因为匹配的括号表示那个点所有的子节点都跑完了,但是没有经过i,j(否则右括号就会在j后面),所以i,j间的距离就是i,j间的失配括号总数

如何产生一颗树的括号序列

按顺序遍历即可,代码实现如下:

void dfs(int u,int fa){
    s[++cnt]=-1;//先加入左括号
    s[++cnt]=u;//再加入该点
    for(int i=head[u];i;i=edge[i].next){
        int v=edge[i].v;
        if(v==fa)continue;
        dfs(v,u);
    }
    s[++cnt]=-2;//子树跑完了加入右括号 
}

括号序列怎么用

我们可以将树拍扁成一个序列,然后可以直接用数据结构来维护这颗树,比如说用线段树来维护,对于本题,我们使用线段树来维护区间内最大的两个关灯点的距离
对于一个区间,我们用a,b来表示这个区间的右,左失配括号数量(因为一个区间右括号一定在左括号前面,否则会匹配掉),用val来表示这个区间的失配括号数量之和,所以对于任意两个相邻的区间(不是指线段树的区间,而是任意区间)1,2,有:
val=a1+abs(b1-a2)+b2
a1,a2,b1,b2,表示两个区间的右左括号数
稍微变形一下:
val=va1+abs(b1-a2)+b2=max((a1+b1)+(b2-a2),(a1-b1)+(a2+b2))
即max(左区间的(a+b)+右区间的(b-a),左区间的(a-b)+右区间的(a+b))
接下来引入线段树来维护区间:
对于每一个线段树的区间,如何根据左右子区间来维护val?

我们定义l1,l2,r1,r2来表示最大前缀(a-b),最大前缀(a+b),最大后缀(a+b),最大后缀(b-a),这样val=max(max(lson.val,rson.val),max(lson.r1+rson.l1,lson.r2+rson.l2))
接下来是维护l1,l2,r1,r2:

    tr[i].l1=max(lson.l1,rson.l1-lson.a+lson.b);//取左边的max或者右边的max与左边整体 
    tr[i].l2=max(lson.l2,max(rson.l2-lson.b+lson.a,rson.l1+lson.b+lson.a));
    //注:rson.l1+lson.b==rson.b+(lson.b-rson.a)
    tr[i].r1=max(rson.r1,max(lson.r1-rson.a+rson.b,lson.r2+rson.a+rson.b));
    //注:lson.r2+rson.a==lson.a+(rson.a-lson.b) 
    tr[i].r2=max(rson.r2,lson.r2+rson.a-rson.b);//取右边的max或左边的max与右边整体 

比较难理解的是那两个a+b,自己看注释体会一下,还理解不了可以画画图

维护a,b:

if(lson.b>rson.a){
        tr[i].a=lson.a;
        tr[i].b=lson.b-rson.a+rson.b;//左区间的左括号与右区间的右括号抵消 
    }
    else{
        tr[i].a=rson.a-lson.b+lson.a;
        tr[i].b=rson.b;
    }

然后,对于所有线段树内的最底层的点如何定义呢?

如果这个点是左括号,那就b=1,右括号那就a=1
如果这个点是关灯点,那就l1,l2,r1,r2=0
否则l1,l2,r1,r2=-inf,表示所有的非关灯点(包括括号点和开灯点)全部都不参与答案的统计
初始的val为-inf,因为单个点的val没有意义,两个点构成的区间的val必须由l1,l2,r1,r2生成

至此,基本上的难点都已经讲完了,剩下的直接看完整代码就能看懂了

完整代码

//[ZJOI2007]捉迷藏
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
inline int Read(){
    int x=0;
    char c=getchar();
    while(c>'9'||c<'0')c=getchar();
    while(c>='0'&&c<='9')x=x*10+c-'0',c=getchar();
    return x;
} 
inline char Readc(){
    char c=getchar();
    while(c!='C'&&c!='G')c=getchar();
    return c;
} 
const int mx=100100;
const int inf=1<<29;
int n;
struct Edge{
    int v;
    int next;
}edge[mx<<1];
int top,head[mx];
void addedge(int u,int v){
    edge[++top].v=v;
    edge[top].next=head[u];
    head[u]=top;
}
void add(int u,int v){
    addedge(u,v);
    addedge(v,u);
} 
int s[mx*3];//树形成的括号序列,-1表示左括号,-2表示右括号,u表示点的编号 
int cnt;//总的点数 
int pos[mx];//每个点对应的线段树内的点的位置 
int on[mx];//on[u]:u点是否开灯 
void dfs(int u,int fa){
    s[++cnt]=-1;//先加入左括号
    s[++cnt]=u;//再加入该点
    pos[u]=cnt;//记录每个点在线段树内的位置 
    for(int i=head[u];i;i=edge[i].next){
        int v=edge[i].v;
        if(v==fa)continue;
        dfs(v,u);
    }
    s[++cnt]=-2;//子树跑完了加入右括号 
}
#define lson tr[i<<1]
#define rson tr[i<<1|1]
struct Node{
    int a,b,l1,l2,r1,r2,val;
    //设a1,a2,b1,b2分别表示左右区间(指任意区间,而非线段树的区间)的右,左括号
    //val=max(a1+abs(a2-b1)+b2)=max((a1+b1)+(b2-a2),(a1-b1)+(a2+b2)) 
    //a,b:每个区间的右左括号数
    //l1,l2:前缀max(b-a),max(a+b) 
    //r1,r2:后缀max(a+b),max(a-b)
    int l,r;//线段树区间左右端点 
}tr[(3*mx)<<2];
inline int max(int x,int y){return x>y?x:y;}
void update(int i){
    if(lson.b>rson.a){
        tr[i].a=lson.a;
        tr[i].b=lson.b-rson.a+rson.b;//左区间的左括号与右区间的右括号抵消 
    }
    else{
        tr[i].a=rson.a-lson.b+lson.a;
        tr[i].b=rson.b;
    }
    tr[i].l1=max(lson.l1,rson.l1-lson.a+lson.b);//取左边的max或者右边的max与左边整体 
    tr[i].l2=max(lson.l2,max(rson.l2-lson.b+lson.a,rson.l1+lson.b+lson.a));
    //注:rson.l1+lson.b==rson.b+(lson.b-rson.a)
    tr[i].r1=max(rson.r1,max(lson.r1-rson.a+rson.b,lson.r2+rson.a+rson.b));
    //注:lson.r2+rson.a==lson.a+(rson.a-lson.b) 
    tr[i].r2=max(rson.r2,lson.r2+rson.a-rson.b);//取右边的max或左边的max与右边整体 
    tr[i].val=max(max(lson.val,rson.val),max(lson.r1+rson.l1,lson.r2+rson.l2));
}
void build(int i,int l,int r){
    tr[i].l=l,tr[i].r=r;
    if(l==r){
        tr[i].a=tr[i].b=0;
        tr[i].l1=tr[i].l2=tr[i].r1=tr[i].r2=tr[i].val=-inf;
        if(s[l]==-1)tr[i].b=1;
        else if(s[l]==-2)tr[i].a=1;
        else if(on[s[l]]==0)tr[i].l1=tr[i].l2=tr[i].r1=tr[i].r2=0;//将关灯的点改为0 
        return;
    }
    int mid=(l+r)>>1;
    build(i<<1,l,mid);
    build(i<<1|1,mid+1,r);
    update(i);
}
void change(int i,int k){//k表示点对应的线段树的点的位置 
    if(tr[i].l==tr[i].r){
        tr[i].a=tr[i].b=0;
        tr[i].l1=tr[i].l2=tr[i].r1=tr[i].r2=on[s[k]]?-inf:0;//将开灯的点改为-inf 
        tr[i].val=-inf;
        return;
    }    
    int mid=(tr[i].l+tr[i].r)>>1;
    if(k<=mid)change(i<<1,k);
    else change(i<<1|1,k);
    update(i);
}
int main(){
//    freopen("in.txt","r",stdin);
    n=Read();
    for(int i=1;i<n;++i){
        int u=Read(),v=Read();
        add(u,v);
    }
    dfs(1,0);
    build(1,1,cnt);
    int num=n;//关灯的房间数
    int T=Read();
    while(T--){
        char c=Readc();
        if(c=='C'){
            int u=Read();
            num+=on[u]==1?1:-1;
            on[u]^=1;
            change(1,pos[u]);
        }
        else{
            if(num==0)printf("-1\n");
            else if(num==1)printf("0\n");
            else {
                printf("%d\n",tr[1].val);
            }
        }
    } 
    return 0;
}

总结

该方法的复杂度为O(nlogn),复杂度是十分优秀的,但是思维量巨大,尤其是val的变形以及l1,l2,r1,r2的维护,十分难想,如果是考试,多半想不出来或者没时间想
所以,我们有必要学习一下思路二,这是更一般的做法


思路二:点分树

用到的知识:点分树(会点分治就行)+堆

点分树是什么

前置知识:点分治
在点分治中我们找到了每个子树的重心,如果把每一层子树的重心当成上一层重心的儿子节点,我们就构成了一颗树:<stron>,所以,很显然,这颗树的点数为n,层数为logn
有了这样一颗点分树之后,有如下结论:
我们修改一个点时,受到影响的点有:这个点沿点分树一直到根的所有点
所以,每次只需要进行logn次修改就可以修改与这个点有关的所有路径</stron>

为什么想到点分树

对于树上的路径问题,点分治是一个非常常用,且非常高效的算法,而如果题目涉及修改操作,一般就要使用点分树

本题的解法

本题的要求是求最长的路径,我们可以对于每一个重心,维护它每个子节点(下层重心)对应区域的所有点到该点的距离,然后对 每个区域的最长距离 中第一长和第二长的统计答案。
这两个操作我们都可以用堆来实现,即:
对每个点开两个堆fdist,sdist,fdist装该点所在区域内所有点到该点父节点(上一层的重心)的距离,sdist装该点所有子节点(下一层的重心)所对应区域的最长距离(即下层重心的fdist的堆顶),然后将每个点的sdist的堆顶和第二堆顶(堆顶出堆后的堆顶)装入ans堆中
对于每次修改操作,我们需要将特定的距离删除或者入堆,入堆很容易,但是删除怎么实现?
于是,本题所需的数据结构变为:能找出最大元素且能删除特定元素
有两种思路:
1.用平衡树
2.用两个堆,一个为原来的堆,一个为删除堆,对于每次删除操作,我们只需将元素加入删除堆,然后对于每次找堆顶操作,我们需要判断两个堆的堆顶是否相等,如果相等,就同时出堆
我建议用第二种,常数小,且代码短。

完整代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
inline int Read(){
    int x=0;
    char c=getchar();
    while(c>'9'||c<'0')c=getchar();
    while(c>='0'&&c<='9')x=x*10+c-'0',c=getchar();
    return x;
} 
inline char Readc(){
    char c=getchar();
    while(c!='C'&&c!='G')c=getchar();
    return c;
} 
void write(int x){
    if(x>9)write(x/10);
    putchar(x%10+'0');
}
const int mx=100100;
int n,m;
struct Edge{
    int v;
    int next;
}edge[mx<<1];
int top,head[mx];
inline void addedge(int u,int v){
    edge[++top].v=v;
    edge[top].next=head[u];
    head[u]=top;
}
inline void add(int u,int v){
    addedge(u,v);
    addedge(v,u);
}
inline int max(int x,int y){return x>y?x:y;}
int cnt,c[mx];//关灯的总点数,每个点的状态 
int maxn[mx],size[mx],root,s;
int vis[mx];
int fa[mx];//每个点对应点分树上的父亲节点 
void find(int u,int f){//找重心 
    maxn[u]=0,size[u]=1;
    for(int i=head[u];i;i=edge[i].next){
        int v=edge[i].v;
        if(v==f||vis[v])continue;
        find(v,u);
        size[u]+=size[v];
        maxn[u]=max(maxn[u],size[v]); 
    }
    maxn[u]=max(maxn[u],s-size[u]);
    if(maxn[u]<maxn[root])root=u;
}
int dist[mx][20];//dist[u][deep]表示在deep深度的子树上u到重心的距离,这样定义可以避免每次bfs都memset整个数组 
struct Pr_q{
    priority_queue<int>q,d;//当前队列,已删除队列
    inline void push(int x){q.push(x);}//插入x 
    inline void del(int x){d.push(x);}//删除x 
    inline int size(){return q.size()-d.size();}
    inline int top(){
        while((!q.empty())&&(!d.empty())&&q.top()==d.top()){
            q.pop();
            d.pop();
        }
        if(q.empty())return -1e9;
        return q.top();
    }
    inline int two_top(){
        if(size()<2)return -1e9;
        int tp=top();
        q.pop();
        int tp2=top();
        q.push(tp);
        return tp+tp2;
    } 
}ans,sdist[mx],fdist[mx];//答案堆,所有子节点的最大距离,每个点所在的区域的点到父亲节点的距离
inline void bfs(int s,int deep,int f){//从s出发,在子块内生成每个点到u的距离 
    queue<int>q;
    dist[s][deep]=1;
    fdist[f].push(1);
    q.push(s);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=head[u];i;i=edge[i].next){
            int v=edge[i].v;
            if(vis[v])continue;
            if(dist[v][deep]==0){
                dist[v][deep]=dist[u][deep]+1;
                fdist[f].push(dist[v][deep]);
                q.push(v);
            }
        } 
    }
}
int dep[mx];//每个点的深度 
int l_ans[mx];//每个点上次操作后的答案 
void solve(int u,int deep){//u:本层的重心,deep:深度 
    vis[u]=1;
    sdist[u].push(0);//这个点与一条子路径构成路径
    for(int i=head[u];i;i=edge[i].next){
        int v=edge[i].v;
        if(vis[v])continue;
        maxn[root=0]=1<<30;
        s=size[v];
        find(v,0);
        fa[root]=u;
        dep[root]=dep[u]+1;
        bfs(v,deep,root);
        sdist[u].push(fdist[root].top());
        solve(root,deep+1); 
    }
    l_ans[u]=sdist[u].two_top();
    ans.push(l_ans[u]);
}
inline void change(int u){//改变u点的状态 
    c[u]^=1;
    if(c[u]==0)++cnt,sdist[u].push(0);
    else --cnt,sdist[u].del(0);
    int now=u;//从u一直更新到根 
    while(now){
        int now_ans=sdist[now].two_top();
        if(l_ans[now]!=now_ans){
            ans.del(l_ans[now]);
            ans.push(l_ans[now]=now_ans);//修改该点的答案
        }
        if(fa[now]==0)break;
        int x1=fdist[now].top();
        if(c[u]==0)fdist[now].push(dist[u][dep[now]-1]);
        else fdist[now].del(dist[u][dep[now]-1]); //更新fdist堆
        int x2=fdist[now].top();
        if(x1!=x2){
            sdist[fa[now]].del(x1);
            sdist[fa[now]].push(x2);//更新now的父亲的sdist堆
        }
        now=fa[now]; 
    }
}
int main(){
//    freopen("in.txt","r",stdin);
    n=Read();
    for(int i=1;i<n;++i){
        int u=Read(),v=Read();
        add(u,v);
    }
    s=n;
    maxn[0]=1<<30;
    find(1,0);
    dep[root]=0;
    solve(root,0);
    cnt=n;//关灯的房间数
    int T=Read();
    while(T){
        char c=Readc();
        if(c=='G'){
            if(cnt==0)printf("-1\n");
            else if(cnt==1)printf("0\n");
            else {
            write(ans.top());
            putchar('\n');    
            }
        }
        else{
            int x=Read();
            change(x);
        }
        --T;
    } 
    return 0;
}

总结

虽然代码长,复杂度高(复杂度为O(n logn logn))但是思路却比括号序列简单了不少,已经看懂括号序列的也建议学一下点分树,这种方法适用面更广。
另外,由于点分树巨大的常数(或许是我写的太差了),这道题虽然理论复杂度可以过,但实际提交却有一个点T了(别的oj网站上能过,别问,问就是牛客网评测姬太菜了)


码字不易,求赞