【笔记】树链(熟练)剖分
/*参考 https://www.cnblogs.com/LoveYayoi/p/6745541.html
https://www.cnblogs.com/George1994/p/7821357.html
以及我校学长的优秀讲解。
*/
树链剖分可以将树剖分成一条一条的链,形成一条新链,然后通过一些维护线性数据的数据结构(比如线段树)来维护树上的信息,例如 树上求两点间边or点权和、边权or点权max or min、修改边or点权 等。
P.S.来自另一位大佬(LoveYayoi)的解释:给定一棵形态不变的树,要求支持查询一条链,修改一条链。
对于“给定一棵形态变化的树,要求支持查询和修改一条链”,需要用到LCT。
一、概念:
重儿子:u的所有子节点中,子树最大的一个。特别的,根节点自己是一个重儿子。
轻儿子:u的所有子节点中,除了重儿子的其他儿子。
重边:u与其重儿子所连的边。
轻边:u与其轻儿子所连的边。
重链:重边所形成的链。
轻链:轻边所形成的链。
dep[i]:点i在树上的深度。
siz[i]:以点i为根的子树(包括点i)的大小(点数之和)。
fa[i]:点i的父节点。
son[i]:点i的重儿子。
top[i]:点i所在的链的顶端。对于轻链,其顶端为点i本身。
dfn[i]:点i在DFS序中的编号。
rnk[i]:DFS中编号为i的点在树上的编号。
二、性质:
1.一棵树被剖分后所形成的重链数与轻链数不会超过logn条。
->这样每蹦一次操作一次,共操作O(重链数)=O(logn)次;每次操作的复杂度是O(logn);
总共的时间复杂度是O(qlogn^2)
2.若一条边(u,v)是一条轻边,则size(v)<size(u)/2。
->若一条边(u,v)是一条轻边,即点v是轻儿子,则点u至少有两个子节点;若点v是轻儿子,其子节点最多不会超过size(u)/2,否则v就成了重儿子。
三、代码:
(一)预处理
第一遍DFS,预处理出dep[]、fa[]、siz[]。
void dfs1(int u,int f,int d){ dep[u]=d; siz[u]=1; fa[u]=f; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa[u]) continue; dfs1(v,u,d+1); siz[u]+=siz[v]; if(!son[u]||siz[v]>siz[son[u]]) son[u]=v; } }
第二遍DFS,预处理出tid[]、rnk[]、top[]。
int idx; void dfs2(int u,int tp){ dfn[u]=++idx;//u的DFS序编号为idx rnk[idx]=u;//在DFS序上编号为idx的点在树上的编号为u top[u]=tp; if(!son[u]) return; dfs2(son[u],tp);//处理重链 for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa[u]||v==son[u]) continue; dfs2(v,v);//处理轻链 } }
(二)链上查询修改问题
①LCA(u,v)例:P3379 【模板】最近公共祖先(LCA)
对于树上两点u与v间路径的一系列问题,可以将其拆分为u->lca(u,v)和v->lca(u,v)两个子问题。类似于倍增求LCA,树链剖分也是类似的方法。对于u和v两点的LCA,我们让所在链的深度(即dep[top[]])大的一点跳到另一点所在的链上,当两点同在一链时,深度小的一点就是LCA(u,v)。
int lca(int u,int v){ int f1=top[u],f2=top[v]; //令所在链的深度大的一点跳到另一点所在的链上 while(f1!=f2){ //令u为所在链的深度大的一点 if(dep[f1]<dep[f2]){ swap(f1,f2); swap(u,v); } u=fa[f1]; //u跳到所在链的顶端f1=top[u]的父节点,从而进入下一条重链。 f1=top[u]; //同样所在链顶端f1也要修改 } //当两点同处一链时,深度小的为LCA int lca; if(dep[u]>=dep[v]) { lca=v; } else lca=u; return lca; }
②链上点权和、点权max... 例:P2590 [ZJOI2008]树的统计
在求LCA时通过线段树随时更新ans即可。注意查询时查询的是点的DFS序。
->链上点权和
int lca_sum(int u,int v){ int ans=0; int f1=top[u],f2=top[v]; while(f1!=f2){ if(dep[f1]<dep[f2]){ swap(f1,f2); swap(u,v); } ans+=ask_sum(1,dfn[f1],dfn[u]);//线段树区间求和 u=fa[f1]; f1=top[u]; } int lca,tmp; if(dep[u]>=dep[v]) { lca=v,tmp=u; } else lca=u,tmp=v; ans+=ask_sum(1,dfn[lca],dfn[tmp]); return ans; }
->链上最大点权
int lca_max(int u,int v){ int ans=-(1<<30); int f1=top[u],f2=top[v]; while(f1!=f2){ if(dep[f1]<dep[f2]){ swap(f1,f2); swap(u,v); } ans=max(ans,ask_max(1,tid[f1],tid[u]));//线段树区间max u=fa[f1]; f1=top[u]; } int lca,tmp; if(dep[u]>=dep[v]) { lca=v,tmp=u; } else lca=u,tmp=v; ans=max(ans,ask_max(1,tid[lca],tid[tmp])); return ans; }
P.S.
1.求链上边权和、链上边权max...时,可以将边权下放为点权。此时注意查询的边界问题:由于u向上蹦时,会从u蹦到fa[top[u]]。事实上我们只经过了top[u]~u之间的边,因此查询的边界为dfn[top[u]]~dfn[u],而不包括dfn[fa[top[u]]]的信息,因为没有经过其保存的边。
2.建树时,已知的是线段树上的编号,即DFS序编号。所以需要将线段树编号转化为树上的编号,例如tree[u].sum=val[rnk[l]],其中val[]表示点权,rnk[l]表示DFS编号为l的点在树上的编号为dfn[l]。
(三)子树查询修改问题 例:P3384 【模板】树链剖分
对于以u为根的子树,其DFS序为一段有序且连续的序列,所以我们记录点u的DFS序的范围,对于修改子树的操作直接修改DFS序的两端即可。
(代码有点长。。。就折叠了)
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int N=100000+100; int n,m,r,p; //邻接表 struct ed{ int to,nxt; }e[N<<1]; int head[N],tot; void add(int u,int v){ e[++tot]=(ed){v,head[u]}; head[u]=tot; } //线段树 int dep[N],siz[N],fa[N],top[N],son[N],dfn[N],rnk[N],left[N],right[N],val[N]; struct node{ int l,r; long long sum; long long add; }tree[N<<2]; void updata(int u){ tree[u].sum=tree[u<<1].sum+tree[u<<1|1].sum; tree[u].sum%=p; } void build(int u,int l,int r){ tree[u].l=l,tree[u].r=r,tree[u].add=0; if(l==r){ tree[u].sum=val[rnk[l]]; return ; } int mid=(l+r)>>1; build(u<<1,l,mid);build(u<<1|1,mid+1,r); updata(u); } void pushdown(int u){ int l=tree[u].l,r=tree[u].r,k=tree[u].add; if(k){ tree[u<<1].add+=k; tree[u<<1].sum+=(tree[u<<1].r-tree[u<<1].l+1)*k; tree[u<<1].sum%=p; tree[u<<1|1].add+=k; tree[u<<1|1].sum+=(tree[u<<1|1].r-tree[u<<1|1].l+1)*k; tree[u<<1|1].sum%=p; tree[u].add=0; } } void modify(int u,int ql,int qr,int k){ int l=tree[u].l,r=tree[u].r; if(ql<=l&&qr>=r){ tree[u].sum+=(r-l+1)*k; tree[u].sum%=p; tree[u].add+=k; return ; } pushdown(u); int mid=(l+r)>>1; if(ql<=mid) modify(u<<1,ql,qr,k); if(qr>mid) modify(u<<1|1,ql,qr,k); updata(u); } long long ask_sum(int u,int ql,int qr){ long long ans=0; int l=tree[u].l,r=tree[u].r; if(ql<=l&&qr>=r) return tree[u].sum%p; if(ql>r||qr<l) return 0; pushdown(u); int mid=(l+r)>>1; if(ql<=mid) { ans+=ask_sum(u<<1,ql,qr); ans%=p; } if(qr>mid) { ans+=ask_sum(u<<1|1,ql,qr); ans%=p; } return ans; } //树链剖分 void dfs1(int u,int f,int d){ dep[u]=d; fa[u]=f; siz[u]=1; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa[u]) continue; dfs1(v,u,d+1); siz[u]+=siz[v]; if(!son[u]||siz[v]>siz[son[u]]) son[u]=v; } } int idx; void dfs2(int u,int tp){ top[u]=tp; dfn[u]=++idx; rnk[idx]=u; left[u]=right[u]=idx; if(!son[u]) return; dfs2(son[u],tp); for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa[u]||v==son[u]) continue; dfs2(v,v); } right[u]=idx; } void lca_modify(int u,int v,int k){ int f1=top[u],f2=top[v]; while(f1!=f2){ if(dep[f1]<dep[f2]){ swap(f1,f2); swap(u,v); } modify(1,dfn[f1],dfn[u],k); u=fa[f1]; f1=top[u]; } int lca,tmp; if(dep[u]>=dep[v]) lca=v,tmp=u; else lca=u,tmp=v; modify(1,dfn[lca],dfn[tmp],k); return ; } long long lca_sum(int u,int v){ int f1=top[u],f2=top[v]; long long ans=0; while(f1!=f2){ if(dep[f1]<dep[f2]){ swap(f1,f2); swap(u,v); } ans+=ask_sum(1,dfn[f1],dfn[u]); ans%=p; u=fa[f1]; f1=top[u]; } int lca,tmp; if(dep[u]>=dep[v]) lca=v,tmp=u; else lca=u,tmp=v; ans+=ask_sum(1,dfn[lca],dfn[tmp]); ans%=p; return ans; } int main(){ scanf("%d%d%d%d",&n,&m,&r,&p); for(int i=1;i<=n;i++) scanf("%d",&val[i]); for(int i=1;i<n;i++){ int u,v; scanf("%d%d",&u,&v); add(u,v); add(v,u); } dfs1(r,0,1); dfs2(r,r); build(1,1,n); for(int i=1;i<=m;i++){ int op,x,y,z; scanf("%d",&op); if(op==1){//链上修改 scanf("%d%d%d",&x,&y,&z); lca_modify(x,y,z); } else if(op==2){//链上查询 scanf("%d%d",&x,&y); printf("%lld\n",lca_sum(x,y)); } else if(op==3){//子树修改 scanf("%d%d",&x,&z); modify(1,left[x],right[x],z); } else if(op==4){//子树查询 scanf("%d",&x); printf("%lld\n",ask_sum(1,left[x],right[x])); } } return 0; }

浙公网安备 33010602011771号