首页 > 其他分享 >LCT小记

LCT小记

时间:2024-07-16 17:11:40浏览次数:13  
标签:LCT rs int splay fa ls 节点 小记

简介

LCT 是常用的一种动态树。对于一般的树上问题,我们会用树剖解决,但是如果遇到动态增删边的问题就需要 LCT 来解决。

LCT 的本质上是一种链剖分,我们将所有的边剖分为虚边实边,所以整棵树是由若干条实链构成的,实链之间用虚边相连。

我们通过 splay 来维护实链的信息,并以从上到下的顺序保持中序遍历不变,并实现各种操作。

实现

P3690 【模板】动态树(LCT) 为例。

基础

我们首先需要定义节点结构体:

struct Node {
	int val, sum, fa, rev;//本节点的值,节点维护的子树的异或和,父节点,翻转标记 
	int ch[2];//两个儿子 
	Node () {
		val = sum = fa = rev = ch[0] = ch[1] = 0;
	}
} a[N];

然后我们再定义宏:

#define ls(x) a[x].ch[0]
#define rs(x) a[x].ch[1]
#define fa(x) a[x].fa

辅助函数

首先我们需要两个函数 get 和 isroot。

get 表示当前节点是父节点的左节点还是右节点,左节点返回 0,右节点返回 1。

isroot 用来判断这个节点是不是其所在实链的 splay 的根

由于 LCT 有虚实边之分,其中实边的特点是父节点和自己互相联系,而虚边则是认父不认子,也就是说,从父结点是无法走到虚变连接的点的,但是从虚变可以走到父节点。

所以我们知道,判断一个点是不是所在实链的根,只要看其到父节点的边是不是虚边即可。只要父亲的两个儿子都不是自己就说明是虚边。

这两个函数就实现了:

bool get(int x) {//判断是那个儿子 
	return rs(fa(x)) == x;
}
bool isroot(int x) {//判断是不是 splay 的根,虚边认父不认子 
	return ls(fa(x)) != x && rs(fa(x)) != x;
}

然后我们还需要数据结构的常规操作:pushup 和 pushdown,这两个没啥好说的,这道题我们需要区间翻转(至于为什么我们之后再说),维护异或和:

void pushup(int x) {//pushup
	a[x].sum = (a[ls(x)].sum ^ a[rs(x)].sum ^ a[x].val);
}
void pushdown(int x) {//pushdown
	if (a[x].rev) {
		swap(ls(ls(x)), rs(ls(x)));
		swap(ls(rs(x)), rs(rs(x)));
		a[ls(x)].rev ^= 1;
		a[rs(x)].rev ^= 1;
		a[x].rev = 0;
	}
}

但是我们还需要一个函数 update,用来将 \(x\) 到所在实链 splay 的根的路径上所有点都 pushdown,因为我们后面的操作有些是从下往上的,所以不一定父节点就 pushdown 过了,所以需要这个函数来 pushdown。

void upd(int x) {//将 x 到 splay 根的路径都pushdown 
	if (!isroot(x))
		upd(fa(x));
	pushdown(x);
}

splay 的操作

splay 的核心操作是 rotate 和 splay。

rotate 与正常的 splay 基本一样,但是有一点要注意:正常我们需要找到 \(x\) 的父节点 \(y\) 和爷爷 \(z\),如果爷爷存在,就要更新爷爷的儿子。但是由于现在是在 LCT 中,我们需要用 isroot 来判断爷爷存不存在,而不能简单地用是不是 0 来判断。

void rotate(int x) {
	int y = fa(x), z = fa(y), k = get(x);//x - y - z, k 表示 x 是哪边的 
	if (!isroot(y))//如果 y 不是根,就把 z 的 y 儿子变成 x
		a[z].ch[get(y)] = x;
	fa(a[x].ch[1 - k]) = y;//将 x 的儿子过继给 y
	a[y].ch[k] = a[x].ch[1 - k];
	a[x].ch[1 - k] = y;//将父节点变成自己的子节点 
	fa(y) = x, fa(x) = z;//更新父节点指向 
	pushup(y), pushup(x);//注意顺序!!! 
}

而 splay 也类似,需要将判断是不是根用 isroot 来判断,这一条适用于所有操作

我的写法遵循的逻辑是:如果需要旋父亲就旋父亲,否则一直旋自己。而不是一次性两次旋自己,但是或以下图就会发现这启示是等价的,没用影响。

注意最开始要 update 一下。

void splay(int x) {//将 x 变成根 
	upd(x);//更新整个路径 
	while (!isroot(x)) {//只要不是根 
		if (!isroot(fa(x)) && !isroot(fa(fa(x))) && get(x) == get(fa(x))) 
			rotate(fa(x));//如果都是同一边都一样就转父亲
		//上面需要判断父亲和爷爷是不是根,否则都不能双旋 
		rotate(x);//自己肯定会转 
	}
} 

access

LCT 的核心操作之一,将 \(x\) 到原树的根的路径上的所有边都变成实边。

我们考虑该如何实现,对于一个点,首先我们将 splay 到根,然后我们需要跨过虚边的限制。

现在我们的父节点并不认识我们,所以我们需要尝试将我们挂到这个父节点下面,成为实边。

所以我们将父节点 splay 到根,由于我们是按照中序遍历,所以我们需要变成其右儿子。

但是如果人家本来有右儿子怎么办?考虑到虚边认父不忍子,我们直接修改,这样那个右儿子就变成了虚边!!!!

所以我们这样就可以将到根路径上所有边变成实边。

我们可以更进一步,如果最开始我们把 \(x\) 的右儿子也变成虚边,那么最终得到的链将会只包括根到 \(x\) 的路径上的点。

于是我们就实现了这个功能。代码中我们通过储存上一个点来实现:

void access(int x) {//将 x 到根的路径变成实链,并且没有其他点 
	int p = 0;//上次的点
	while (x) {//只要 x 还有
		splay(x);//把 x 旋到根
		rs(x) = p;//这一步首先踢掉了原来的儿子,变成了新的儿子
		pushup(x);//更新一下
		p = x, x = fa(x);//更新 
	} 
}

makeroot

makeroot 也是核心操作之一,我们现在想要将 \(x\) 变成原树的根(换根)。

首先,我们先 access 变成实链,由于不知道 splay 根节点,我们可以 splay(x) 变成根。

现在我们需要用到翻转标记了,我们发现换根之后有且只有这条链上的顺序会发生翻转,于是我们打一个翻转标记即可(不要忘了同时也要交换左右儿子)。

void makeroot(int x) {//将 x 变成原树的根 
	access(x);//弄到同一颗 splay 上
	splay(x);//将 x 变成根
	//现在由于没有其他点,所以我们可以直接反转整个链
	a[x].rev ^= 1;//翻转标记 
	swap(ls(x), rs(x));//翻转 
} 

findroot

findroot 用来找到 \(x\) 所在原树的根。

我们依然是 access 加 splay,然后我们发现根应该是第一个,所以我们不断地找左儿子即可。

注意找的同时需要 pushdown,并且为了保证时间复杂度最后需要 splay 一下。

int findroot(int x) {//查找所在原树的根 
	access(x);
	splay(x);
	while (ls(x)) {//只要还有左儿子 
		pushdown(x);//先pushdown
		x = ls(x); 
	}
	splay(x);//保证复杂度
	return x; 
}

split

将 \(x\) 到 \(y\) 的路径上单独变成一棵 splay。

这个很简单,直接 makeroot + access + splay 即可,最后在 y 上处理。

void spt(int x, int y) {//将 x 到 y 单独拿出来作为一棵 splay, 并且 y 是根 
	makeroot(x);
	access(y);
	splay(y);
}

两个核心操作,连接与切除。

对于链接来说,我们先 makeroot(x),然后如果本来不在于一起,那么就把 x 变成 y 的虚儿子。

对于切除,依然是先 makeroot(x),然后我们思考条件:x 和 y 联通且路径上没有别的点,也就是 y 是 x 的右儿子且没有左儿子。

这种写法将本来需要的 access 和 splay 操作在 findroot 里就有了,所以 if 的顺序不能变!

void link(int x, int y) {//加一条边在 x 和 y 之间 
	makeroot(x);//将 x 变成树的根
	if (findroot(y) != x)//如果 y 和 x 不在一起
		fa(x) = y; //这里是虚儿子 
	pushup(x);//更新 x 
}
void cut(int x, int y) {//x 和 y 的边切断 
	makeroot(x);
	//顺序不能变!findroot蕴含了access和splay x 到根的操作
	if (findroot(y) == x && fa(y) == x && !ls(y))
		fa(y) = rs(x) = 0;//双向断开 
	pushup(x);//更新 x  
}

完整代码

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5;

namespace LCT {
	struct Node {
		int val, sum, fa, rev;//本节点的值,节点维护的子树的异或和,父节点,翻转标记 
		int ch[2];//两个儿子 
		Node () {
			val = sum = fa = rev = ch[0] = ch[1] = 0;
		}
	} a[N];
	
	#define ls(x) a[x].ch[0]
	#define rs(x) a[x].ch[1]
	#define fa(x) a[x].fa
	
	bool get(int x) {//判断是那个儿子 
		return rs(fa(x)) == x;
	}
	bool isroot(int x) {//判断是不是 splay 的根,虚边认父不认子 
		return ls(fa(x)) != x && rs(fa(x)) != x;
	}
	
	void pushup(int x) {//pushup
		a[x].sum = (a[ls(x)].sum ^ a[rs(x)].sum ^ a[x].val);
	}
	void pushdown(int x) {//pushdown
		if (a[x].rev) {
			swap(ls(ls(x)), rs(ls(x)));
			swap(ls(rs(x)), rs(rs(x)));
			a[ls(x)].rev ^= 1;
			a[rs(x)].rev ^= 1;
			a[x].rev = 0;
		}
	}
	
	void upd(int x) {//将 x 到 splay 根的路径都pushdown 
		if (!isroot(x))
			upd(fa(x));
		pushdown(x);
	}
	
	void rotate(int x) {
		int y = fa(x), z = fa(y), k = get(x);//x - y - z, k 表示 x 是哪边的 
		if (!isroot(y))//如果 y 不是根,就把 z 的 y 儿子变成 x
			a[z].ch[get(y)] = x;
		fa(a[x].ch[1 - k]) = y;//将 x 的儿子过继给 y
		a[y].ch[k] = a[x].ch[1 - k];
		a[x].ch[1 - k] = y;//将父节点变成自己的子节点 
		fa(y) = x, fa(x) = z;//更新父节点指向 
		pushup(y), pushup(x);//注意顺序!!! 
	}
	
	void splay(int x) {//将 x 变成根 
		upd(x);//更新整个路径 
		while (!isroot(x)) {//只要不是根 
			if (!isroot(fa(x)) && !isroot(fa(fa(x))) && get(x) == get(fa(x))) 
				rotate(fa(x));//如果都是同一边都一样就转父亲
			//上面需要判断父亲和爷爷是不是根,否则都不能双旋 
			rotate(x);//自己肯定会转 
		}
	} 
	
	void access(int x) {//将 x 到根的路径变成实链,并且没有其他点 
		int p = 0;//上次的点
		while (x) {//只要 x 还有
			splay(x);//把 x 旋到根
			rs(x) = p;//这一步首先踢掉了原来的儿子,变成了新的儿子
			pushup(x);//更新一下
			p = x, x = fa(x);//更新 
		} 
	}
	
	void makeroot(int x) {//将 x 变成原树的根 
		access(x);//弄到同一颗 splay 上
		splay(x);//将 x 变成根
		//现在由于没有其他点,所以我们可以直接反转整个链
		a[x].rev ^= 1;//翻转标记 
		swap(ls(x), rs(x));//翻转 
	} 
	
	int findroot(int x) {//查找所在原树的根 
		access(x);
		splay(x);
		while (ls(x)) {//只要还有左儿子 
			pushdown(x);//先pushdown
			x = ls(x); 
		}
		splay(x);//保证复杂度
		return x; 
	}
	
	void spt(int x, int y) {//将 x 到 y 单独拿出来作为一棵 splay, 并且 y 是根 
		makeroot(x);
		access(y);
		splay(y);
	}
	
	void link(int x, int y) {//加一条边在 x 和 y 之间 
		makeroot(x);//将 x 变成树的根
		if (findroot(y) != x)//如果 y 和 x 不在一起
			fa(x) = y; //这里是虚儿子 
		pushup(x);//更新 x 
	}
	void cut(int x, int y) {//x 和 y 的边切断 
		makeroot(x);
		//顺序不能变!findroot蕴含了access和splay x 到根的操作
		if (findroot(y) == x && fa(y) == x && !ls(y))
			fa(y) = rs(x) = 0;//双向断开 
		pushup(x);//更新 x  
	}
}
int n, m;


int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> LCT::a[i].val, LCT::a[i].sum = LCT::a[i].val;
	for (int i = 1, op, x, y; i <= m; i++) {
		cin >> op >> x >> y;
		if (op == 0) {
			LCT::spt(x, y);
			printf("%d\n", LCT::a[y].sum);
		} 
		else if (op == 1) 
			LCT::link(x, y);
		else if (op == 2)
			LCT::cut(x, y);
		else {
			LCT::makeroot(x);
			LCT::splay(x);
			LCT::a[x].val = y;
			LCT::pushup(x); 
		}
	}
	return 0;
}

标签:LCT,rs,int,splay,fa,ls,节点,小记
From: https://www.cnblogs.com/rlc202204/p/18305662

相关文章

  • 微信小程序代码审计小记
    本文参考文章地址:https://zhuanlan.zhihu.com/p/694193212准备工具1.反编译后的小程序文件夹详情请参考《日拱一卒之微信小程序自动化辅助渗透工具》https://www.cnblogs.com/--l-/p/182455582.审阅工具vscode2.1下载VsCode。点击图示位置的“下载”即可。下载地址:htt......
  • 万能欧几里得小记
    类欧几里得类欧几里得算法解决这样的问题:\[f(p,q,r,n)=\sum_{x=0}^n[\frac{px+r}{q}]\]可以理解为一条直线下方的整点个数。第一步首先,我们可以将\(r\)对\(q\)取模,从而提取出一个不变量。第二步我们可以继续将\(p\)对\(q\)取模,从而使得\(p,r\)都在\([0,q)......
  • Journalctl命令常见用法
    1、查询指定时间范围内的日志journalctl--since"2023-01-0100:00:00"--until"2023-01-0223:59:59"journalctl-usshd--since"2023-05-01"--until"2023-5-31"-n202、查询指定系统单元服务的日志journalctl-unginx--sinceyesterdayjournalc......
  • 温故而知新之burp升级小记
    个人记录贴。本文仅作为技术交流使用,如有违反行为本文作者概不负责。最近突然想升级一下burpsuite,看看有啥新功能没。挺久没升级了。结果发现burp无法正常抓包,于是有了以下内容。本文内容参考链接:https://blog.csdn.net/qq_44159028/article/details/116043520升级burp详情......
  • Miller-Rabin 和 Pollard-Rho 小记
    Miller-Rabin可以帮助我们快速判断一个大数是不是质数,现在已经有了确定性算法。在\(2^{64}\)范围内,我们可以快速地进行确定性判素。二次校验定理:若\(p\)为奇质数,则\(a^x\equiv1\pmodp\)的解为\(x=±1\)。我们有这样的流程:令\(d=p-1\),然后不断检验\(a^d\)......
  • min25 小记
    min25筛可以处理这样一类问题:求$F(x)$的前缀和,其中$F(x)$是积性函数,且可以表示成$F(p)=p^a+p^b+...$这样的形式,其中$p$是质数。如果$F(p)$是多项式,我们把它们拆开分别算,然后加起来。我们希望先求出其质数位置上的值,设$g(i,j)$为$[2,i]$中,删去了$p_1$到......
  • MTT 小记
    有的时候,我们想要让多项式乘法结果中的系数,对一些不是那么常规的模数取模,或者对任意质数\(p\)取模,这时候我们可以用MTT来解决。MTT有两种,一种是三模数NTT,另一种是拆系数FFT。三模数NTT三模数NTT就是,选择三个著名的NTT模数,比如998244353、1004535809、469762049,......
  • R语言、SAS潜类别(分类)轨迹模型LCTM分析体重指数 (BMI)数据可视化|附代码数据
    全文下载链接: http://tecdat.cn/?p=26105 最近我们被客户要求撰写关于LCTM的研究报告,包括一些图形和统计输出。在本文中,潜类别轨迹建模(LCTM)是流行病学中一种相对较新的方法,用于描述生命过程中的暴露,它将异质人群简化为同质模式或类别。然而,对于给定的数据集,可以根据类的数......
  • vue3+node.js+mysql+electron+express实现用户登录,文章写入删除,全量更新,增量更新,和截
    第一件事情是安装node.js,去官网下,在终端node-v,npm-v有版本号就行了,不必搞环境配置,保姆级别教程,感谢哥有时间。嘻嘻,祝大家开心。1.首先你要创建electron项目打开vscode,新建终端输入代码npminit这个代码是初始化的意思会生成一个文件package.json里面的代码应该是这......
  • 【python小记】使用openpyxl库在同一个工作表下复制单元格(包括它们的值、样式和合并属
    fromopenpyxlimportload_workbook#加载工作簿和工作表wb=load_workbook('test.xlsx')sheet=wb['sheet1']#定义一个函数来复制样式defcopy_style(source_cell,target_cell):ifsource_cell.has_style:target_cell.font=source_cell.font.co......