【线段树】【括号序列】【ZJOI2007】捉迷藏 Hide

题目描述

【恶意玩梗】JiajiaWind是一对恩爱的夫妻,并且他们有很多孩子。某天,JiajiaWind和孩子们决定在家里玩捉迷藏游戏。他们的家很大且构造很奇特,由N个屋子和N-1条双向走廊组成,这N-1条走廊的分布使得任意两个屋子都互相可达。
游戏是这样进行的,孩子们负责躲藏,Jiajia负责找,而Wind负责操纵这N个屋子的灯。在起初的时候,所有的灯都没有被打开。每一次,孩子们只会躲藏在没有开灯的房间中,但是为了增加刺激性,孩子们会要求打开某个房间的电灯或者关闭某个房间的电灯。为了评估某一次游戏的复杂性,Jiajia希望知道可能的最远的两个孩子的距离(即最远的两个关灯房间的距离)。
我们将以如下形式定义每一种操作:
C(hange) i
改变第i个房间的照明状态,若原来打开,则关闭;若原来关闭,则打开。
G(ame)
开始一次游戏,查询最远的两个关灯房间的距离。

输入

第一行包含一个整数 N ,表示房间的个数,房间将被编号为1,2,3N的整数。接下来 N1 行每行两个整数 a b,表示房间 a 与房间b之间有一条走廊相连。接下来一行包含一个整数 Q ,表示操作次数。接着Q行,每行一个操作,如上文所示。

输出

对于每一个操作Game,输出一个非负整数,表示最远的两个关灯房间的距离。若只有一个房间是关着灯的,输出0;若所有房间的灯都开着,输出-1。

样例输入

8
1 2
2 3
3 4
3 5
3 6
6 7
6 8
7
G
C 1
G
C 2
G
C 1
G

样例输出

4
3
3
4


网上其他题解主要分两种,一种是点分治【这里没有】,另一种就是括号序列+线段树。其实还看到了一个树链剖分的。
这题的括号序列做法先出于CQX冬令营讲稿《数据结构的提炼与压缩》,详见岛娘博客:某岛,这里做一下解释。
用括号表示,[其实就相当于沿着边向上走一步,]其实就相当于沿着边向下走一步。[]就是这个点入了一次又出了一次。
对于定义的 S(a,b) ,以及 dis(S),right_plus,right_minus,left_plus,left_minus 的转移

dis(S)=max{dis(S1),left_minus(S2)+right_plus(S1),left_plus(S2)+right_minus(S1),dis(S2)}

dis(S1) dis(S2) 不用多说,中间那两个则是跨过连接点去掉匹配的括号后新的括号序列的 dis ,取max即可得到这个合并了 S1 S2 后所得到 dis(S) 的最优值。

right_plus(S)right_minus(S)left_plus(S)left_minus(S)=max{right_plus(S1)c+d,right_minus(S1)+c+d,right_plus(S2)}=max{right_minus(S1)+cd,right_minus(S2)}=max{left_plus(S2)b+a,left_minus(S2)+b+a,left_plus(S1)}=max{left_minus(S2)+ba,left_minus(S1)}

对于 right_plus ,分别为 b>c b<c ,和只要 S2 的情况。
而对于 right_minus ,分别为 c>b ,和只要 S2 的情况。
对于 left 同理。【自己太笨了当初看这里看了好长时间】
细节处理和岛娘相同即可。
个人认为这样的思路算不上线段树吧大概,写的是归并貌似好一点。但是这个还是搞得像非递归线段树一样。
上代码

#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
#define lch tree[a*2+1]
#define rch tree[a*2+2]
const int MIN=-(1<<25);
vector<int> graph[100000];
//房间状态,0为灭,1为亮
bool state[100000]={0};
//n:灭灯房间数 t:临时变量
//pos:房间对应树中位置
//tree第二维:a, b, left_plus, right_plus, left_minus, right_minus, dis
int n, t, pos[100000]={0}, tree[1<<21][7]={0};
//叶子结点赋初值
void v(int a, int p){
    tree[a][0]=tree[a][1]=0;
    tree[a][2]=tree[a][3]=tree[a][4]=tree[a][5]=tree[a][6]=MIN;
    //这里的赋值是这样的,对于每个房间,共三个叶子节点,相当于[A]
    //-1就是[,-2就是],房间节点则a=b=0
    //这里也看了好半天
    if(p==-1)
        tree[a][1]=1;
    else if(p==-2)
        tree[a][0]=1;
    else if(!state[p])
        tree[a][2]=tree[a][3]=tree[a][4]=tree[a][5]=0;
}
//深搜建图
void dfs(int a, int fa){
    v(t++, -1);
    v(pos[a]=t++, a);
    for(vector<int>::iterator it=graph[a].begin();it!=graph[a].end();it++)
        if(*it!=fa)
            dfs(*it, a);
    v(t++, -2);
}
//合并两个子节点,套用公式
int merge(int a){
    if(lch[1]<rch[0]){
        tree[a][0]=lch[0]-lch[1]+rch[0];
        tree[a][1]=rch[1];
    }
    else{
        tree[a][0]=lch[0];
        tree[a][1]=lch[1]-rch[0]+rch[1];
    }
    tree[a][2]=max(lch[2], max(rch[2]-lch[1]+lch[0], rch[4]+lch[1]+lch[0]));
    tree[a][3]=max(lch[3]-rch[0]+rch[1], max(lch[5]+rch[0]+rch[1], rch[3]));
    tree[a][4]=max(rch[4]+lch[1]-lch[0], lch[4]);
    tree[a][5]=max(rch[5], lch[5]+rch[0]-rch[1]);
    tree[a][6]=max(lch[6], max(rch[4]+lch[3], max(rch[2]+lch[5], rch[6])));
}
//自底向上修改
void M(int a){
    while(a!=0){
        merge(a);
        a=(a-1)/2;
    }
    merge(0);
}
int main(){
    int p=1;
    scanf("%d", &n);
    //子节点至少从3n开始,同时保证上方(整个图)是一棵完全二叉树
    while(p<3*n)
        p<<=1;
    t=p-1;
    int a, b;
    for(int i=0;i<n-1;i++){
        scanf("%d%d", &a, &b);
        a--; b--;
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
    dfs(0, -1);
    //合并节点建树
    for(int i=p-2;i>=0;i--)
        merge(i);
    int q;
    char c;
    scanf("%d", &q);
    for(int i=0;i<q;i++){
        getchar();
        c=getchar();
        //Change
        if(c=='C'){
            scanf("%d", &a);
            a--;
            state[a]=!state[a];
            if(state[a]) n--;
            else n++;
            //修改树
            v(pos[a], a);
            M((pos[a]-1)/2);
        }
        //Game
        else{
            if(!n) printf("-1\n");
            else if(n==1) printf("0\n");
            else printf("%d\n", tree[0][6]);
        }
    }
    return 0;
}

还是得用好归并思想。点分治什么的以后再说。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值