树链剖分学习笔记
(感谢大树的讲解~听了他的讲解以后感觉树链剖分没有想象中的那么难)
树链剖分的定义为,对树上的节点划分成若干个集合,使得每个集合内节点相互之间的连边是一条链。
注意,这里说的边指的是从父亲节点到儿子节点的有向边。例如,一个节点有两个儿子节点,则这三个节点组成的集合不符合上述定义(因为边的方向问题不能算作一条链)。
树链剖分的一种方法是树的轻重链剖分。假设这棵树为有根树。
对每个节点i,定义size[i]为以该节点为根的子树的节点数目。定义一个节点的重儿子为该节点的所有儿子节点中size最大的一个(如果有多个可任取其一)。该节点的其余儿子节点称为轻儿子。
我们以这样一种方式来进行树链剖分:
1.每个节点的后继为该节点的重儿子,则该后继唯一。将这项操作进行到底,则可得到一条链。
2.对于该节点的轻儿子,以轻儿子本身为根,进行树链剖分,递归进行剖分即可。
则我们可以将树剖分为若干条链。并且从每个点到根节点的路径中经过的链数不会超过(log n),这是因为从每个链的顶端i向上走的时候,这个顶端一定为其父亲节点的轻儿子(否则它的父亲才会是链的顶端,矛盾),所以剩余的子树至少有size[i]个点,初始时size[i]为一,每到顶端向上走一步当前走过的点的数量均最少倍增。因此经过的链数不会超过(log n)。
所以如果我们对于每个链维护一个线段树/数状数组/其他数据结构,则对于每次有关树上两点间路径的查询,均可在O(该数据结构的查询复杂度*log n)内解决。
假设我们需要维护的数据结构为线段树。事实上,我们不需要建立若干棵线段树,我们可以把每个节点映射标号为pos[i],使得每条链中所有节点的pos[i]是连续的,即将每个链连续地映射到一个线段树上。这样,我们就可以只建立一棵线段树,就可以对每条链上的查询计算结果。
然后在谈谈查询操作。我们需要维护这样几个数组
father[i](意义显然……)
top[i]表示点i所在的链中最高点(即下述deep最小的点)的编号
deep:我们可以先把每个链看成一个节点,在这个图中计算每条链的深度。假设点i是链j的一个节点,则deep[i]=j在新图中的深度
这一步类似与倍增LCA的思路。如果我们要查询的路径为(u,v),如果deep[u] deep[v]相等,我们跳过这一步。否则的话不妨设deep[u]>deep[v],我们先让点u不断向上”走“,每次走到father[top[u]],通过查询query(pos[u],pos[top[u]])记录这一段的值,并且合并每次计算的值。直到u和v的deep值相等。
u和v的deep相等时,它们不断同时往上走,走到fahter[top[u]],查询query(pos[u],pos[top[u]]),并且记录合并,直到它们的top节点相同。这时查询query(pos[u],pos[v])并进行最后一次合并即可。
树链剖分的理论部分到此结束。该算法的所有问题都找到了解决方案。
——————————————————————————————————————————————
实现的时候我们大概需要维护这样几个数组
pos top deep father hs(hs[i]表示这一节点的重儿子标号,这一数组也可不维护)
需要进行两次深搜
第一次计算father hs
第二次计算 pos top deep
pos的计算方法:第二次宽搜的时候,对于每个节点,先搜索重儿子,然后打时间戳,即可保证每条链映射到线段树或其他数据结构上都是连续的。
相关题目占坑。
——————————————————————————————————————————————
啊!啊!啊!
noi2015的Day1 T2就是树剖的水水水水水水水题!
线段树只有个区间赋值的操作!!
当时真是好sb啊。。。。如果当时听过大树的讲课多好啊QAQ
这题涉及两种操作:
1.区间赋值从根节点到某固定节点
2.区间赋值某棵子树
用线段树的话,操作一的实现很容易。操作二的实现需要考虑pos的生成方式,其本质为dfs序。只需预处理出每个点的子树的pos最大值,然后区间赋值一次即可。因为其子树的pos标号必定在父亲节点和最大值之间,并且其中不会混入其他子树。
代码很诡异。在本地测没有问题,在COGS和UOJ上测T到死……结果在Luogu莫名就A掉了。原因有待考证。
update20161110:死因:被卡常数
在此记录几个被卡常数以后的做法:
1.快速读入(貌似快了不少)
2.压内存,把没必要的内存清掉,比如线段树3*maxn改成2*maxn(快了不少)
3.函数前加inline(貌似也能快一些)
4.函数尽量少调用,减少调用次数(貌似没快多少……)
5.不要以为cout不慢。。换成printf分分钟AC。。。
AC代码。。QAQ求COGS管理员不要拉黑我,我就是交了十几次还重测了十几次。。
__
20161110再update:
抓大放小,刚才试了一下,貌似scanf和printf就能AC,不要作死用string就好。。
maya我都干了些什么。。而且100000本来n*logn^2就卡时还作死。。
#include<cstdio> #include<iostream> #include<cstring> #include<vector> #include<algorithm> using namespace std; struct SEG{ int left,right; int lchild,rchild; int cover; //区间赋值标记。未赋值定义为-1 void clear(int l=0,int r=0){ left=l;right=r; lchild=rchild=cover=-1; } }; template<class T> inline bool getd(T& x){ int ch=getchar(); bool neg=false; while(ch!=EOF && ch!='-' && !isdigit(ch)) ch=getchar(); if(ch==EOF) return false; if(ch=='-'){ neg=true; ch=getchar(); } x=ch-'0'; while(isdigit(ch=getchar())) x=x*10+ch-'0'; if(neg) x=-x; return true; } const int maxn=100050; const int INF=2147483647; int n,m,tot,t; SEG tree[2*maxn]; vector<int> G[maxn]; int father[maxn],pos[maxn],top[maxn],heavy[maxn],size1[maxn],low[maxn]; inline int length(int l1,int r1,int l2,int r2){ return max(min(r1,r2)-max(l1,l2)+1,0); } inline void dfs_first(int u) { size1[u]=1; int mx=-INF; for(int i=0;i<G[u].size();i++){ father[G[u][i]]=u; dfs_first(G[u][i]); size1[u]+=size1[G[u][i]]; mx=max(mx,size1[G[u][i]]); } for(int i=0;i<G[u].size();i++) if(mx==size1[G[u][i]]) heavy[u]=G[u][i]; } inline void dfs(int u,int t){ top[u]=t; pos[u]=tot++; for(int i=0;i<G[u].size();i++) if(G[u][i]==heavy[u]) dfs(G[u][i],t); for(int i=0;i<G[u].size();i++) if(G[u][i]!=heavy[u]) dfs(G[u][i],G[u][i]); } inline int bulid(int left,int right) { int p=t++; SEG& now=tree[p]; now.clear(left,right); if(right>left){ int k=(left+right)>>1; now.lchild=bulid(left,k); now.rchild=bulid(k+1,right); } return p; } //cover只有最顶端是对的 inline int query(int root,int l,int r) //区间查询,返回区间和 { if(root==-1) return 0; SEG& now=tree[root]; if(now.left>r || now.right<l) return 0; if(now.cover!=-1) return length(now.left,now.right,l,r)*now.cover; return query(now.lchild,l,r)+query(now.rchild,l,r); } inline void change(int root,int l,int r,int t,int road) //区间覆盖 { if(root==-1) return; SEG& now=tree[root]; if(road!=-1) now.cover=road; if(now.left>=l && now.right<=r){ now.cover=t; return; } if(now.right<l || now.left>r) return; change(now.lchild,l,r,t,now.cover); change(now.rchild,l,r,t,now.cover); now.cover=-1; } inline int find_max_pos(int u) { if(low[u]!=-1) return low[u]; int ans=pos[u]; int t=-1; for(int i=G[u].size()-1;i>=0;i--) if(G[u][i]!=heavy[u] || i==0){ t=i; ans=find_max_pos(G[u][i]); break; } for(int i=0;i<G[u].size();i++) if(i!=t) find_max_pos(G[u][i]); low[u]=ans; return ans; } inline void query_install(int t) { int ans=0; while(t!=-1){ ans+=(pos[t]-pos[top[t]]+1)-query(0,pos[top[t]],pos[t]); change(0,pos[top[t]],pos[t],1,-1); t=father[top[t]]; } printf("%d\n",ans); } inline void query_unstall(int t) { // int mxpos=find_max_pos(t); printf("%d\n",query(0,pos[t],low[t])); change(0,pos[t],low[t],0,-1); } inline void init() { getd(n); for(int i=1;i<n;i++){ int t; getd(t); G[t].push_back(i); //注意:本题为单向边,从父亲节点指向儿子节点。反向的边存在father里面 } tot=0; t=0; father[0]=-1; dfs_first(0); dfs(0,0); bulid(0,tot-1); //建树,预处理 // for(int i=0;i<n;i++) change(0,i,i,0,-1); getd(m); } inline void work() { memset(low,-1,sizeof(low)); find_max_pos(0); for(int i=0;i<m;i++){ char s[10]; int t; scanf("%s%d",s,&t); if(s[0]=='i') query_install(t);//安装:安装一条路径 else if(s[0]=='u') query_unstall(t);//卸载:删除一个子树 } } int main() { // freopen("manager.in","r",stdin); // freopen("manager.out","w",stdout); #ifdef LOCAL // freopen("input.txt","r",stdin); #endif init(); work(); fclose(stdin); fclose(stdout); return 0; }