1095 [ZJOI2007]Hide 捉迷藏

来源:互联网 发布:算法求最小公倍数 编辑:程序博客网 时间:2024/05/22 01:39

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 100011using 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还没学,过几天再更。

原创粉丝点击