洛谷·[模板]树链剖分

写在前面

首先,在学树链剖分之前最好先把 LCA、树形DP、DFS序 这三个知识点学了

emm还有必备的 链式前向星、线段树 也要先学了。

如果这三个知识点没掌握好的话,树链剖分难以理解也是当然的。


树链剖分

树链剖分 就是对一棵树分成几条链,把树形变为线性,减少处理难度

需要处理的问题:

  • 将树从x到y结点最短路径上所有节点的值都加上z
  • 求树从x到y结点最短路径上所有节点的值之和
  • 将以x为根节点的子树内所有节点值都加上z
  • 求以x为根节点的子树内所有节点值之和

目录:

概念

  • 重儿子:对于每一个非叶子节点,它的儿子中 以那个儿子为根的子树节点数最大的儿子 为该节点的重儿子 (Ps: 感谢@shzr大佬指出我此句话的表达不严谨qwq, 已修改)
  • 轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子
  • 叶子节点没有重儿子也没有轻儿子(因为它没有儿子。。)
  • 重边:一个父亲连接他的重儿子的边称为重边 //原写法:连接任意两个重儿子的边叫做重边
  • 轻边:剩下的即为轻边
  • 重链:相邻重边连起来的 连接一条重儿子 的链叫重链
    • 对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链
    • 每一条重链以轻儿子为起点

dfs1()

这个dfs要处理几件事情:

  • 标记每个点的深度dep[]
  • 标记每个点的父亲fa[]
  • 标记每个非叶子节点的子树大小(含它自己)
  • 标记每个非叶子节点的重儿子编号son[]
inline void dfs1(int x,int f,int deep){//x当前节点,f父亲,deep深度
dep[x]=deep;//标记每个点的深度
fa[x]=f;//标记每个点的父亲
siz[x]=1;//标记每个非叶子节点的子树大小
int maxson=-1;//记录重儿子的儿子数
for(Rint i=beg[x];i;i=nex[i]){
int y=to[i];
if(y==f)continue;//若为父亲则continue
dfs1(y,x,deep+1);//dfs其儿子
siz[x]+=siz[y];//把它的儿子数加到它身上
if(siz[y]>maxson)son[x]=y,maxson=siz[y];//标记每个非叶子节点的重儿子编号
}
}//变量解释见最下面

dfs2()

这个dfs2也要预处理几件事情

  • 标记每个点的新编号
  • 赋值每个点的初始值到新编号上
  • 处理每个点所在链的顶端
  • 处理每条链

顺序:先处理重儿子再处理轻儿子,理由后面说

inline void dfs2(int x,int topf){//x当前节点,topf当前链的最顶端的节点
id[x]=++cnt;//标记每个点的新编号
wt[cnt]=w[x];//把每个点的初始值赋到新编号上来
top[x]=topf;//这个点所在链的顶端
if(!son[x])return;//如果没有儿子则返回
dfs2(son[x],topf);//按先处理重儿子,再处理轻儿子的顺序递归处理
for(Rint i=beg[x];i;i=nex[i]){
int y=to[i];
if(y==fa[x]||y==son[x])continue;
dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链
}
}//变量解释见最下面

处理问题

Attention 重要的来了!!!

前面说到dfs2的顺序是先处理重儿子再处理轻儿子

我们来模拟一下:

  • 因为顺序是先重再轻,所以每一条重链的新编号是连续的
  • 因为是dfs,所以每一个子树的新编号也是连续的

现在回顾一下我们要处理的问题

  • 处理任意两点间路径上的点权和
  • 处理一点及其子树的点权和
  • 修改任意两点间路径上的点权
  • 修改一点及其子树的点权

1、当我们要处理任意两点间路径时:

设所在链顶端的深度更深的那个点为x点

  • ans加上x点到x所在链顶端 这一段区间的点权和
  • 把x跳到x所在链顶端的那个点的上面一个点

不停执行这两个步骤,直到两个点处于一条链上,这时再加上此时两个点的区间和即可

这时我们注意到,我们所要处理的所有区间均为连续编号(新编号),于是想到线段树,用线段树处理连续编号区间和

每次查询时间复杂度为\(O( \log^2n)\)

inline int qRange(int x,int y){
int ans=0;
while(top[x]!=top[y]){//当两个点不在同一条链上
if(dep[top[x]]<dep[top[y]])swap(x,y);//把x点改为所在链顶端的深度更深的那个点
res=0;
query(1,1,n,id[top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
ans+=res;
ans%=mod;//按题意取模
x=fa[top[x]];//把x跳到x所在链顶端的那个点的上面一个点
}
//直到两个点处于一条链上
if(dep[x]>dep[y])swap(x,y);//把x点深度更深的那个点
res=0;
query(1,1,n,id[x],id[y]);//这时再加上此时两个点的区间和即可
ans+=res;
return ans%mod;
}//变量解释见最下面

2、处理一点及其子树的点权和:

想到记录了每个非叶子节点的子树大小(含它自己),并且每个子树的新编号都是连续的

于是直接线段树区间查询即可

时间复杂度为\(O( \log n)\)

inline int qSon(int x){
res=0;
query(1,1,n,id[x],id[x]+siz[x]-1);//子树区间右端点为id[x]+siz[x]-1
return res;
}

当然,区间修改就和区间查询一样的啦~~

inline void updRange(int x,int y,int k){
k%=mod;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
update(1,1,n,id[top[x]],id[x],k);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
update(1,1,n,id[x],id[y],k);
} inline void updSon(int x,int k){
update(1,1,n,id[x],id[x]+siz[x]-1,k);
}//变量解释见最下面

建树

既然前面说到要用线段树,那么按题意建树就可以啦!

不过,建树这一步当然是在处理问题之前哦~

AC代码:

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdio>
#define Rint register int
#define mem(a,b) memset(a,(b),sizeof(a))
#define Temp template<typename T>
using namespace std;
typedef long long LL;
Temp inline void read(T &x){
x=0;T w=1,ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')w=-1,ch=getchar();
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^'0'),ch=getchar();
x=x*w;
} #define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
#define len (r-l+1) const int maxn=200000+10;
int n,m,r,mod;
//见题意
int e,beg[maxn],nex[maxn],to[maxn],w[maxn],wt[maxn];
//链式前向星数组,w[]、wt[]初始点权数组
int a[maxn<<2],laz[maxn<<2];
//线段树数组、lazy操作
int son[maxn],id[maxn],fa[maxn],cnt,dep[maxn],siz[maxn],top[maxn];
//son[]重儿子编号,id[]新编号,fa[]父亲节点,cnt dfs_clock/dfs序,dep[]深度,siz[]子树大小,top[]当前链顶端节点
int res=0;
//查询答案 inline void add(int x,int y){//链式前向星加边
to[++e]=y;
nex[e]=beg[x];
beg[x]=e;
}
//-------------------------------------- 以下为线段树
inline void pushdown(int rt,int lenn){
laz[rt<<1]+=laz[rt];
laz[rt<<1|1]+=laz[rt];
a[rt<<1]+=laz[rt]*(lenn-(lenn>>1));
a[rt<<1|1]+=laz[rt]*(lenn>>1);
a[rt<<1]%=mod;
a[rt<<1|1]%=mod;
laz[rt]=0;
} inline void build(int rt,int l,int r){
if(l==r){
a[rt]=wt[l];
if(a[rt]>mod)a[rt]%=mod;
return;
}
build(lson);
build(rson);
a[rt]=(a[rt<<1]+a[rt<<1|1])%mod;
} inline void query(int rt,int l,int r,int L,int R){
if(L<=l&&r<=R){res+=a[rt];res%=mod;return;}
else{
if(laz[rt])pushdown(rt,len);
if(L<=mid)query(lson,L,R);
if(R>mid)query(rson,L,R);
}
} inline void update(int rt,int l,int r,int L,int R,int k){
if(L<=l&&r<=R){
laz[rt]+=k;
a[rt]+=k*len;
}
else{
if(laz[rt])pushdown(rt,len);
if(L<=mid)update(lson,L,R,k);
if(R>mid)update(rson,L,R,k);
a[rt]=(a[rt<<1]+a[rt<<1|1])%mod;
}
}
//---------------------------------以上为线段树
inline int qRange(int x,int y){
int ans=0;
while(top[x]!=top[y]){//当两个点不在同一条链上
if(dep[top[x]]<dep[top[y]])swap(x,y);//把x点改为所在链顶端的深度更深的那个点
res=0;
query(1,1,n,id[top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
ans+=res;
ans%=mod;//按题意取模
x=fa[top[x]];//把x跳到x所在链顶端的那个点的上面一个点
}
//直到两个点处于一条链上
if(dep[x]>dep[y])swap(x,y);//把x点深度更深的那个点
res=0;
query(1,1,n,id[x],id[y]);//这时再加上此时两个点的区间和即可
ans+=res;
return ans%mod;
} inline void updRange(int x,int y,int k){//同上
k%=mod;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]])swap(x,y);
update(1,1,n,id[top[x]],id[x],k);
x=fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
update(1,1,n,id[x],id[y],k);
} inline int qSon(int x){
res=0;
query(1,1,n,id[x],id[x]+siz[x]-1);//子树区间右端点为id[x]+siz[x]-1
return res;
} inline void updSon(int x,int k){//同上
update(1,1,n,id[x],id[x]+siz[x]-1,k);
} inline void dfs1(int x,int f,int deep){//x当前节点,f父亲,deep深度
dep[x]=deep;//标记每个点的深度
fa[x]=f;//标记每个点的父亲
siz[x]=1;//标记每个非叶子节点的子树大小
int maxson=-1;//记录重儿子的儿子数
for(Rint i=beg[x];i;i=nex[i]){
int y=to[i];
if(y==f)continue;//若为父亲则continue
dfs1(y,x,deep+1);//dfs其儿子
siz[x]+=siz[y];//把它的儿子数加到它身上
if(siz[y]>maxson)son[x]=y,maxson=siz[y];//标记每个非叶子节点的重儿子编号
}
} inline void dfs2(int x,int topf){//x当前节点,topf当前链的最顶端的节点
id[x]=++cnt;//标记每个点的新编号
wt[cnt]=w[x];//把每个点的初始值赋到新编号上来
top[x]=topf;//这个点所在链的顶端
if(!son[x])return;//如果没有儿子则返回
dfs2(son[x],topf);//按先处理重儿子,再处理轻儿子的顺序递归处理
for(Rint i=beg[x];i;i=nex[i]){
int y=to[i];
if(y==fa[x]||y==son[x])continue;
dfs2(y,y);//对于每一个轻儿子都有一条从它自己开始的链
}
} int main(){
read(n);read(m);read(r);read(mod);
for(Rint i=1;i<=n;i++)read(w[i]);
for(Rint i=1;i<n;i++){
int a,b;
read(a);read(b);
add(a,b);add(b,a);
}
dfs1(r,0,1);
dfs2(r,r);
build(1,1,n);
while(m--){
int k,x,y,z;
read(k);
if(k==1){
read(x);read(y);read(z);
updRange(x,y,z);
}
else if(k==2){
read(x);read(y);
printf("%d\n",qRange(x,y));
}
else if(k==3){
read(x);read(y);
updSon(x,y);
}
else{
read(x);
printf("%d\n",qSon(x));
}
}
}

树链剖分详解(洛谷模板 P3384)的更多相关文章

  1. P3384 【模板】树链剖分 题解&&树链剖分详解

    题外话: 一道至今为止做题时间最长的题: begin at 8.30A.M 然后求助_yjk dalao后 最后一次搞取模: awsl. 正解开始: 题目链接. 树链剖分,指的是将一棵树通过两次遍历后 ...

  2. 树链剖分详解&题解 P6098 【[USACO19FEB]Cow Land G】

    看到各位大佬们已经把其他的东西讲的很明白了,我这个 juruo 就讲一讲最基本的树链剖分吧. 0.树剖是什么?能吃吗? 不能吃 树剖是树链剖分的简称,我们一般说的树剖其实指重链剖分.当然,还有一种长链 ...

  3. 绝对是全网最好的Splay 入门详解——洛谷P3369&BZOJ3224: Tyvj 1728 普通平衡树 包教包会

    平衡树是什么东西想必我就不用说太多了吧. 百度百科: 一个月之前的某天晚上,yuli巨佬为我们初步讲解了Splay,当时接触到了平衡树里的旋转等各种骚操作,感觉非常厉害.而第二天我调Splay的模板竟 ...

  4. 差分约束详解&&洛谷SCOI2011糖果题解

    差分约束系统: 如果一个系统由n个变量和m个约束条件组成,形成m个形如ai-aj≤k的不等式(i,j∈[1,n],k为常数),则称其为差分约束系统(system of difference const ...

  5. 洛谷 P3384 【模板】树链剖分

    树链剖分 将一棵树的每个节点到它所有子节点中子树和(所包含的点的个数)最大的那个子节点的这条边标记为"重边". 将其他的边标记为"轻边". 若果一个非根节点的子 ...

  6. 【算法学习】【洛谷】树链剖分 & P3384 【模板】树链剖分 P2146 软件包管理器

    刚学的好玩算法,AC2题,非常开心. 其实很早就有教过,以前以为很难就没有学,现在发现其实很简单也很有用. 更重要的是我很好调试,两题都是几乎一遍过的. 介绍树链剖分前,先确保已经学会以下基本技巧: ...

  7. 洛谷 P3384 【模板】树链剖分-树链剖分(点权)(路径节点更新、路径求和、子树节点更新、子树求和)模板-备注结合一下以前写的题目,懒得写很详细的注释

    P3384 [模板]树链剖分 题目描述 如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作: 操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节 ...

  8. 洛谷p3384【模板】树链剖分题解

    洛谷p3384 [模板]树链剖分错误记录 首先感谢\(lfd\)在课上调了出来\(Orz\) \(1\).以后少写全局变量 \(2\).线段树递归的时候最好把左右区间一起传 \(3\).写\(dfs\ ...

  9. 『题解』洛谷P3384 【模板】树链剖分

    Problem Portal Portal1: Luogu Description 如题,已知一棵包含\(N\)个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作: 操作\(1\): ...

随机推荐

  1. 《JAVA程序设计》_第三周学习总结

    20175217吴一凡 一.IDEA学生免费版申请后续 收到这个邮件,就说明你申请成功了,点这里进去就行了 点击接受 在下一个界面登录你之前注册的账号绑定许可证就行了,重新登录你的账号就有了一年的许可 ...

  2. 详解Linux双网卡绑定之bond0

    1.什么是bond? 网卡bond是通过多张网卡绑定为一个逻辑网卡,实现本地网卡的冗余,带宽扩容和负载均衡,在生产场景中是一种常用的技术.Kernels 2.4.12及以后的版本均供bonding模块 ...

  3. koa2源码解读及实现一个简单的koa2框架

    阅读目录 一:封装node http server. 创建koa类构造函数. 二:构造request.response.及 context 对象. 三:中间件机制的实现. 四:错误捕获和错误处理. k ...

  4. Linux内存管理 (23)一个内存Oops解析

    专题:Linux内存管理专题 关键词:DataAbort.fsr.pte.backtrace.stack.   在内存相关实际应用中,内存异常访问是一种常见的问题. 本文结合异常T32栈回溯.Oops ...

  5. openstack网络基础:网络叠加模式VLAN、VxLAN、GRE

    什么是叠加网络1.一个数据包(或帧)封装在另一个数据包内;被封装的包转发到隧道端点后再被拆装.2.叠加网络就是使用这种所谓“包内之包”的技术安全地将一个网络隐藏在另一个 网络中,然后将网络区段进行迁移 ...

  6. C#while死循环时候cpu占用比例大

    C#while死循环时候cpu占用比例大 原因:线程等不到释放,windows运行模式是抢占资源 解决方法:休眠一毫秒让垃圾回收可以进来回收资源 while (true) { Thread.Sleep ...

  7. PHP之pear包总结

    现在我们开发的时候,尤其是使用框架进行项目开发的时候,都会有一个专门的包管理工具,对,那就是composer,使用这个工具可以简单快速的引入一个代码包,十分快捷好用.接下就总结一下,经常使用到的pea ...

  8. 题解 P1601 【A+B Problem(高精)】

    P1601 A+B Problem(高精) 题目描述 高精度加法,x相当于a+b problem,b不用考虑负数. 输入输出格式 输入格式: 分两行输入a,b<=10^500 输出格式: 输出只 ...

  9. 数据交换格式与SpringIOC底层实现

    1.数据交换格式 1.1 有哪些数据交换格式 客户端与服务器常用数据交换格式xml.json.html 1.2 数据交换格式应用场景 1.2.1 移动端(安卓.iOS)通讯方式采用http协议+JSO ...

  10. CentOS下添加Root权限用户(超级用户)方法

    1.添加普通用户[root@server ~]# useradd chenjiafa   //添加一个名为chenjiafa的用户[root@server ~]# passwd chenjiafa   ...