LinkCutTree LCT

news/2025/2/23 2:48:04/文章来源:https://www.cnblogs.com/HarlemBlog/p/18715579
更新日志 2025/02/14:开工。

概念

LCT 可以解决动态树下树链信息的维护。

动态树的意思是可以动态割边、连边。

首先我们将讲解整体思路,然后对各个函数依次介绍。

LCT 的均摊复杂度为 \(O(n\log n)\) 的,证明复杂,所以这里全部掠过,保证复杂度的操作都会提出。

实现

维护

由于动态的割边与连边,所以 LCT 维护的实际上是一片森林。

对于每一棵原树,我们对其进行实链剖分。类似于重链剖分,但实链剖分的实链与虚链是我们按需要决定的。

此外,对于每一棵原树,我们维护一棵辅助树。每一棵辅助树由多棵 Splay 组成,每棵 Splay 维护原树中一条实链。

每棵 Splay 中序遍历的顺序是原树中的深度顺序,由浅到深。

对于原树中每一条虚链,我们采取“认父不认子”的维护方式,对于其子节点记录其父节点,但其父节点不计这个子节点。这样可以快速判断这条链是实链还是虚链。

下面我们对每个函数进行详细讲解。

函数

信息维护函数

用于维护树链信息。

在 Splay 上,我们对于每个节点维护其子树信息,那么这棵 Splay 的根节点就维护了这条实链的信息。

pushup pushdown

这里以维护链异或和为例。

同时,我们必须维护一个子树翻转信息,这将会在后面 makeroot 函数中用到。

题目中额外的信息维护,全部写在这里就可以。

	void pushup(int x){sum[x]=sum[son[x][0]]^val[x]^sum[son[x][1]];}void pushdown(int x){if(rev[x]){if(son[x][0])swap(son[son[x][0]][0],son[son[x][0]][1]),rev[son[x][0]]^=1;if(son[x][1])swap(son[son[x][1]][0],son[son[x][1]][1]),rev[son[x][1]]^=1;rev[x]^=1;}}

update

这个函数用于传递一整条链(实际上是当前点到链头)的标记,方便后续对链的操作。

isroot 函数用于判断是否为当前 Splay 根节点,具体实现后面马上就会讲到。

递归更新即可。

	void update(int x){if(!isroot(x))update(fa[x]);pushdown(x);}

结构信息函数

isroot

用于判断这个节点是否为当前 Splay 根节点。由于虚链认父不认子,所以我们只需要判定这个节点是否为父节点的子节点之一即可。

	#define isroot(x) (x!=son[fa[x]][0]&&x!=son[fa[x]][1])

get

用于获取当前节点为父节点的左右子节点。没啥好说的。

	#define get(x) (x==son[fa[x]][1])

Splay 函数

由于实链是通过 Splay 维护的,所以会用到一些 Splay 的函数,在这里一并讲解。

rotate

用于把当前节点向上旋转。听起来略微抽象。

借图 OI-Wiki:

\(x\) 为当前节点,\(y\) 为其 Splay 内父节点。上面分别是左右节点的情况。

我们以情况 \(1\) 为例,情况 \(2\) 同理。

首先,为了保证中序遍历顺序不变,我们将 \(y\) 的左子节点设为 \(x\) 的右子节点。如果原来存在左子节点,那么把它变成虚链即可,不用特别处理。

然后,我们把 \(y\) 设作 \(x\) 的右子节点,再把 \(y\) 原来的父节点设作 \(x\) 的父节点。

旋转之后,我们显然要更新节点信息,从下往上更新即可,也就是先 \(y\)\(x\)

需要注意操作的顺序。

	void rotate(int x){int y=fa[x],z=fa[y],k=get(x);if(!isroot(y))son[z][get(y)]=x;son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;son[x][k^1]=y,fa[y]=x,fa[x]=z;pushup(y);pushup(x);}

splay

这个操作用于把一个节点旋转到其所在 Splay 的根节点,同时保证均摊复杂度。

复杂度证明这里暂且略去,如有需要,请自行查询 OI-Wiki。

在旋转操作之前,我们先把一路上的标记全部传递下来,也就是 update 一遍。

zig

当前节点父节点为根的情况,直接向上 rotate 即可。

zig-zig

当前节点父节点是同一侧子节点的情况,具体的:

我们先 rotate(y),然后 rotate(x) 即可。

zig-zag

当前节点父节点不是同一侧子节点的情况,具体的:

我们先 rotate(x),然后 rotate(y) 即可。

	void splay(int x){update(x);for(int f;f=fa[x],!isroot(x);rotate(x))if(!isroot(f))rotate(get(x)==get(f)?f:x);}

核心函数

这两个函数是 LCT 最核心的函数。

access

在原树上将当前点到原树根节点拉出一条实链。

对于我们经过的每一个节点,我们首先将其旋转到其 Splay 的根节点,然后将其右子节点设为上一棵 Splay 的根节点(如果这是第一步,就设为空,因为要单独拉出一条实链,所以要把下面的部分截掉)(旋转到根节点和放右子节点是为了维护中序深度递增)。

由于链发生了变化,别忘了 \(pushup\)

我们最后走到的节点显然就是原树的根节点。我们这里顺便返回一下辅助树的新根节点,方便 makeroot 操作。

	int access(int x){int p;for(p=0;x;p=x,x=fa[x]){splay(x);son[x][1]=p;pushup(x);}return p;}

makeroot

为了方便维护中序深度递增,我们有时候会需要把某一个节点设为原树的根节点,比如连边的时候。

首先我们拉出当前节点到原树根的实链,也就是 access 一遍。

其余节点的相对深度关系都没有变化,因此我们只需要反转这条实链的深度关系即可。

所以我们考虑反转中序顺序,也就是反转每个节点的左右子节点。

	void makeroot(int x){x=access(x);swap(son[x][0],son[x][1]);rev[x]^=1;}

常用函数

find

用于查找所在原树的根。

我们将当前节点到原根拉一条实链,然后这条实链中序第一个遍历到的就是根节点。

具体的,我们先把当前节点转到根节点,然后一直往左走就行。

为了维护复杂度,把找到的根节点转到这条实链的根节点。

	int find(int x){access(x);splay(x);pushdown(x);while(son[x][0])x=son[x][0],pushdown(x);splay(x);return x;}

split

用于单独拉出 \(x\)\(y\) 的链。前提是两者连通。

我们只需要先把 \(x\) 设为根节点,然后拉通 \(y\) 到根的链即可。

为了方便 cut 操作,我们通常把 \(y\) 转到 Splay 根节点。

	void split(int x,int y){makeroot(x);access(y);splay(y);}

连接两个节点,前提是两者不连通。

我们令为 \(x\) 连向 \(y\),为了维护深度关系,我们先把 \(x\) 设作原树根节点。

然后,我们把 \(x\) 转到 Splay 的根节点,并连一条向 \(y\) 的虚链即可。

	void link(int x,int y){makeroot(x);splay(x);fa[x]=y;}

cut

断开一条边,前提是有这条边。

提前存好边就行。有一个根据性质的判断方法,我不会。

我们先取出连接二者的实链,由于二者有边相连,所以二者在实链上必然相邻。

直接双向断开即可。

	void cut(int x,int y){split(x,y);son[y][get(x)]=fa[x]=0;}

change

修改点权操作。为了方便维护子树信息,我们先把要修改的点转到 Splay 根节点,然后直接改,并更新子树信息。

	void change(int x,int v){splay(x);val[x]=v;pushup(x);}

模板

以维护异或和为例。

struct LCT{int son[N][2],fa[N];int rev[N];int val[N],sum[N];#define isroot(x) (x!=son[fa[x]][0]&&x!=son[fa[x]][1])#define get(x) (x==son[fa[x]][1])void pushup(int x){sum[x]=sum[son[x][0]]^val[x]^sum[son[x][1]];}void pushdown(int x){if(rev[x]){if(son[x][0])swap(son[son[x][0]][0],son[son[x][0]][1]),rev[son[x][0]]^=1;if(son[x][1])swap(son[son[x][1]][0],son[son[x][1]][1]),rev[son[x][1]]^=1;rev[x]^=1;}}void update(int x){if(!isroot(x))update(fa[x]);pushdown(x);}void rotate(int x){int y=fa[x],z=fa[y],k=get(x);if(!isroot(y))son[z][get(y)]=x;son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;son[x][k^1]=y,fa[y]=x,fa[x]=z;pushup(y);pushup(x);}void splay(int x){update(x);for(int f;f=fa[x],!isroot(x);rotate(x))if(!isroot(f))rotate(get(x)==get(f)?f:x);}int access(int x){int p;for(p=0;x;p=x,x=fa[x]){splay(x);son[x][1]=p;pushup(x);}return p;}void makeroot(int x){x=access(x);swap(son[x][0],son[x][1]);rev[x]^=1;}int find(int x){access(x);splay(x);pushdown(x);while(son[x][0])x=son[x][0],pushdown(x);splay(x);return x;}void link(int x,int y){makeroot(x);splay(x);fa[x]=y;}void split(int x,int y){makeroot(x);access(y);splay(y);}void cut(int x,int y){split(x,y);son[y][get(x)]=fa[x]=0;}void change(int x,int v){splay(x);val[x]=v;pushup(x);}
}lct;

例题

LG3690

代码
#include<bits/stdc++.h>
using namespace std;typedef long long ll;
typedef unsigned long long ull;
typedef __int128 i128;
typedef double db;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
template <typename Type>
using vec=vector<Type>;
template <typename Type>
using grheap=priority_queue<Type>;
template <typename Type>
using lrheap=priority_queue<Type,vector<Type>,greater<Type> >;
#define fir first
#define sec second
#define pub push_back
#define pob pop_back
#define puf push_front
#define pof pop_front
#define chmax(a,b) a=max(a,b)
#define chmin(a,b) a=min(a,b)
#define rep(i,x,y) for(int i=(x);i<=(y);i++)
#define per(i,x,y) for(int i=(x);i>=(y);i--)
#define repl(i,x,y) for(int i=(x);i<(y);i++)
#define file(f) freopen(#f".in","r",stdin);freopen(#f".out","w",stdout);const int inf=0x3f3f3f3f;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int mod=/*1e9+7*/998244353;const int N=1e5+5;struct LCT{int son[N][2],fa[N];int rev[N];int val[N],sum[N];#define isroot(x) (x!=son[fa[x]][0]&&x!=son[fa[x]][1])#define get(x) (x==son[fa[x]][1])void pushup(int x){sum[x]=sum[son[x][0]]^val[x]^sum[son[x][1]];}void pushdown(int x){if(rev[x]){if(son[x][0])swap(son[son[x][0]][0],son[son[x][0]][1]),rev[son[x][0]]^=1;if(son[x][1])swap(son[son[x][1]][0],son[son[x][1]][1]),rev[son[x][1]]^=1;rev[x]^=1;}}void update(int x){if(!isroot(x))update(fa[x]);pushdown(x);}void rotate(int x){int y=fa[x],z=fa[y],k=get(x);if(!isroot(y))son[z][get(y)]=x;son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;son[x][k^1]=y,fa[y]=x,fa[x]=z;pushup(y);pushup(x);}void splay(int x){update(x);for(int f;f=fa[x],!isroot(x);rotate(x))if(!isroot(f))rotate(get(x)==get(f)?f:x);}int access(int x){int p;for(p=0;x;p=x,x=fa[x]){splay(x);son[x][1]=p;pushup(x);}return p;}void makeroot(int x){x=access(x);swap(son[x][0],son[x][1]);rev[x]^=1;}int find(int x){access(x);splay(x);pushdown(x);while(son[x][0])x=son[x][0],pushdown(x);splay(x);return x;}void link(int x,int y){makeroot(x);splay(x);fa[x]=y;}void split(int x,int y){makeroot(x);access(y);splay(y);}void cut(int x,int y){split(x,y);son[y][get(x)]=fa[x]=0;}void change(int x,int v){splay(x);val[x]=v;pushup(x);}
}lct;int n,m;
set<pii> st;int main(){ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;int v;rep(i,1,n)cin>>v,lct.change(i,v);int op,x,y;rep(i,1,m){cin>>op>>x>>y;if(op==0)lct.split(x,y),cout<<lct.sum[y]<<'\n';if(op==1)if(lct.find(x)!=lct.find(y))lct.link(x,y),st.insert({min(x,y),max(x,y)});if(op==2)if(st.count({min(x,y),max(x,y)}))lct.cut(x,y),st.erase({min(x,y),max(x,y)});if(op==3)lct.change(x,y);}return 0;
}

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

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

相关文章

分享一个用于免费取名chrome插件

這是一個幫助父母為寶寶取名的 Chrome 擴充功能,基於八字命理為寶寶打造獨特而富有寓意的名字。 功能特點支持輸入寶寶姓氏 可選擇寶寶性別(男寶寶/女寶寶) 可選擇名字長度(二字名/三字名) 支持輸入出生日期和時辰 提供多種期望寓意選擇 支持自定義期望寓意 完全繁體中文…

100N03-ASEMI豆浆机专用MOS管100N03

100N03-ASEMI豆浆机专用MOS管100N03编辑:ll 100N03-ASEMI豆浆机专用MOS管100N03 型号:100N03 品牌:ASEMI 封装:TO-252 最大漏源电流:100A 漏源击穿电压:30V 批号:最新 RDS(ON)Max:5.0mΩ 引脚数量:3 沟道类型:N沟道MOS管 芯片尺寸:MIL 漏电流: 恢复时间:ns 芯片…

NLLB 与 ChatGPT 双向优化:探索翻译模型与语言模型在小语种应用的融合策略

本文探讨了 NLLB 翻译模型与 ChatGPT 在小语种应用中的双向优化策略。首先介绍了 NLLB-200 的背景、数据、分词器和模型,以及其与 LLM(Large Language Model)的异同和协同关系。接着列举了实战与应用的案例,包括使用 ChatGPT 生成的样本微调 NLLB-200 和使用 NLLB-200 的翻…

段码液晶显示屏驱动芯片/LCD驱动控制器-VK1072B SOP28可最大支持184的LCD屏

产品品牌:永嘉微电/VINKA 产品型号:VK1072 封装形式:SOP28 概述 VK1072B是一个点阵式存储映射的LCD驱动器,可支持最大 72点(18SEGx4COM)的LCD屏,也支持2COM和3COM的 LCD屏。单片机可通过三条通信线配置显示参数和发送显示 数据,也可通过指令进入省电模式。LJQ4073特点 •…

通义灵码插件下载量破千万!感谢大家的喜爱

近年来,人工智能技术以惊人的速度发展,尤其是大模型技术的突破,为 AI 在编程领域的应用注入了全新动力。从代码补全到全流程软件开发,从自然语言生成代码到AI 程序员的上线,大模型正在重构传统的编程范式,推动 AI 编码全面落地,也重新定义了人与 AI 的协作开发模式。近年…

用 DeepSeek 给对象做个网站,她一定感动坏了

最后祝有情人终成眷属,没有情人的,跟自己好好相处,缘分总会到来 ❤️大家好,我是程序员鱼皮。又是一年特殊的日子,作为一名程序员,总是幻想着自己有对象, 总是想着用自己贼拉牛 X 的编程技术给对象做个网站。本文对应视频,观看体验更好哦:https://bilibili.com/video/…

利用 proxychains 代理下载huggingface数据

socket 代理使用安装 brew install proxychains-ng 配置$ tail -n 3 /opt/homebrew/etc/proxychains.conf #socks4 127.0.0.1 9050 socks5 127.0.0.1 <端口>使用# pkill proxychains4-daemon # step 1. proxychains4-daemon # step 2 proxychains4 huggingface-cli…

Proxmox VE 实战

Proxmox VE 1.Ventoy制作启动镜像 镜像下载地址: https://www.ventoy.net/cn/download.html 2.启动主机进入BIOS页面 进入系统BIOS菜单 \1. 打开USB \2设置USB 启动 进入 Ventoy 3.启动程序选择图形化安装自定义分区设置root密码以及邮箱设置网络以及IP(根据实际服务器设置)4…

文论阅读:RS-2022-3MRS: An Effective Coarse-to-Fine Matching Method for Multimodal Remote Sensing Imagery

本文提出了一种有效的多模态遥感图像(3MRS)粗到细匹配方法。在粗匹配阶段,首先使用相位一致性模型计算的最大矩上检测特征点。然后使用索引图进行特征描述,该索引图是通过在使用一组log-Gabor滤波器获得的卷积图像的所有方向上找到最大值的索引而构建的。最后,通过图像匹配…

prometheus安装部署

环境:os:Centos 1.二进制包下载地址https://prometheus.io/download/ cd /softwget https://github.com/prometheus/prometheus/releases/download/v3.1.0/prometheus-3.1.0.linux-amd64.tar.gz 2.安装[root@prometheus local]#tar zxvf prometheus-3.1.0.linux-amd64.tar.gz[r…

头疼的架构师:难以分工的RTL设计

RTL 设计工程中遇到一种怪像:虽然可用的人手很多,但很难将任务拆分分配下去,导致人力出现紧张。将原因归因于下:RTL 代码可读性差 抛一个仓库让成员从源码中理解难度颇高。往往需要配合辅助的文档以及频繁对接,这极大分散顶层开发架构师的精力; 控制模块耦合性强 组合逻辑…