树链剖分学习笔记 By cellur925
先%一发机房各路祖传树剖大师%%%。
近来总有人向我安利树剖求LCA,然鹅我还是最爱树上倍增。然鹅又发现近年一些题目(如天天爱跑步、运输计划等在树上进行操作的题目),我有把树转化为一条链求解的思路,但是不知道怎么实现。于是还是学了树链剖分(真香),就权当打暴力的工具了。其实主要是学习它的思想,而它实际包含的知识(线段树(大多情况用线段树,理论上应该还能用其他数据结构维护)、dfs序与时间戳、树的遍历)比较基础,只要把他们掌握,学习树剖就不难了。讲真树剖可能是我学的最快的知识。
主要思想:划分轻重链,把树上的某条路径化为一条链,再用数据结构维护。(把一棵树拆成若干互不相交的链)
树剖中的一些概念:
这里可详见@communist dalao的解释部分
可见,树剖中我们需要记录很多的量,于是就有了我们的第一个核心算法:两遍dfs,求出所有信息!
通常我们第一遍dfs求出d[](深度),f[](父节点),size[](子树大小),son[](重儿子)。
void dfs1(int u,int fa,int dep) {//第一遍dfs 预处理出f[]/d[]/son[]/size[] f[u]=fa; d[u]=dep; size[u]=1; for(int i=head[u];i;i=edge[i].next) { int v=edge[i].to; if(v==fa) continue; dfs1(v,u,dep+1); size[u]+=size[v]; if(size[v]>size[son[u]]) son[u]=v; } }
第二遍我们在第一遍的基础上继续求出其他量。top[](当前节点所在链的最顶层节点),id[](节点的新编号,可理解为时间戳),rk[](保存dfs标号在树上的具体节点,实际操作可为点权)
这里id与rk的关系有点像时间戳与dfs序,但不完全是,emm可以感性理解下。(时间戳与dfs序,他们的联系具体在这里探讨过。)
至于为什么要引入他们,因为我们希望一条重链在数据结构(如线段树)上的排列分布是连续的,这样我们才好维护他们。
void dfs2(int u,int t) {//第二遍dfs 预处理出id[]/rk[]/top[] top[u]=t; id[u]=++cnt; rk[cnt]=val[u]; if(!son[u]) return ;//找到了叶子 //通过优先进入重儿子来保证一条重链上各节点dfs序连续 dfs2(son[u],t); for(int i=head[u];i;i=edge[i].next) { int v=edge[i].to; if(v==son[u]||v==f[u]) continue; dfs2(v,v);//在轻链上 top为本身 } }
有了这些操作,我们就可以进行线段树的维护了。这里的线段树中,节点标号用的是我们刚映射好的时间戳,权值是节点的点权,重链上的节点在线段树上标号连续。(注意我们的rk数组,建树时用的是它而不是初始点权。)
之后我们就按线段树规矩建树、修改、查询即可。这是一个相对独立的部分。
以上便是树链剖分的基本操作,我们以LuoguP3384【模板】树链剖分为例,讲解一下树剖的具体食用方法。
题目要求我们维护:
操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z
操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和
操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z
操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和
慢慢分析。在操作1中,我们首先需要让x,y到同一个重链上,但是在到达之前,我们也需要记录下。于是就有了
void treeadd(int x,int y,int w) { while(top[x]!=top[y]) { if(d[top[x]]<d[top[y]]) swap(x,y); change(1,id[top[x]],id[x],w); x=f[top[x]]; } if(d[x]>d[y]) swap(x,y); change(1,id[x],id[y],w); }
操作2原理相似。因为这部分我讲的不是很好==。我是看这位大神的blog看懂的,这里就甩链接了...。
ll treeask(int x,int y) { ll ans=0; while(top[x]!=top[y]) { if(d[top[x]]<d[top[y]]) swap(x,y); (ans+=ask(1,id[top[x]],id[x]))%=moder; x=f[top[x]]; } if(d[x]>d[y]) swap(x,y); (ans+=ask(1,id[x],id[y]))%=moder; return ans; }
至于操作3.4,其实最简单了qwq。我们直接在线段树上操作就行了。
综上,我们解决了第一道树剖题。
1 #include<cstdio> 2 #include<algorithm> 3 #define maxn 100090 4 5 using namespace std; 6 typedef long long ll; 7 8 int n,m,root,moder,tot,cnt; 9 int head[maxn],size[maxn],d[maxn],f[maxn],son[maxn],val[maxn]; 10 int top[maxn],id[maxn],rk[maxn]; 11 struct node{ 12 int to,next; 13 }edge[maxn*2]; 14 struct SegmentTree{ 15 int l,r; 16 ll w,lazy; 17 }t[maxn*4]; 18 19 void add(int x,int y) 20 { 21 edge[++tot].to=y; 22 edge[tot].next=head[x]; 23 head[x]=tot; 24 } 25 26 void dfs1(int u,int fa,int dep) 27 {//第一遍dfs 预处理出f[]/d[]/son[]/size[] 28 f[u]=fa; 29 d[u]=dep; 30 size[u]=1; 31 for(int i=head[u];i;i=edge[i].next) 32 { 33 int v=edge[i].to; 34 if(v==fa) continue; 35 dfs1(v,u,dep+1); 36 size[u]+=size[v]; 37 if(size[v]>size[son[u]]) 38 son[u]=v; 39 } 40 } 41 42 void dfs2(int u,int t) 43 {//第二遍dfs 预处理出id[]/rk[]/top[] 44 top[u]=t; 45 id[u]=++cnt; 46 rk[cnt]=val[u]; 47 if(!son[u]) return ;//找到了叶子 48 //通过优先进入重儿子来保证一条重链上各节点dfs序连续 49 dfs2(son[u],t); 50 for(int i=head[u];i;i=edge[i].next) 51 { 52 int v=edge[i].to; 53 if(v==son[u]||v==f[u]) continue; 54 dfs2(v,v);//在轻链上 top为本身 55 } 56 } 57 58 void update(int p) 59 { 60 if(t[p].l==t[p].r) return ; 61 if(!t[p].lazy) return ; 62 t[p*2].w+=t[p].lazy*(t[p*2].r-t[p*2].l+1); 63 t[p*2+1].w+=t[p].lazy*(t[p*2+1].r-t[p*2+1].l+1); 64 t[p*2].lazy+=t[p].lazy; 65 t[p*2+1].lazy+=t[p].lazy; 66 t[p].lazy=0; 67 } 68 69 void build(int p,int l,int r) 70 { 71 t[p].l=l,t[p].r=r; 72 if(l==r) 73 { 74 t[p].w=rk[l]; 75 return ; 76 } 77 int mid=(l+r)>>1; 78 build(p*2,l,mid); 79 build(p*2+1,mid+1,r); 80 (t[p].w=t[p*2].w+t[p*2+1].w)%=moder; 81 } 82 83 void change(int p,int l,int r,int x) 84 { 85 update(p); 86 if(t[p].l==l&&t[p].r==r) 87 { 88 t[p].w+=x*(r-l+1); 89 t[p].lazy+=x; 90 return ; 91 } 92 int mid=(t[p].l+t[p].r)>>1; 93 if(l>mid) change(p*2+1,l,r,x); 94 else if(r<=mid) change(p*2,l,r,x); 95 else change(p*2,l,mid,x),change(p*2+1,mid+1,r,x); 96 (t[p].w=t[p*2].w+t[p*2+1].w)%=moder; 97 } 98 99 ll ask(int p,int l,int r) 100 { 101 update(p); 102 if(t[p].l==l&&t[p].r==r) return (t[p].w)%moder; 103 int mid=(t[p].l+t[p].r)>>1; 104 if(l>mid) return (ask(p*2+1,l,r))%moder; 105 else if(r<=mid) return (ask(p*2,l,r))%moder; 106 else return (ask(p*2,l,mid)+ask(p*2+1,mid+1,r))%moder; 107 } 108 109 ll treeask(int x,int y) 110 { 111 ll ans=0; 112 while(top[x]!=top[y]) 113 { 114 if(d[top[x]]<d[top[y]]) swap(x,y); 115 (ans+=ask(1,id[top[x]],id[x]))%=moder; 116 x=f[top[x]]; 117 } 118 if(d[x]>d[y]) swap(x,y); 119 (ans+=ask(1,id[x],id[y]))%=moder; 120 return ans; 121 } 122 123 void treeadd(int x,int y,int w) 124 { 125 while(top[x]!=top[y]) 126 { 127 if(d[top[x]]<d[top[y]]) swap(x,y); 128 change(1,id[top[x]],id[x],w); 129 x=f[top[x]]; 130 } 131 if(d[x]>d[y]) swap(x,y); 132 change(1,id[x],id[y],w); 133 } 134 135 int main() 136 { 137 scanf("%d%d%d%d",&n,&m,&root,&moder); 138 for(int i=1;i<=n;i++) scanf("%d",&val[i]); 139 for(int i=1;i<=n-1;i++) 140 { 141 int x=0,y=0; 142 scanf("%d%d",&x,&y); 143 add(x,y);add(y,x); 144 } 145 dfs1(root,0,1); 146 dfs2(root,root); 147 build(1,1,n); 148 while(m--) 149 { 150 int opt=0; 151 scanf("%d",&opt); 152 if(opt==1) 153 { 154 int x=0,y=0,z=0; 155 scanf("%d%d%d",&x,&y,&z); 156 treeadd(x,y,z); 157 } 158 else if(opt==2) 159 { 160 int x=0,y=0; 161 scanf("%d%d",&x,&y); 162 printf("%lld\n",treeask(x,y)); 163 } 164 else if(opt==3) 165 { 166 int x=0,y=0; 167 scanf("%d%d",&x,&y); 168 change(1,id[x],id[x]+size[x]-1,y); 169 } 170 else if(opt==4) 171 { 172 int x=0; 173 scanf("%d",&x); 174 printf("%lld\n",ask(1,id[x],id[x]+size[x]-1)%moder); 175 } 176 } 177 return 0; 178 }
Update:2018/10/3
早上来做了一道树链剖分...本想一次过的结果卡到九点...。
原因:单点修改的时候用的是x而不是id[x],查询最大值的时候因为忽略了还有负数,所以初始值设的是0,而应该是负无穷。
Update:2018/10/6
今天又做了一道板子题==
依然改了很久==
没有什么困难的操作,有一个单点修改。
开始我是想纯用之前的单点修改不带懒标记的,后来爆零了==
改成区间修改左右端点相同的情况,再开着longlong就能A了==。
百思不得其解,难道只能化为区间修改的特殊情况了么==。
后来在金涛的指点下,发现单点修改下传一个懒标记就行了,就能A了。原因不太明...。
Update:2018-10-31
今日份敲模板的错误:
//size[x]=1
//chushizhi rk[p]->rk[l]
//update 一直在lazy val操作假的