重链剖分学习笔记

news/2024/11/7 18:11:30/文章来源:https://www.cnblogs.com/mountzhu/p/18530948

一、引入

学习一个新的数据结构或者算法前,我们都要了解其用途,毕竟用途才是其被发明出来的原因。那么树链剖分有什么用呢?它维护的是什么样的信息呢?这里我们只探讨重链剖分。

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。

具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。

​ ——OI-WIKI

不知道大家在平时做题的时候会不会遇到这样一类问题:

  1. 将树从 \(x\)\(y\) 结点的路径上所有节点的值都加上 \(z\) (树上路径加)。

    其实可以树上差分

  2. 求树从 \(x\)\(y\) 结点最短路径上所有节点的值之和(树上路径和)。

    其实可以暴力,但是复杂度会炸

欸你有没有发现这种问题如果在序列或者线段上我们是不是很好做,直接线段树/树状数组。

但是在树上就不知道怎么解决了。

总结一下:

  1. 修改 树上两点之间的路径上 所有点的值。

  2. 查询 树上两点之间的路径上 节点权值的 和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)

    ​ ——OI-WIKI

这类问题,我们就利用树剖转化为序列问题来处理。

二、结构&声明

研究一个数据结构或算法,我们也要追问自己为什么它是这样设计的,是根据什么性质,亦或是和什么有关联。上面我们说了我们面临着那样的问题才发明了重链剖分。那么我们思考一下怎么才能实现将树上问题转化成序列。

一种直接的想法是不是就是将它暴力分成很多条链,如下图。

image

可是这种剖分方法会有重复节点,我们不好处理,所以我们要找到一种方法,将树不重不漏地剖成一些链,然后维护。于是重链剖分的方法就出现了。

为什么要将树分成重儿子、轻儿子,为什么要将其剖成重链呢(事实上,根据维护的问题不同,还有长链剖分、实链剖分等,但是这里只讨论重链剖分)?思考我们要剖成链,就要有一个优先,其他靠后,根据这个优先顺序来维护。和树上dsu相似的,一个结点的重儿子是最重要的,优先访问的。
如下图(来自OI-WIKI)
image

一些声明(在有根树中):

名词/变量 含义 名词/变量 含义
重儿子 所有儿子中子树大小最大的儿子 轻儿子 除了重儿子以外的其他儿子都是轻儿子
重边 从这个节点连到重子节点的边 轻边 从这个节点到其它子节点的边
重链 几条重边首尾相接得到的链,特别的,落单的节点也是重链 \(top[N]\) 存储这条重链的顶端
\(val[N]\) 剖分以后对应 \(dfn\) 序的点权 \(sz[N]\) 子树大小
\(s[N]\) 每个结点的重儿子 \(f[N]\) 每个结点的父节点

看不懂的先去看后面,这里只是给出参照,后面有解释。

其实看代码就行,都是很正常的变量名和定义。

三、重链剖分的原理和实现

先来谈谈重链剖分的性质:

  1. 树上每个节点都属于且仅属于一条重链
  2. 所有的重链将整棵树完全剖分
  3. 在剖分时重边优先遍历

这三条算是重申。

  1. 由 3 得 最后树的 DFS 序上,重链内的 DFS 序是连续的。按 \(dfn\) 排序后的序列即为剖分后的链

这一句是重点,揭示了原理。

  1. 当我们向下经过一条轻边时,所在子树的大小至少会除以二,即如果 \((u,v)\) 是一条轻边,那么 \(sz[v]<sz[u]/2\)。所以对于树上的任意一条路径,把它拆分成从 LCA 分别向两边往下走,分别最多走 \(O(\log n)\),因此,树上的每条路径都可以被拆分成不超过 \(O(\log n)\) 条重链。即从根结点到任意结点的路所经过的轻重链的个数必定都小于 \(O(\log n)\)

这一点通常用于时间复杂度证明。

那么我们怎么实现这一点呢?
首先我们要得到上面那些数组的值,这部分可以用两个 \(dfs\) 预处理。
\(dfs1\) 处理 \(sz\)\(f\)\(s\)\(dep\) 这四个数组。没什么好说的。

inline void init1(int nw,int fa){f[nw]=fa;dep[nw]=dep[fa]+1,sz[nw]=1;for(int i=hd[nw];i;i=e[i].nxt){if(fa!=e[i].to){init1(e[i].to,nw);sz[nw]+=sz[e[i].to];if(sz[e[i].to]>sz[s[nw]]) s[nw]=e[i].to;}}
}

\(dfs2\) 将各个重结点连成重链,处理出 \(dfn\),(因为上一次 \(dfs1\) 以后才知道重儿子,才能确定访问顺序),以及当前节点所在链的起点(\(top\)),还有对应 \(dfn\) 上的权值。

inline void init2(int nw,int fa){dfn[nw]=++idx;val[idx]=v[nw];top[nw]=fa;if(!s[nw]) return;init2(s[nw],fa);for(int i=hd[nw];i;i=e[i].nxt){if(e[i].to!=f[nw]&&e[i].to!=s[nw]) init2(e[i].to,e[i].to);//轻链的开头是自己}
}

比较难的就是我们怎么把这些信息整合得到我们所需要的,在下面具体运用时讲吧。
将重链(对应 \(dfn\) 一段区间)用树状数组/线段树来进行维护(这里就选择线段树了,因为不需要动脑子
可以看看 OI-WIKI 的伪代码。

四、应用

1. 求LCA

求LCA有倍增法,是在暴力跳父亲的方法上进行了优化,那么我们来考虑树剖如何优化跳父亲。
既然树被我们剖成了若干链,那LCA就有两种情况:1.同一链上两个节点求LCA,2.不同链上求LCA
第一种显然是深度较浅的,判断方法就是看top
第二种不同链则一定是LCA分叉,分成了重链和轻链,我们不是记了top吗,可以用top直接跳过一条链来加速。
但是一个问题是可能会跳过LCA,比如一个点在另一个点分叉的上方,如果无脑跳就跳过去了。我们要仔细考虑一下如何跳top。
考虑我们的目的就是让两个点跳到同一条链上,所以每次选择top深度较深的那个点跳,如果在同一条链上就结束啦。
可以证明单次查询时间复杂度 \(O(\log n)\)
注意每次跳一整个链,所以跳到 \(f[top[v]]\)
理解 lca 是理解后面路径查修的基础,要看动态演示可以去看董晓算法的图(可能不用)

inline int lca(int u,int v){while(top[u]!=top[v]){if(dep[top[u]]>dep[top[v]]) swap(u,v);v=f[top[v]];}return dep[u]<dep[v]?u:v;
}

2. 子树修改查询。

这个和正常的一样,因为子树内 \(dfn\) 连续,所以记录子树内最大最小的 \(dfn\) ,其实就是正常线段树维护 \(dfn[u]\sim dfn[u+sz[u]-1]\) 这个区间即可。

	Modify(1,dfn[u],dfn[u]+sz[u]-1,x);Query(1,dfn[u],dfn[u]+sz[u]-1)%mod);

3. 路径修改查询。

树上路径 \((u,v) \rightarrow (u,lca)+(lca,v)\),剖成重链以后我们可以将其分为两段:向上跳 \(lca\) 的一段和最后较深的点向较浅的点靠。由于前面说过,通过重链剖分的形式保证了一条链上的 \(dfn\) 序是连续的,所以这两段我们可以将其放到线段树上,在 \(val\) 数组中求区间和。过程中的范围是 \(dfn[{top_u}]\sim dfn[u]\) 设跳到最后较深的点是 \(ed\),则范围是 \(dfn[lca]\sim dfn[ed]\)

inline long long Path_qry(int u,int v){long long ans=0;while(top[u]!=top[v]){if(dep[top[u]]>dep[top[v]])swap(u,v);ans=(ans+Query(1,dfn[top[v]],dfn[v]))%mod;v=f[top[v]]; }if(dep[u]>dep[v])swap(u,v);ans=(ans+Query(1,dfn[u],dfn[v]))%mod;return ans;
}inline void Path_mdf(int u,int v,int k){while(top[u]!=top[v]){if(dep[top[u]]>dep[top[v]]) swap(u,v);Modify(1,dfn[top[v]],dfn[v],k);v=f[top[v]]; }if(dep[u]>dep[v])swap(u,v);Modify(1,dfn[u],dfn[v],k);return;
}

所有程序

#include<bits/stdc++.h>
#define ll long long
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int N=1e5+5;
int n,m,rt,q,mod;
struct E{int to,nxt;
}e[N<<1];int tot,hd[N];
int idx,f[N],v[N],val[N],dep[N];
struct Tr{int l,r;ll val,tag;
}tr[N<<2];
int sz[N],s[N],dfn[N],top[N];
inline int read(){char ch;int x=0,f=1;while(!isdigit(ch=getchar())){if(ch=='-') f=-1;}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch-'0');ch=getchar();}return x*f;
}
inline void Build(const int p,const int l,const int r){tr[p].l=l,tr[p].r=r,tr[p].val=val[r];if(l==r){tr[p].val=val[l]%mod;return;}int mid=(l+r)>>1;Build(ls,l,mid);Build(rs,mid+1,r);tr[p].val=(tr[ls].val+tr[rs].val)%mod;return;
}
inline void Add(int u,int v){++tot,e[tot].to=v;e[tot].nxt=hd[u],hd[u]=tot;
}
inline void init1(int nw,int fa){f[nw]=fa;dep[nw]=dep[fa]+1,sz[nw]=1;for(int i=hd[nw];i;i=e[i].nxt){if(fa!=e[i].to){init1(e[i].to,nw);sz[nw]+=sz[e[i].to];if(sz[e[i].to]>sz[s[nw]]) s[nw]=e[i].to;}}
}
inline void init2(int nw,int fa){dfn[nw]=++idx;val[idx]=v[nw];top[nw]=fa;if(!s[nw]) return;init2(s[nw],fa);for(int i=hd[nw];i;i=e[i].nxt){if(e[i].to!=f[nw]&&e[i].to!=s[nw]){init2(e[i].to,e[i].to);//轻链的开头是自己}}
}
inline int lca(int u,int v){while(top[u]!=top[v]){if(dep[top[u]]>dep[top[v]]) swap(u,v);v=f[top[v]];}return dep[u]<dep[v]?u:v;
}
inline void Change(const int p,const int val){tr[p].val=(tr[p].val+val*(tr[p].r-tr[p].l+1)%mod)%mod;tr[p].tag=(tr[p].tag+val)%mod;return;
}
inline void Pushdown(const int p){if(!tr[p].tag) return;Change(ls,tr[p].tag);Change(rs,tr[p].tag);tr[p].tag=0;return;
}
inline long long Query(const int p,const int l,const int r){if(tr[p].l>=l&&tr[p].r<=r) return tr[p].val;Pushdown(p);int mid=(tr[p].l+tr[p].r)>>1;ll ans=0;if(l<=mid) ans+=Query(ls,l,r);if(mid<r) ans=(ans+Query(rs,l,r))%mod;return ans;
}
inline long long Path_qry(int u,int v){long long ans=0;while(top[u]!=top[v]){if(dep[top[u]]>dep[top[v]])swap(u,v);ans=(ans+Query(1,dfn[top[v]],dfn[v]))%mod;v=f[top[v]]; }if(dep[u]>dep[v])swap(u,v);ans=(ans+Query(1,dfn[u],dfn[v]))%mod;return ans;
}
inline void Modify(const int p,const int l,const int r,const int k){if(tr[p].l>=l&&tr[p].r<=r){tr[p].val=(tr[p].val+k*(tr[p].r-tr[p].l+1)%mod)%mod;tr[p].tag=(tr[p].tag+k)%mod;return;}Pushdown(p);int mid=(tr[p].l+tr[p].r)>>1;if(l<=mid) Modify(ls,l,r,k);if(mid<r) Modify(rs,l,r,k);tr[p].val=(tr[ls].val+tr[rs].val)%mod;return;
} 
inline void Path_mdf(int u,int v,int k){while(top[u]!=top[v]){if(dep[top[u]]>dep[top[v]]) swap(u,v);Modify(1,dfn[top[v]],dfn[v],k);v=f[top[v]]; }if(dep[u]>dep[v])swap(u,v);Modify(1,dfn[u],dfn[v],k);return;
}
int main(){n=read(),m=read(),rt=read(),mod=read(),q=read();for(int i=1;i<=n;++i) v[i]=read();int u,v,x;for(int i=1;i<n;++i){u=read(),v=read();Add(u,v),Add(v,u);}init1(rt,0),init2(rt,rt);Build(1,1,n);while(m--){u=read();if(u==1){u=read(),v=read(),x=read();Path_mdf(u,v,x);}else if(u==2){u=read(),v=read();printf("%lld\n",Path_qry(u,v)%mod);}else if(u==3){u=read(),x=read();Modify(1,dfn[u],dfn[u]+sz[u]-1,x);}else{u=read();printf("%lld\n",Query(1,dfn[u],dfn[u]+sz[u]-1)%mod);}}while(q--){u=read(),v=read();printf("%d\n",lca(u,v));}return 0;
}

五、练习

P3384 【模板】重链剖分/树链剖分
待补

参考 (虽然没有看)

​ 树链剖分 - OI Wiki
​ 树链剖分学习笔记 - l_x_y - 博客园
​ 树链剖分详解 - Ivanovcraft - 博客园
​ 算法学习笔记() DFS序、树链剖分及其应用_dfs序的应用-CSDN博客
​ 树链剖分良心讲解 - 洛谷专栏
​ 树链剖分详解(洛谷模板 P3384) - ChinHhh - 博客园
​ P3384 【模板】重链剖分/树链剖分 - 洛谷
​ 算法学习笔记:树链剖分-CSDN博客
​ 董晓算法 D12 Luogu P3384【模板】轻重链剖分/树链剖分

好多啊
可以看看一些别的类似问题(不是重链剖分),如 K-father and K-son

写完这篇学习笔记的时候正好放学了,郊眠寺正唱到结尾,我又重新拾起来希望,像拾起秋天里一支破败的枯枝。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/828388.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

grafana对redis哨兵模式监控添加主库信息

【环境说明】 redis+sentinel哨兵模式+prometheus+grafana监控面板 【要求】 redis哨兵模式,需要查看主库节点信息,如果发生切换,能查到历史变化信息 【配置操作】 当前已经安装好三个redis数据库跟哨兵模式,并且都安装好了redis-exporter监控服务 prometheus配置监控信息:…

20222327 2024-2025-1 《网络与系统攻防技术》实验四实验报告

一、实验内容 1.恶意代码文件类型标识、脱壳与字符串提取 2.使用IDA Pro静态或动态分析crackme1.exe与crakeme2.exe,寻找特定输入,使其能够输出成功信息。 3.分析一个自制恶意代码样本rada,并撰写报告,回答问题 4.取证分析实践 二、实验过程 1.对恶意代码样本,进行文件类型…

20222327 2024-2025-1 《网络与系统攻防技术》实验四报告

一、实验内容 1.恶意代码文件类型标识、脱壳与字符串提取 2.使用IDA Pro静态或动态分析crackme1.exe与crakeme2.exe,寻找特定输入,使其能够输出成功信息。 3.分析一个自制恶意代码样本rada,并撰写报告,回答问题 4.取证分析实践 二、实验过程 1.对恶意代码样本,进行文件类型…

在Windows上同时运行多个java程序如何区分

显示命令行后通过 jar文件路径区别也可以通过内存大小来判定是哪个程序在服务中也能找到 PID用*.bat来启动 UdpListener8102 效果,会在应用中显示用服务启动则在 后台进程中显示

《地下城与勇士:同人单机版》游戏 —— 经典重现,单人冒险新体验

引言 《地下城与勇士》作为一款经典的多人在线角色扮演游戏(MMORPG),在全球范围内拥有庞大的粉丝群体。对于许多热爱这款游戏的玩家来说,能够体验到一个单人版的《地下城与勇士》无疑是一个令人兴奋的提议。现在,让我们来探索这个由粉丝打造的《地下城与勇士:同人单机版》…

_html

html 一、基本介绍 1、定义:html是一种超文本标记语言,也是一种标识性语言(不是编程语言) 标记:记号(绰号) 超文本:就是页面内容可以包含图片、链接,音乐,视频等素材。 2、为什么学习html? (1)测试页面元素,了解页面页面元素(页面是html语言编写的) (2)进行u…

【semantic Kernel】文转图

SK也对应了文生图的功能,本文中分别使用的是GPT的DALLE 2和DALLE 3来生成图版,虽然不如专业的文生图那么专业,但提示词到位,在一些场景中还是可用的。 下面是最简单的文生图的代码实现,通过TextToImage服务来完成生成,GPT中生成图的尺寸要求是:256x256, 512x512, 1024x1…

docker实现redis集群

1.主从模式(Master-Slave) 1.1主从复制原理 主从复制是redis的一种基本的集群方式,它通过将一个Redis节点(主节点)的数据复制到一个或多个其他Redis节点来实现数据的冗余和备份 主节点负责处理客户端的写操作,同时从节点回自动同步主节点的数据。客户端可以从从节点读取数…

PA3 - 穿越时空的旅程: 批处理系统

PA3 - 穿越时空的旅程: 批处理系统 穿越时空的旅程 概览 需求 程序内部的执行流切换:函数A和函数B可以简单地通过call/jal指令实现; 程序之间的执行流切换:因为安全问题,程序Prog1和程序Prog2的执行流切换只能交给操作系统和硬件来共同实现。 为了满足程序之间的执行流切换…

更快更强 | HP15加热台新品78折!Max温度350度,200度只需60秒!30~150W功率可调,恒温加热和回流焊双模式!

【新品优惠】正点原子HP15加热台更快更强!新品首发78折!最高温度可达350度,200度只需60秒!30~150W功率可调,恒温加热和回流焊双模式!HP15是正点原子全新推出的迷你恒温加热台,设备支持30~150W功率可调,在150W功率下从室温升至200度仅需60秒,可控温度高达350度,同时支…

MySQL登录报socket错误

现象 MySQL本地无法登录报错如下# mysql -uroot -p ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock修复方法 查看my.cnf配置文件确认mysql.sock位置带socket登录# mysql -uroot -p --socket=/var/lib/mysql/mysql.sock

Linxu 安装 Redis

1、下载redis 官网地址:https://redis.io/downloads/#redis-stack-downloads 或wget直接获取:wget https://download.redis.io/redis-stable.tar.gz 2、安装(不清楚linux命令的可以自行查询,make编译源文件,make install安装)1 # mkdir /usr/local/redis 2 # tar xf redis-…