BSOJ2381:捉迷藏 括号序列 线段树维护

解释有部分来自某论文,代码改自hzwer

2381 -- 【ZJOI2007】捉迷藏
Description
  Jiajia和Wind是一对恩爱的夫妻,并且他们有很多孩子。某天,Jiajia、Wind和孩子们决定在家里玩捉迷藏游戏。他们的家很大且构造很奇特,由N个屋子和N-1条双向走廊组成,这N-1条走廊的分布使得任意两个屋子都互相可达。
  游戏是这样进行的,孩子们负责躲藏,Jiajia负责找,而Wind负责操纵这N个屋子的灯。在起初的时候,所有的灯都没有被打开。每一次,孩子们只会躲藏在没有开灯的房间中,但是为了增加刺激性,孩子们会要求打开某个房间的电灯或者关闭某个房间的电灯。为了评估某一次游戏的复杂性,Jiajia希望知道可能的最远的两个孩子的距离(即最远的两个关灯房间的距离)。
  我们将以如下形式定义每一种操作:
  
Input
  输入文件hide.in第一行包含一个整数N,表示房间的个数,房间将被编号为1,2,3…N的整数。接下来N-1行每行两个整数a, b,表示房间a与房间b之间有一条走廊相连。接下来一行包含一个整数Q,表示操作次数。接着Q行,每行一个操作,如上文所示。
Output
  对于每一个操作Game,输出一个非负整数到hide.out,表示最远的两个关灯房间的距离。若只有一个房间是关着灯的,输出0;若所有房间的灯都开着,输出-1。
Sample Input
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
Sample Output
4
3
3
4
Hint
【数据规模】
  对于20%的数据,N≤50, Q≤100;
  对于60%的数据,N≤3000, Q≤10000;
对于100%的数据,N≤100000, Q≤500000。
 

这道题只有这么难了,要我自己做根本想不起来。
 标准的树链剖分基本没法做,我们要用到
括号序列
来解决这个问题。
首先,括号序列
图片

如图的一棵树,它的括号序列就是   (1(2(4)2(5)2)1(3(6)3(7)3)1) 
为什么用括号序列呢?  
// 为了方便书写,括号都写成中括号
假设我们先序遍历得到如右的括号序列:
[A[B[E][F[H][I]]][C][D[G]]]
去掉字母,就得到了: [[[][[][]]][][[]]]
称为这棵树的括号编码。
现在,我想询问E->G取出介于他们之间的括号编码: 
][[][]]][][[
去掉可以匹配的括号对 剩下 
]][[  

为什么去掉可匹配的括号对?因为我们记录的是点到点的距离,然而可匹配的括号是DFS的路径,比如,点到点的距离是一个倒写的V,然而DFS路径却在这个倒写的V中间又多走了一条路,然而这条路是纯粹多余的。 

我们看到 2 个 ] 和 2 个 [,也就是说,在树中,从 E 向上爬 2 步,再向下走 2 步就到了 G。
所以现在我们把介于两点之间的括号编码命名为S(a,b),表示有a个]和b个[
所以,对于两个点 U,V,如果介于这两点之间编码 S 可表示为 (a, b),UV之间的距离就是 a+b。  
也就是说,我们维护一个max{a+b|S'(a,b)是S的子串,且S'介于两个黑点之间}
S是全树的括号编码,现在,我们又记录dis(s),表示我们维护的max,这个dis可以从左右儿子中搜集,线段树的做法已经显而易见。

考虑对于两段括号编码 S1(a1, b1) 和 S2(a2, b2),如果它们连接起来形成 S(a, b)。
注意到 S1、S2 相连时又形成了成对的括号,合并后它们会被抵消掉。

所以:

当 a2 < b1 时第一段 [ 就被消完了,两段 ] 连在一起,例如:] ] [ [ + ] ] ] [ [ = ] ] ] [ [当 a2 >= b1 时第二段 ] 就被消完了,两段 [ 连在一起,例如:] ] [ [ [ + ] ] [ [ = ] ] [ [ [  (?..反了?。。。

这样,就得到了一个十分有用的结论:

当 a2 < b1 时,(a,b) = (a1, b2+b1-a2),
当 a2 >= b1 时,(a,b) = (a1+a2-b1, b2)。

由此,又得到几个简单的推论:

(i) a+b = a1+b2+|a2-b1| = max{(a1-b1)+(a2+b2), (a1+b1)+(b2-a2)}
(ii) a-b = a1-b1+a2-b2
(iii) b-a = b2-a2+b1-a1

由 (i) 式,可以发现,要维护 dis(s),就必须对子串维护以下四个量:

right_plus:max{a+b | S’(a,b) 是 S 的一个后缀,且 S’ 紧接在一个黑点之后}
right_minus:max{a-b | S’(a,b) 是 S 的一个后缀,且 S’ 紧接在一个黑点之后}
left_plus:max{a+b | S’(a,b) 是 S 的一个前缀,且有一个黑点紧接在 S 之后}
left_minus:max{b-a | S’(a,b) 是 S 的一个前缀,且有一个黑点紧接在 S 之后}

这样,对于 S = S1 + S2,其中 S1(a, b)、S2(c, d)、S(e, f),就有

(e, f) = b < c ? (a-b+c, d) : (a, b-c+d)
dis(S) = max{dis(S1), left_minus(S2)+right_plus(S1), left_plus(S2)+right_minus(S1), dis(S2)}

那么,增加这四个参数是否就够了呢?
是的,因为:

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

这样一来,就可以用线段树处理编码串了。实际实现的时候,在编码串中加进结点标号会更方便,对于底层结点,如果对应字符是一个括号或者一个白点,那 么right_plus、right_minus、left_plus、left_minus、dis 的值就都是 -inf;如果对应字符是一个黑点,那么 right_plus、right_minus、left_plus、left_minus 都是 0,dis 是 -inf。

下面贴上代码和注意事项。
#include<iostream>
#include<cstdio>
#include<cstring>
#define L(x) (x<<1)
#define R(x) (x<<1|1)
#define inf 0x3fffffff//不能用0x7fffffff,会炸!
using namespace std;
inline int getint()
{
int bj=1;
char ch=getchar();
    while(ch<'0'||ch>'9')
    {
    if(ch=='-')bj=-1;
    ch=getchar();
}
int ret=0;
while(ch>='0'&&ch<='9')ret=ret*10+ch-'0',ch=getchar();
return ret*bj;
}
char cmd[10];
int n,q,cnt=0,tim=0,black;
int h[1000005]={0},tid[1000005]={0},rank[1000005]={0};
bool bla[1000005]={0};
struct Edge
{
int to,next;
}w[1000005]={0};
struct Seg
{
int l,r,lminus,lplus,rminus,rplus,lk,rk,dis;
void init(int x)
{
dis=-inf;
lk=rk=0;
if(rank[x]==-10086)lk=1;//-10086--> [
if(rank[x]==-10087)rk=1;//-10087--> ]
if(rank[x]>0&&bla[rank[x]])//这是一个黑房间
lminus=lplus=rminus=rplus=0;
else lminus=lplus=rminus=rplus=-inf;//不是黑房间,不能用
}
}tree[1000005]={0};
inline int max(int a,int b,int c)
{
return max(max(a,b),c);
}
void add(int x,int y)
{
cnt++;w[cnt].to=y;w[cnt].next=h[x];h[x]=cnt;
}
void DFS(int x,int fa)
{
rank[++tim]=-10086;//一进入DFS,就打左括号
tid[x]=++tim;
rank[tim]=x;
for(int i=h[x];i;i=w[i].next)
{
int to=w[i].to;
if(to!=fa)
{
DFS(to,x);
}
}
rank[++tim]=-10087;//结束时,打右括号
}
inline Seg Pushup(Seg s1,Seg s2)
{
int a=s1.rk,b=s1.lk,c=s2.rk,d=s2.lk;
Seg s;
s.l=s1.l;s.r=s2.r;
s.dis=max(s1.dis,s2.dis);
s.dis=max(s.dis,s1.rplus+s2.lminus,s1.rminus+s2.lplus);
if(b<c)s.rk=a+c-b,s.lk=d;
else s.rk=a,s.lk=d+b-c;
s.rplus=max(s1.rplus+d-c,s1.rminus+c+d,s2.rplus);
s.rminus=max(s2.rminus,s1.rminus+c-d);
s.lplus=max(s1.lplus,s2.lplus+a-b,s2.lminus+a+b);
s.lminus=max(s1.lminus,s2.lminus+b-a);
return s;

}//这段不解释,看上面的讲解,这个Pushup写的有点神奇,写成void也可以,把root传进去就行了
void build(int l,int r,int root)
{
tree[root].l=l;tree[root].r=r;
if(l==r){tree[root].init(l);return;}
int mid=(l+r)>>1;
build(l,mid,L(root));
build(mid+1,r,R(root));
tree[root]=Pushup(tree[L(root)],tree[R(root)]);
}
void change(int x,int root)
{
    if(tree[root].l==tree[root].r){tree[root].init(tree[root].l);return;}//单点取反,在tree.init里修改
    int mid=(tree[root].l+tree[root].r)>>1;
    if(x<=mid)change(x,L(root));
    else change(x,R(root));
    tree[root]=Pushup(tree[L(root)],tree[R(root)]);
}
int main()
{
black=n=getint();//要记录黑房间个数,一开始全黑
for(int i=1;i<=n;i++)bla[i]=1;//房间是否黑暗
for(int i=2;i<=n;i++)
{
int x=getint(),y=getint();
add(x,y);
add(y,x);
}
DFS(1,0);//dfs部分去掉了dfs2和dep,son,大大简化了。
build(1,tim,1);//加上括号共有tim各点,全扔进去
q=getint();
while(q--)
{
scanf("%s",&cmd);
if(cmd[0]=='C')
{
int x=getint();
if(bla[x])black--;
else black++;
bla[x]^=1;
change(tid[x],1);
}
if(cmd[0]=='G')
{
if(black==0)puts("-1");
else if(black==1)puts("0");
else printf("%d\n",tree[1].dis);//所有信息都上传到了根节点,根节点包含了整个树的dis
}
}
return 0;
}


 讲解部分来源: NOI08 冬令营论文 《数据结构的提炼与压缩》
http://www.shuizilong.com/house/archives/bzoj-1095-zjoi2007hide-%E6%8D%89%E8%BF%B7%E8%97%8F/ ;

另外附上理解的时候用的草稿纸,能帮助到就太好了。图片
 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值