前言:似乎好久没写公开的博客了,,,原内容我也是咕咕咕了好久才记得要学的,,,,果然,,人类的本性
这篇博客的操作基于洛谷上的树链剖分模板题:传送门
定义:
- 重儿子:对于每一个非叶节点,他的儿子A中,以某个儿子B为根,其子树节点最大的那个B就被成为A的重儿子
- 轻儿子:非叶节点中除了重儿子的其余节点(废话)
- 重边:连接重儿子及其父亲节点的边
- 轻边:除了重边以外的边废话*2
- 重链:
- 相邻重边链接起来的链
- 叶子节点自己
- 每一条重链以轻儿子为起点
其余解释见代码注释


1 #include<iostream> 2 #include<cstdio> 3 using namespace std; 4 #define ll long long 5 /* 6 树链剖分: 7 个人理解就是给你一颗有点权的树,然后再给你一些骚操作,例如本题的操作 8 个人认为,全过程就是你先把这个树找到他们的重儿子,然后按照dfs序把他们进行剖分 9 把这棵树肢解成一段一段的链条,每条链条把他们当成一条线段,再用线段树维护 10 前置知识: 11 图论基本常识,带tag的线段树,dfs序 12 */ 13 namespace the_Death{ 14 const int MAXN=1e5+10; 15 int n,m,root,mod,tot; 16 int head[MAXN],ver[MAXN<<1],nxt[MAXN<<1]; 17 void add(int x,int y){ 18 ver[++tot]=y;nxt[tot]=head[x];head[x]=tot; 19 }//存图 20 int dfn[MAXN],dfn2[MAXN],size[MAXN],dep[MAXN]; 21 int fa[MAXN],top[MAXN],son[MAXN],dp,id[MAXN]; 22 //树剖所用 23 //dfn[MAXN]->标记dfs序,dfn2[MAXN]->标记树剖出的链的右端点 24 //dep[MAXN]->这个点所在的深度,fa[MAXN]->某个点的父亲节点标号 25 //son[MAXN]->重儿子标号,id[MAXN]->记录某个dfs序的原序号是什么 26 //size[MAXN]->子树大小,dp->dfs序的序号,top[MAXN]->树剖后某个链的起点 27 ll w[MAXN],d;int x,y,op; 28 //w[MAXN]->节点上的初值 29 //———————————————————————————————————树剖的前置工作———————————————————————————————————————————— 30 void dfs1(int x,int d){ 31 //目的:为了找到重儿子&父亲,某点的深度,以某点为根的子树大小 32 dep[x]=d;size[x]=1; 33 //x的深度是d,以x为根的子树大小为1 34 for(register int i=head[x];i;i=nxt[i]){ 35 int y=ver[i]; 36 if(y==fa[x]) continue; 37 fa[y]=x;dfs1(y,d+1);size[x]+=size[y]; 38 //标记父节点标号,再向下dfs,而后遍历树后,统计子树大小 39 if(size[y]>size[son[x]]) son[x]=y; 40 //标记每个子树的重儿子标号 41 } 42 } 43 void dfs2(int x){ 44 //目的:为了找到dfs序,某条链剖完后的左右端点,某个dfs序所对应的原节点 45 //关于“先重后轻”处理链,原因见最下面代码结束后的地方 46 dfn[x]=++dp;id[dp]=x; 47 //dfn[]->标记dfs序;id[]->序号dp的位置对应x节点 48 if(!top[x]) top[x]=x; 49 //如果这个点所在链上没有top[]的话,就指定它是起点 50 if(son[x]) top[son[x]]=top[x],dfs2(son[x]); 51 //如果这个点下面有重儿子的话。优先处理重儿子,并且把这个重儿子当为这个链的top[] 52 for(register int i=head[x];i;i=nxt[i]){ 53 int y=ver[i]; 54 if(y==fa[x]||y==son[x]) continue; 55 //如果这个点不是重儿子,不是父亲,那就以轻儿子为端点继续搜 56 dfs2(y); 57 } 58 dfn2[x]=dp;//这个链全部剖完后,右端点的dfs序 59 } 60 //——————————————————————————————————————线段树———————————————————————————————————————————————— 61 struct ziji{ 62 ll sum,tag; 63 //tag(i)->以i为根节点的子树应该加上多少 64 #define sum(i) mn[i].sum 65 #define tag(i) mn[i].tag 66 }mn[MAXN<<2];//线段树 67 void pushup(int x){ 68 sum(x)=(sum(x<<1)+sum(x<<1|1))%mod; 69 } 70 void build(int now,int l,int r){ 71 if(l==r){ 72 sum(now)=w[id[l]]%mod; 73 //由于是把每棵树按照dfs序进行的树剖 74 //所以每一个叶子节点的值应该是还原dfs序后的真实序号所对应的点权 75 tag(now)=0;return; 76 } 77 tag(now)=0;int mid=l+r>>1; 78 build(now<<1,l,mid);build(now<<1|1,mid+1,r); 79 pushup(now); 80 } 81 void pushdown(int now,int l,int r){//下放标记 82 if(!tag(now)) return ; 83 (tag(now<<1)+=tag(now))%=mod; 84 (tag(now<<1|1)+=tag(now))%=mod; 85 //左右子树将要加的值,应该是以自己父亲节点为根要加的值和以自己为根要加的值之和 86 int mid=l+r>>1; 87 sum(now<<1)=(sum(now<<1)+tag(now)*(mid-l+1)%mod)%mod; 88 sum(now<<1|1)=(sum(now<<1|1)+tag(now)*(r-mid)%mod)%mod; 89 //sum上要加的值,应该就是自己所维护的这一段树链的长度*要以这个点为根所加上的值 90 //由于add()函数里已经处理了以自己为根的子树加上tag后的值 91 //所以此时你只要更新自己左右节点的变化值就可以了 92 tag(now)=0; 93 } 94 void add(int now,int l,int r,int L,int R,ll d){ 95 if(L<=l&&r<=R){ 96 (tag(now)+=d)%mod; 97 sum(now)=(sum(now)+(r-l+1)*d%mod)%mod; 98 //当你要处理的L,R在你找的范围里,那你直接更新就好了 99 return ; 100 } 101 pushdown(now,l,r);int mid=l+r>>1; 102 if(L<=mid) add(now<<1,l,mid,L,R,d); 103 if(mid<R) add(now<<1|1,mid+1,r,L,R,d); 104 pushup(now); 105 } 106 ll ask(int now,int l,int r,int L,int R){ 107 if(L<=l&&r<=R) return sum(now); 108 pushdown(now,l,r); 109 //如果没有在你找到的区间里,代表这个链的跨度比较大 110 //所以此时你要更新你的tag与sum 111 ll ret=0;int mid=l+r>>1; 112 if(L<=mid) ret=(ret+ask(now<<1,l,mid,L,R))%mod; 113 if(mid<R) ret=(ret+ask(now<<1|1,mid+1,r,L,R))%mod; 114 return ret; 115 } 116 //———————————————————————————————————树链剖分&线段树融合—————————————————————————————————————————— 117 //个人认为这一下就是把你剖好的链和你写好的线段树相结合 118 void work1(int x,int y,ll d){ 119 //树上加 120 /* 121 原理总括性的说明: 122 当你要对L到R的规定区间里加d,你所要面对有两种情况: 123 1.这俩在一条链上->你直接按照线段树的方式进行维护 124 2.这俩不在一条链上: 125 首先你要知道,无论你怎么剖分,本质上他们呢还是在树上的。 126 所以你可以通过类似于倍增找lca的方法,让他们变成一条链上,然后再回到情况1 127 由于是从下向上更新,所以每次向上走的时候都要对以自己为根的子树进行维护操作 128 */ 129 while(top[x]!=top[y]){ 130 //如果两点不在一条链上 131 if(dep[top[x]]<dep[top[y]]) swap(x,y); 132 //如果x所在链顶点比y所在链的顶点浅,就交换 133 add(1,1,n,dfn[top[x]],dfn[x],d); 134 x=fa[top[x]]; 135 //把x跳到x所在链顶端的那个点的上面一个点 136 }//如此操作,直到x,y在同一个链上 137 if(dep[x]<dep[y]) swap(x,y); 138 add(1,1,n,dfn[y],dfn[x],d); 139 //最后再在这一条链上加 140 } 141 ll work2(int x,int y){//树上查询,同上 142 ll ret=0; 143 while(top[x]!=top[y]){ 144 if(dep[top[x]]<dep[top[y]]) swap(x,y); 145 ret=(ret+ask(1,1,n,dfn[top[x]],dfn[x]))%mod; 146 x=fa[top[x]]; 147 } 148 if(dep[x]<dep[y]) swap(x,y); 149 ret=(ret+ask(1,1,n,dfn[y],dfn[x]))%mod; 150 return ret; 151 } 152 //——————————————————————————————————————————————主程序———————————————————————————————————————— 153 int main(){ 154 scanf("%d%d%d%d",&n,&m,&root,&mod); 155 for(register int i=1;i<=n;++i) scanf("%lld",&w[i]); 156 for(register int i=1;i<=n-1;++i) 157 scanf("%d%d",&x,&y),add(x,y),add(y,x); 158 dfs1(root,1);dfs2(root);build(1,1,n); 159 while(m--){ 160 scanf("%d",&op); 161 switch(op){ 162 case 1:scanf("%d%d%lld",&x,&y,&d),work1(x,y,d);break; 163 case 2:scanf("%d%d",&x,&y),printf("%lld\n",work2(x,y));break; 164 case 3:scanf("%d%lld",&x,&d),add(1,1,n,dfn[x],dfn2[x],d);break; 165 case 4:scanf("%d",&x),printf("%lld\n",ask(1,1,n,dfn[x],dfn2[x]));break; 166 } 167 } 168 system("pause");return 0; 169 } 170 } 171 int main(void){ 172 the_Death::main(); 173 return 0; 174 }
NOTICE:关于为什么第二次dfs要先重后轻
在先重后轻的方式下,你可以保证重儿子的dfs序是连续的,也可以保证子树的编号是来连续的
才不是因为别人都告诉我要先重后轻我才这样做的呢
最后国际惯例:thankyou for your attention
代码参考于 *Miracle*