树链剖分小结
树链剖分,计算机术语,指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、BST、SPLAY、线段树等)来维护每一条链。
——百度百科
重链剖分
概念 1 重儿子
一个父节点的所有儿子中,子树节点最大( 最大)的节点。
记为 。
概念2 轻儿子
父节点所有儿子中,除过重儿子的所有节点。
概念3 重边
由父亲节点和重儿子连接成的边。
概念4 轻边
由父亲节点和轻儿子连接成的边。
概念5 重链
由多条重边连成的链。
概念5 轻边
由多条轻边连成的链。
实现思路
- 对于一个节点先找出它所在的子树大小,同时我们可以得到它的所有子节点的子树大小 ,这样我们可以得到此节点的重儿子。
例如,点 1 的三个儿子分别是 2,3,4。
2 所在的子树大小为 5,
3 所在的子树大小为 2,
4 所在的子树大小为 6,
那么 1 的重儿子就是 4。
-
在 的过程中顺便记录节点 的父亲 ,从根节点的深度 等。
-
再来一遍 ,连接重链,标记每个节点的 序,处理出每个节点所在重链的顶点 和节点编号 。
代码实现
第一遍 dfs
作用主要是找出每个节点的深度和重儿子。
int son[500010],deep[500101],f[500101]; int siz[500010]; //son:重儿子 //deep:节点深度 //f:节点的父节点 void dfs1(int u,int fa) { f[u]=fa; deep[u]=deep[fa]+1; siz[u]=1; for(int i=head[u];i;i=edge[i].next) { int v=edge[i].to; if(v==fa)continue; dfs1(v,u); siz[u]+=siz[v]; if(siz[son[u]]<siz[v]) son[u]=v; } }
第二遍 dfs
作用主要是求出每条重链的顶点和节点的 序。
//rk:节点编号 //id:dfn序 //top:重链顶端 void dfs2(int u,int t) { top[u]=t; if(son[u])dfs2(son[u],t); id[u]=++cnt; rk[cnt]=u; 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); } }
这样就完成了树链剖分的基础操作(链式前向星存图)。
线段树操作总结
单点加,区间加
void add(int now,int l,int r,int k) { tr[now].lazy+=k; tr[now].sum+=k*(r-l+1); } void pushdown(int now,int l,int r) { if(!tr[now].lazy) return ; int mid=(l+r)>>1; add(lid,l,mid,tr[now].lazy); add(rid,mid+1,r,tr[now].lazy); tr[now].lazy=0; } void change(int now,int l,int r,int x,int y,int k) { if(x<=l&&r<=y) { add(now,l,r,k);return; } pushdown(now,l,r); int mid=(l+r)>>1; if(x<=mid) change(lid,l,mid,x,y,k); if(y>mid) change(rid,mid+1,r,x,y,k); tr[now].sum=tr[lid].sum+tr[rid].sum; }
单点查,区间查
最基础的线段树操作之一。
别忘了 。
int query(int now,int l,int r,int x,int y) { if(x<=l&&r<=y) return tr[now].sum; pushdown(now,l,r); int mid=(l+r)>>1; int res=0; if(x<=mid) res+=query(lid,l,mid,x,y); if(y>mid) res+=query(rid,mid+1,r,x,y); return res; }
子树加
通过调用区间加的函数来实现。
一个节点 的子树的 序的范围是 。
其中 表示子树大小,在 中求得。
void add_tree(int u,int k) { change(1,1,n,id[u],id[u]+siz[u]-1,k); }
树链加
代码基本同求 。
void add_path(int u,int v,int k) { while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]]) swap(u,v); change(1,1,n,id[top[u]],id[u],k); u=f[top[u]]; } if(dep[u]>dep[v])swap(u,v); change(1,1,n,id[u],id[v],k); }
子树查
代码同子树加。只不过调用的函数不同。
int query_tree(int u) { return query(1,1,n,id[u],id[u]+siz[u]-1); }
树链查
代码同树链加,也是调用的函数不同。
int query_path(int u,int v) { int res=0; while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]])swap(u,v); res+=query(1,1,n,id[top[u]],id[u]); u=f[top[u]]; } if(dep[u]>dep[v])swap(u,v); res+=query(1,1,n,id[u],id[v]); return res; }
边权下放
从网上学的,详见例题 3。
题目讲解
例题 1:P3384 【模板】重链剖分/树链剖分
简明地讲,这道题就是用线段树维护一个树链剖分,要做到子树改,树链改,子树查,树链查。
#include<bits/stdc++.h> using namespace std; #define N 200001 int w[N]; int n,m,p,s; struct node{ int to,next; }edge[N]; int head[N],ccnt; inline void add(int u,int v) { edge[++ccnt].next=head[u]; edge[ccnt].to=v; head[u]=ccnt; } int dep[N],son[N],f[N],siz[N]; void dfs1(int u,int fa) { dep[u]=dep[fa]+1; f[u]=fa; siz[u]=1; for(int i=head[u];i;i=edge[i].next) { int v=edge[i].to; if(v==fa) continue; dfs1(v,u); siz[u]+=siz[v]; if(siz[son[u]]<siz[v])son[u]=v; } } int top[N],rk[N],id[N],nw[N]; int cnt; void dfs2(int u,int t) { top[u]=t,id[u]=++cnt,nw[cnt]=w[u]; if(!son[u])return ; dfs2(son[u],t); // rk[cnt]=u; 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); } } class wme{ private: struct nodee { int num,lazy; }tr[N<<2]; public: #define lid now<<1 #define rid now<<1|1 void build(int now,int l,int r) { if(l==r) { tr[now].num=nw[r]; return ; } int mid=(l+r)>>1; build(lid,l,mid),build(rid,mid+1,r); tr[now].num=(tr[lid].num+tr[rid].num)%p; } void Add(int now,int l,int r,int k) { tr[now].num+=k*(r-l+1); tr[now].lazy+=k; } void pushdown(int now,int l,int r) { if(!tr[now].lazy)return; int mid=(l+r)>>1; Add(lid,l,mid,tr[now].lazy); Add(rid,mid+1,r,tr[now].lazy); tr[now].lazy=0; } void upd(int now,int l,int r,int x,int y,int k) { if(x<=l&&r<=y) { Add(now,l,r,k);return ; } pushdown(now,l,r); int mid=(l+r)>>1; if(x<=mid)upd(lid,l,mid,x,y,k); if(y>mid) upd(rid,mid+1,r,x,y,k); tr[now].num=(tr[lid].num+tr[rid].num)%p; } void upd_Path(int u,int v,int k) { while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]])swap(u,v); upd(1,1,n,id[top[u]],id[u],k); u=f[top[u]]; } if(dep[u]<dep[v])swap(u,v); upd(1,1,n,id[v],id[u],k); } void upd_Tree(int u,int k) { upd(1,1,n,id[u],id[u]+siz[u]-1,k); } long long query(int now,int l,int r,int x,int y) { if(x<=l&&r<=y) return tr[now].num; pushdown(now,l,r); int mid=(l+r)>>1; long long res=0; if(x<=mid) res+=query(lid,l,mid,x,y); if(y>mid) res+=query(rid,mid+1,r,x,y); return res%p; } long long query_Path(int u,int v) { long long res=0; while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]])u^=v^=u^=v; res+=query(1,1,n,id[top[u]],id[u]); u=f[top[u]]; }res%=p; if(dep[u]<dep[v])u^=v^=u^=v; res+=query(1,1,n,id[v],id[u]); return res; } long long query_Tree(int u) { return query(1,1,n,id[u],id[u]+siz[u]-1); } }st; signed main() { cin>>n>>m>>s>>p; for(int i=1;i<=n;i++) cin>>w[i]; for(int i=1;i<n;i++) { int u,v; cin>>u>>v; add(u,v),add(v,u); } dfs1(s,0); dfs2(s,s); st.build(1,1,n); while(m--) { int t,u,v,k; cin>>t>>u; if(t==1) { cin>>v>>k; st.upd_Path(u,v,k); } else if(t==2) { cin>>v; cout<<st.query_Path(u,v)%p<<endl; } else if(t==3) { cin>>k; st.upd_Tree(u,k); } else cout<<st.query_Tree(u)%p<<endl; } return 0; }
例题 2:#P396. 「ZJOI 2008」树的统计
单点修改,树链求和、最大值。
最大值的求法与求和大同小异。
核心代码:
long long query_Pathmax(int u,int v) { long long res=-1145141919; while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]])swap(u,v); res=max(res,querymax(1,1,n,id[top[u]],id[u])); u=f[top[u]]; } if(dep[u]<dep[v])swap(u,v); res=max(res,querymax(1,1,n,id[v],id[u])); return res; }
例题 3:#P398. [SPOJ375 QTREE]难存的情缘
这道题给出了书上每条边的权值,会修改某条边的权值和查询两点树链边权最大值。
难点在边权下放和修改上。我参考了别人的思路,用了几个 vector
去搞。
记录每条边的下方节点,把边权转化成点权。
void dfs1(int u) { siz[u]=1; for(int i=0;i<edge[u].size();i++) { int v=edge[u][i]; if(!siz[v]) { dep[v]=dep[u]+1;f[v]=u; indexnode[Index[u][i]]=v;//重点 a[v]=W[u][i]; dfs1(v); siz[u]+=siz[v]; if(siz[v]>siz[son[u]]) son[u]=v; } } }
for(int i=1;i<n;i++) { cin>>u>>v>>w; Index[u].push_back(i);//Index 向量和 indexnode 数组完成了边权下放 Index[v].push_back(i);// edge[u].push_back(v); edge[v].push_back(u); W[u].push_back(w); W[v].push_back(w); // add(u,v,w),add(v,u,w); }
还有一点,在 query_path
的时候,不能包含两点的 ,因为 的点权是 上面的边的,不包含在树链中。
完整代码:
#include<bits/stdc++.h> using namespace std; int n,m; const int N=2e4+2; int ww[N]; vector<int>Index[N],edge[N],W[N]; int indexnode[N]; int a[N]; int son[N],siz[N],f[N],dep[N]; void dfs1(int u) { siz[u]=1; for(int i=0;i<edge[u].size();i++) { int v=edge[u][i]; if(!siz[v]) { dep[v]=dep[u]+1;f[v]=u; indexnode[Index[u][i]]=v; a[v]=W[u][i]; dfs1(v); siz[u]+=siz[v]; if(siz[v]>siz[son[u]]) son[u]=v; } } } int top[N],nw[N],id[N],cnt; void dfs2(int u,int t) { top[u]=t; id[u]=++cnt; nw[cnt]=u; if(son[u])dfs2(son[u],t); for(int i=0;i<edge[u].size();i++) { int v=edge[u][i]; if(v==son[u]||v==f[u])continue; dfs2(v,v); } } class wmw{ private: struct nodee{ int mx; }tr[N<<2]; public: #define lid now<<1 #define rid now<<1|1 void build(int now,int l,int r) { if(l==r) { tr[now].mx=a[nw[l]];return ; } int mid=(l+r)>>1; build(lid,l,mid),build(rid,mid+1,r); tr[now].mx=max(tr[lid].mx,tr[rid].mx); } void change(int now,int l,int r,int x,int y,int k) { if(x<=l&&r<=y) { tr[now].mx=k; return ; } int mid=(l+r)>>1; if(x<=mid) change(lid,l,mid,x,y,k); if(y>mid) change(rid,mid+1,r,x,y,k); tr[now].mx=max(tr[lid].mx,tr[rid].mx); } int query(int now,int l,int r,int x,int y) { if(x<=l&&r<=y) { return tr[now].mx; } int res=-INT_MAX-1; int mid=(l+r)>>1; if(x<=mid) res=max(res,query(lid,l,mid,x,y)); if(y>mid) res=max(res,query(rid,mid+1,r,x,y)); return res; } int query_path(int u,int v) { int res=-1145141919; while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]])swap(u,v); res=max(res,query(1,1,n,id[top[u]],id[u])); u=f[top[u]]; } if(dep[u]>dep[v]) swap(u,v); res=max(res,query(1,1,n,id[u]+1,id[v])); return res; } }st; signed main() { cin>>n;int u,v,w; for(int i=1;i<n;i++) { cin>>u>>v>>w; Index[u].push_back(i); Index[v].push_back(i); edge[u].push_back(v); edge[v].push_back(u); W[u].push_back(w); W[v].push_back(w); } dfs1(1); dfs2(1,1); st.build(1,1,n); string c;cin>>c; while(c!="DONE") { int x,y; cin>>x>>y; if(c[0]=='Q') { cout<<st.query_path(x,y)<<endl; } else if(c[0]=='C') { x=indexnode[x]; st.change(1,1,n,id[x],id[x],y); } cin>>c; } }
例题 4:P3178 [HAOI2015] 树上操作
板子题。题目要求单点修改,子树修改,树链查询。
但是树链查询是从根节点到某一节点的,比较弱化,可以使用差分去做,但是我选择树剖+线段树。
子树查询代码:
void add_tree(int u,int k) { change(1,1,n,id[u],id[u]+siz[u]-1,k); }
例题 5:P3833 [SHOI2012] 魔法树
这道题核心考察了树链加。
依然是 lca 的代码,改几下就好了。
void add_path(int u,int v,int k) { while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]]) swap(u,v); change(1,1,n,id[top[u]],id[u],k); u=f[top[u]]; } if(dep[u]>dep[v])swap(u,v); change(1,1,n,id[u],id[v],k); }
例题 6:P4281 [AHOI2008] 紧急集合 / 聚会
这道题只需要求出 即可。但是在求两点距离的时候,不需要用线段树维护,去 query_path
。
只需要用到 数组。
int dis(int u,int v) { int lc=lca(u,v); return dep[u]+dep[v]-2*dep[lc]; }
例题 7:P3979 遥远的国度
是个可爱的小紫。
和「BZOJ3306」树 这道题很像啊,换根树剖。
搞清楚当前 的深度和查询的 的关系就行了。
int getans(int u) { if(root==u) return st.query(1,1,n,1,n); if(dep[u]>=dep[root]) return st.query_tree(u); if(dep[u]<dep[root]&&f[lca(u,root)]==u) { int ff=lca(u,root); return min(st.query(1,1,n,1,id[ff]-1),st.query(1,1,n,id[ff]+siz[ff],n)); } else return st.query_tree(u); }
例题 8:P2146 [NOI2015] 软件包管理器
全世界的 OI 同仁都应该写一下这道题。。
浪费了我整整半天的时间,重构了一遍。
-
对于 操作,先
query_path(1,u)
,再upd_Path(1,u,1)
,输出 减去刚刚query_path(1,u)
的值。 -
对于 操作,输出
st.query_Tree(u)
,再upd_Tree(u,0)
修改子树。
代码里面总共有 3 个 bug。
- dfs2 里的条件
void dfs2(int u,int t) { top[u]=t;id[u]=++cnt; if(!son[u]) return ; dfs2(son[u],t); for(int i=0;i<edge[u/*这里,原本写的是i*/].size();i++) { int v=edge[u][i]; if(v==f[u]||v==son[u]) continue; dfs2(v,v); } }
void pushdown(int now,int l,int r) { if(tr[now].lazy==-1) return ; tr[lid].lazy=tr[rid].lazy=tr[now].lazy; //下面的两个 r-l+1 都错了,应该是 l-mid+1 和 r-mid tr[lid].sum=(r-l+1)*tr[now].lazy; tr[rid].sum=(r-l+1)*tr[now].lazy; tr[now].lazy=-1; }
- 修改 之后 还是错的
void add(int now,int l,int r,int k) { tr[now].lazy=k; //下面的 (r-l+1)*k 原本没有乘 k tr[now].sum=(r-l+1)*k; } void pushdown(int now,int l,int r) { if(tr[now].lazy!=-1){ int mid=(l+r)>>1; add(lid,l,mid,tr[now].lazy); add(rid,mid+1,r,tr[now].lazy); tr[now].lazy=-1; } }
例题 9:P4315 月下“毛景树”
整整一天!!!!
细节爆炸。
新的边权下放方法(链式前向星)
难点在于边权下放和 。
void pushdown(int now,int l,int r) { if(tr[now].tag!=-1) { tr[lid].mx=tr[rid].mx=tr[now].tag+tr[now].lazy; tr[lid].tag=tr[rid].tag=tr[now].tag+tr[now].lazy; tr[lid].lazy=tr[rid].lazy=0;//在区间覆盖时,子树的区间修改懒标记要清零!! tr[now].lazy=0; tr[now].tag=-1; } if(tr[now].lazy) { tr[lid].lazy+=tr[now].lazy; tr[rid].lazy+=tr[now].lazy; tr[lid].mx+=tr[now].lazy; tr[rid].mx+=tr[now].lazy; tr[now].lazy=0; } }
最难绷的是,第一遍写,调完交上去只过了第 11 个点,
第二遍写,调完交上去只有第 11 个点没过。
原来是有个数据要 change_path(2,2,5)
,由于某些神秘特性,第一次写的 屏蔽了此操作,第二次写的却完美踩坑。
因此我加了特判:
if(c[1]=='d') { int u,v,w;cin>>u>>v>>w; if(u==v) continue;//这里 st.change_path(u,v,w); }
最终 AC 了。
#include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+2; struct node{ int to,next,w; }edge[N<<1]; int head[N],cnt;int n,Val[N]; void add(int u,int v,int w) { edge[++cnt].next=head[u]; edge[cnt].to=v; edge[cnt].w=w; head[u]=cnt; } int siz[N],son[N],dep[N],f[N]; void dfs1(int u) { siz[u]=1; for(int i=head[u];i;i=edge[i].next) { int v=edge[i].to,w=edge[i].w; if(!siz[v]) { dep[v]=dep[u]+1,f[v]=u; Val[v]=w;//边权下放 dfs1(v); siz[u]+=siz[v]; if(siz[son[u]]<siz[v]) son[u]=v; } } } int top[N],id[N],tim,nw[N]; void dfs2(int u,int t) { id[u]=++tim,top[u]=t,nw[tim]=u; if(!son[u]) return ; dfs2(son[u],t); for(int i=head[u];i;i=edge[i].next) { int v=edge[i].to; if(v==f[u]||v==son[u]) continue; dfs2(v,v); } } class wmw{ private: struct ndoee{ int tag,lazy,mx; }tr[N<<2]; public: #define lid now<<1 #define rid now<<1|1 void build(int now,int l,int r) { tr[now].tag=-1; if(l==r) { tr[now].mx=Val[nw[l]];return ; } int mid=(l+r)>>1; build(lid,l,mid),build(rid,mid+1,r); tr[now].mx=max(tr[lid].mx,tr[rid].mx); } void pushdown(int now,int l,int r) { if(tr[now].tag!=-1) { tr[lid].mx=tr[rid].mx=tr[now].tag+tr[now].lazy; tr[lid].tag=tr[rid].tag=tr[now].tag+tr[now].lazy; tr[lid].lazy=tr[rid].lazy=0; tr[now].lazy=0; tr[now].tag=-1; } if(tr[now].lazy) { tr[lid].lazy+=tr[now].lazy; tr[rid].lazy+=tr[now].lazy; tr[lid].mx+=tr[now].lazy; tr[rid].mx+=tr[now].lazy; tr[now].lazy=0; } } void cover(int now,int l,int r,int x,int y,int k) { if(x<=l&&r<=y) { tr[now].lazy=0; tr[now].tag=k,tr[now].mx=k;return ; } int mid=(l+r)>>1; pushdown(now,l,r); if(x<=mid) cover(lid,l,mid,x,y,k); if(y>mid) cover(rid,mid+1,r,x,y,k); tr[now].mx=max(tr[lid].mx,tr[rid].mx); } void change(int now,int l,int r,int x,int y,int k) { if(x<=l&&r<=y) { tr[now].lazy+=k,tr[now].mx+=k;return ; } int mid=(l+r)>>1; pushdown(now,l,r); if(x<=mid) change(lid,l,mid,x,y,k); if(y>mid) change(rid,mid+1,r,x,y,k); tr[now].mx=max(tr[lid].mx,tr[rid].mx); } void cover_path(int u,int v,int k) { while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]]) swap(u,v); cover(1,1,n,id[top[u]],id[u],k); u=f[top[u]]; } if(dep[u]>dep[v]) swap(u,v); cover(1,1,n,id[u]+1,id[v],k); } void change_path(int u,int v,int k) { while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]]) swap(u,v); change(1,1,n,id[top[u]],id[u],k); u=f[top[u]]; } if(dep[u]>dep[v]) swap(u,v); change(1,1,n,id[u]+1,id[v],k); } int query(int now,int l,int r,int x,int y) { if(x<=l&&r<=y) { return tr[now].mx; } int mid=(l+r)>>1,res=-1145141919; pushdown(now,l,r); if(x<=mid) res=max(res,query(lid,l,mid,x,y)); if(y>mid) res=max(res,query(rid,mid+1,r,x,y)); return res; } int query_path(int u,int v) { int res=-1145141919; while(top[u]!=top[v]) { if(dep[top[u]]<dep[top[v]]) swap(u,v); res=max(res,query(1,1,n,id[top[u]],id[u])); u=f[top[u]]; } if(dep[u]>dep[v]) swap(u,v); res=max(res,query(1,1,n,id[u]+1,id[v])); return res; } }st; signed main() { cin>>n; for(int i=1;i<n;i++) { int u,v,w; cin>>u>>v>>w; add(u,v,w),add(v,u,w); } dfs1(1); // printf("ced\n"); dfs2(1,1); // printf("ced\n"); st.build(1,1,n); while(191981) { // cout<<st.query_path(2,1)<<endl; string c; cin>>c; if(c[1]=='d') { int u,v,w;cin>>u>>v>>w; if(u==v) continue;//特判 st.change_path(u,v,w); } else if(c[2]=='v') { int u,v,w; cin>>u>>v>>w; st.cover_path(u,v,w); } else if(c[2]=='a') { int u,v; cin>>u>>v; u=dep[edge[u*2-1].to]<dep[edge[u*2].to]?edge[u*2].to:edge[u*2-1].to;//神奇的链式前向星边权下放 // cout<<u<<endl; st.cover(1,1,n,id[u],id[u],v); } else if(c[2]=='x') { int u,v;cin>>u>>v; cout<<st.query_path(u,v)<<endl; } else return 0; } }
例题 10:P3128 [USACO15DEC] Max Flow P
这道题很简单。
不需要建树,每次操作树链加,维护最大值。
最后输出 即可。
的操作如下,比较神秘:
void pushdown(int now,int l,int r) { if(!tr[now].lazy) return ; tr[lid].lazy+=tr[now].lazy; tr[rid].lazy+=tr[now].lazy; tr[lid].mx+=tr[now].lazy; tr[rid].mx+=tr[now].lazy; tr[now].lazy=0; }
例题 11:CF343D Water Tree
看起来很板,但是某两人同时挂在这个题上。
问题在于区间覆盖上。
观察到询问中有一个操作叫
将点 到 的路径上的所有节点的权值改为 。
在线段树的 中我们有这样的语句:
void pushdown(int now,int l,int r) { if(!tr[now].lazy) return ; int mid=(l+r)>>1; Add(lid,l,mid,tr[now].lazy); Add(rid,mid+1,r,tr[now].lazy); tr[now].lazy=0; }
可以看到,如果当前点的 ,那么不会进行后面的下放操作。但是在本题中, 是具有意义的。
意思是,执行上面说的那条操作时,节点的 就是 ,不会下放,而是保留原来的值。解决的方法很简单,把 的默认值设成 即可(可以参考 月下毛景树 那个题)
修改完是这样的:
void pushdown(int now,int l,int r) { if(tr[now].lazy==-1) return ; int mid=(l+r)>>1; Add(lid,l,mid,tr[now].lazy); Add(rid,mid+1,r,tr[now].lazy); tr[now].lazy=-1; }
完美解决。
例题 12:CF916E Jamie and Tree
这个就很有意思了。
换根 + 树剖。
树剖部分很板,非常板,甚至不需要写树链操作的代码。
接下来就要考虑换根了:
考虑到修改是找 和 在当前根意义下的 lca,而查询是直接查原始根意义下的 lca。
什么意思呢?
看这张图,如果当前 ,就是图三,点 和 的 lca 就是 。
但是原始图(你剖的那棵树)中,lca 是 。
这就是原始根意义下的 lca 和当前根意义下的 lca 的区别。
所以在修改操作中,操作的子树应是当前根意义下的 lca。
后续叙述中,我们使用 getlca()
表示查找当前根意义下的 lca,lca()
表示查找原始根意义下的 lca。
说清楚了这点,我们接着来讨论当前根分别对修改和查询操作的影响:
对于修改而言:
-
当前根与求得的当前根意义下的 lca 是同一个点,直接修改全部;
-
当前根不在查找出的 lca 子树以内,即 ( 表示查找出的 lca),对修改无影响;
-
当前根在 lca 子树以内,即 ,需要用到容斥:
这时候 的子树是整棵树中除去以 的一个子节点为根,并且 在这个子节点的子树的子树部分,用容斥一搞就好了。
具体地,这样找这个子树:
int find(int u,int v) { int fx=top[u],fy=top[v]; while(fx!=fy) { if(dep[fx]<dep[fy]) swap(u,v),swap(fx,fy); if(fa[fx]==v) return top[u]; u=fa[u],fx=top[u]; } if(dep[u]>dep[v]) swap(u,v); return son[u]; }
对于查找而言:
-
当前根与询问节点是同一个点,直接查全部;
-
当前根不在询问节点以内,即 ,对查找无影响;
-
当前根在询问节点以内,即 ,也是容斥一下就好。
整体的核心代码:
while(q--) { cin>>op>>u; if(op==1) { root=u; } else if(op==2) { cin>>v>>w; int rt=getlca(u,v); if(rt==root) st.update(1,1,n,1,n,w); else if(lca(root,rt)!=rt) st.update(1,1,n,id[rt],id[rt]+siz[rt]-1,w); else { int ck=find(rt,root); st.update(1,1,n,1,n,w),st.update(1,1,n,id[ck],id[ck]+siz[ck]-1,-w);//容斥 } } else { if(u==root) cout<<st.query(1,1,n,1,n); else if(lca(u,root)!=u) cout<<st.query(1,1,n,id[u],id[u]+siz[u]-1); else { int ck=find(u,root); cout<<st.query(1,1,n,1,n)-st.query(1,1,n,id[ck],id[ck]+siz[ck]-1);//容斥 } cout<<"\n"; } }
例题 13:P9432 [NAPC-#1] rStage5 - Hard Conveyors
一道好题。
考虑最优路径一定是原路径上每个点到最近的关键点的最小值。
那我们可以预处理出所有点距离它最近的关键点的距离,拿线段树维护区间最小值。
然后原路径长度拿 BIT 维护区间和。
具体来说,我们用树形 dp 的思想来处理每个点距离它最近的关键点的距离。
-
如果 是关键点,那么 ;
-
否则, 的答案只能从 和 更新,即 。
那我们分两次 ,第一次自下而上维护,第二次自上而下维护就好了。(不知到正确性,但是是对的?)
void dfsdown(int u,int f) { for(int i=head[u];i;i=e[i].next) { int v=e[i].to; if(v==f) continue; dfsdown(v,u); dp[u]=min(dp[u],dp[v]+e[i].w); } } void dfsup(int u,int f) { for(int i=head[u];i;i=e[i].next) { int v=e[i].to; if(v==f) continue; dp[v]=min(dp[v],dp[u]+e[i].w); dfsup(v,u); } }
据说这题有 难度,但是被我 15 分钟切了?
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验