1095 [ZJOI2007]Hide 捉迷藏

1095: [ZJOI2007]Hide 捉迷藏

Time Limit: 40 Sec   Memory Limit: 256 MB
Submit: 4054   Solved: 1709
[ Submit][ Status][ Discuss]

Description

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

Input

  第一行包含一个整数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

对于100%的数据, N ≤100000, M ≤500000。

Source

[ Submit][ Status][ Discuss]


HOME Back

标解: 动态树分治 ||  线段树+括号序列

动态树分治写法:

  这道题真正刷新了我对点分治的理解。在叙述这道题的做法前,请允许我先对点分治做一点小小的总结。       

       往往我们会遇到一些树上路径、树上点对的问题,如果我们暴力把这些信息全扫出来需要O(n^2)的时间复杂度,这肯定是不行的。我们必须考虑优化

       我们考虑树上的一条路径。我们发现,这条路径可以由路径上的任何一个点来贡献这条路径的答案。我们可以在树上寻找一些点,统计所有经过这个点的路径,并做到不重复、全覆盖。那我们该怎么做呢?这就用到了点分治。

       如果要将一棵无根树转化成有根树,我们就会用到点分治——寻找重心。每个分治点都管辖着一片区域。而点分治的精髓就在于,每次点分后最大子树的点数一定会小于n/2。所以递归层数一定是logn层。

       这样我们就可以进行采集信息了。点分治的流程大概如下:

       1.寻找当前无根树的重心

       2.由重心进行Dfs,根据题意采集信息。

       3.进入重心的其他子树中,进行同样的操作。

       这样下来,时间复杂度是O(nlogn),可以接受。这就是点分治的实质。

       那我们现在来考虑这道题。因为这棵树上点的信息是在不断修改的,所以我们需要用到动态的点分治。

       考虑到点分治的递归层数只有logn层,也就是 如果我们把分治点建立一棵树的话,树高不会超过logn。这样我们就可以把信息储存在分治树上,进行修改。

       首先我们去考虑原树中,

       对于每个节点A,我们需要开两个堆:

       First_Heap: 记录A点所管辖区域中所有点到A点的分治树上的父亲的距离。

       Second_Heap: 记录A点所有直接儿子(将A变为根后的原树中A的所有直接)的First_Heap。

       我们还要开一个全局的堆记录答案,记录每个Second_Heap的第一大和第二大的加和。

       有点难理解对吧?先别急,继续往下看。

       设当前点为A点。如果答案是经过A点的一条路径,那么路径的起点一定在A的一棵子树中,终点一定在A的另一棵子树中。那么当我们分治到这个点的时候,对于每棵子树B(B是A的直接儿子),都计算B子树中的所有点到B的距离,扔到一个堆Heap_B里。A开一个堆First_Heap_A,记录所有儿子B的Heap_B的堆顶。然后将A堆的第一大和第二大的加和扔到Ans_Heap中。

       那么我们在想,那个Heap_B应该给谁储存呢?因为在修改的时候肯定会用到它。答案储存到该子树中分治重心上,也就是变成这个重心的First_Heap。

       现在基本上能明白如何初始化了吧?初始化的效率是O(nlogn*logn)


       接着我们来看修改,下面是新建出来的点分树。


       我们发现,如果一个点进行了修改,那么只会影响到它到根上这一条链上的点的堆。那么我们就可以往上爬,进行修改,这样修改的效率是O(Qlogn*logn)。

修改的时候细节还是蛮多的,不过如果能动动脑筋,代码还是能很简洁的。

       这就是动态树分治了,核心就是建立一个树高为logn的点分树,然后在树上进行logn的爬链,来修改信息。

       还有什么不懂的就看看代码吧!

BZOJ传送门: 点击打开链接

#include<cmath>
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define o 100011
using namespace std; 
struct node{
	 priority_queue<int>heap,del;
	 void heap_Push(int x);
	 void del_Push(int x);
	 void Pop();
	 int Top();
	 int Sec();
	 int Size();
}H1[o],H2[o],ANS;
int head[o],nxt[o*2],point[o*2],deep[o],size[o],father[o];
int in[o],rem[o*3],st[o*3][25];
char s[20];
bool vis[o],mark[o];
int n,Q,ui,vi,hehe,tot,cnt,zoom,sum,record,root,light;
void node::heap_Push(int x){heap.push(x);}
void node::del_Push(int x){del.push(x);}
int node::Top(){
	while(del.size()&&heap.top()==del.top())
	heap.pop(),del.pop();
	return heap.top();
}
void node::Pop(){
	while(del.size()&&heap.top()==del.top())
	heap.pop(),del.pop();
	heap.pop();
}
int node::Sec(){
	int rem=Top(),x; Pop();
	x=Top(); heap_Push(rem);
	return x;
}
int node::Size(){
	return heap.size()-del.size();
}
void insert(node &X){
	if(X.Size()>=2) ANS.heap_Push(X.Top()+X.Sec());
}
void Erase(node &X){
	if(X.Size()>=2) ANS.del_Push(X.Top()+X.Sec());
}
void getroot(int now,int dad){
	int maxson=0;
	size[now]=1;
	for(int tmp=head[now];tmp;tmp=nxt[tmp]){
		int v=point[tmp];
		if(!vis[v]&&v!=dad){
			getroot(v,now);
			size[now]+=size[v];
			maxson=max(maxson,size[v]);
		}
	}
	maxson=max(maxson,sum-size[now]);
	if(maxson<record) record=maxson,root=now;
}
void Dfs(int now,int dad,node &S){
	sum++;
	S.heap_Push(deep[now]);
	for(int tmp=head[now];tmp;tmp=nxt[tmp]){
		int v=point[tmp];
		if(!vis[v]&&v!=dad){
			deep[v]=deep[now]+1;
			Dfs(v,now,S);
		}
	}
}
void Build_New_Tree(int now){
	vis[now]=true;
	H2[now].heap_Push(0);
	for(int tmp=head[now];tmp;tmp=nxt[tmp]){
		int v=point[tmp];
		if(!vis[v]){
			deep[v]=1; node S;
			sum=0;Dfs(v,0,S);
			record=1e8; getroot(v,0);
			father[root]=now;
			H1[root]=S;
			if(H1[root].Size())H2[now].heap_Push(H1[root].Top());
			Build_New_Tree(root);
		}
	}
	insert(H2[now]);
}
void addedge(int x,int y){
	tot++;nxt[tot]=head[x];head[x]=tot;point[tot]=y;
	tot++;nxt[tot]=head[y];head[y]=tot;point[tot]=x;
}
void dfs(int now,int fa){
	in[now]=++cnt;
	rem[cnt]=now;
	for(int tmp=head[now];tmp;tmp=nxt[tmp]){
		int v=point[tmp];
		if(v!=fa){
			deep[v]=deep[now]+1;
			dfs(v,now);
			rem[++cnt]=now;
		}
	}
} 
void INIT(){
	for(int i=1;i<=cnt;i++) st[i][0]=rem[i];
	for(int j=1;(1<<j)<=cnt;j++)
		for(int i=1;i+(1<<j)-1<=cnt;i++)
			st[i][j]= deep[st[i][j-1]]<deep[st[i+(1<<(j-1))][j-1]]? st[i][j-1]:st[i+(1<<(j-1))][j-1];
}
int LCA(int x,int y){
	if(x==y)return x;
	int l=in[x],r=in[y];
	if(l>r)swap(l,r);
	int k=log2(r-l+1);
	return deep[st[l][k]]<deep[st[r-(1<<k)+1][k]]? st[l][k]:st[r-(1<<k)+1][k];
}
int DIS(int x,int y){
	return deep[x]+deep[y]-deep[LCA(x,y)]*2;
}
void solve(int now,bool A){
	Erase(H2[now]);
	if(A) H2[now].del_Push(0);
	else H2[now].heap_Push(0);
	insert(H2[now]);
	for(int i=now;father[i];i=father[i]){
		int dis=DIS(now,father[i]);
		Erase(H2[father[i]]);
		if(H1[i].Size()) H2[father[i]].del_Push(H1[i].Top());
		if(A) H1[i].del_Push(dis);
		else H1[i].heap_Push(dis);
		if(H1[i].Size()) H2[father[i]].heap_Push(H1[i].Top());
		insert(H2[father[i]]);		
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<n;i++){
		scanf("%d%d",&ui,&vi);
		addedge(ui,vi);
	}
	hehe=n;
	record=1e8;sum=n;
	getroot(1,0);
	Build_New_Tree(root);
	deep[1]=0;
	dfs(1,0);
	INIT();
	scanf("%d",&Q);
	for(int i=1;i<=Q;i++){
		scanf("%s",s);
		if(s[0]=='C'){
			scanf("%d",&light);
			if(mark[light]) hehe++;
			else hehe--;
			solve(light,mark[light]^=1);
		}
		else{
			if(hehe<=1)printf("%d\n",hehe-1);
			else printf("%d\n",ANS.Top());
		}
	}
	return 0;
}

关于线段树+括号序列的做法Leaves还没学,过几天再更。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值