首页 > 其他分享 >「Note」数据结构方向 - 数据结构进阶

「Note」数据结构方向 - 数据结构进阶

时间:2023-08-22 09:04:20浏览次数:42  
标签:rt return 进阶 int son Note fa 数据结构 节点

1. 平衡树

咕咕咕

2. 树套树

咕咕咕

3. LCT

3.1. 介绍

3.1.1. 基本概念

LCT 全名 Link-Cut-Tree,动态树,是用来维护动态森林的数据结构。
它支持以下操作(需要保证任意操作时刻维护的都为森林):

  • 连边。
  • 断边。
  • 换根。
  • 提取路径信息。

LCT 的大体思路是将每棵树拆分为若干条链,并用平衡树维护链中节点,维护关键字为节点深度,每一条链的状态都是动态的。

由于 LCT 结构的特殊性,一般我们用 Splay 维护链,更加抽象的,我们实际上并不直接利用类似 \(\mathrm{build}\) 的东西将树划分成若干链,我们在需要的时候动态维护我们需要的链,这基于了 Splay 方便的各种操作。

在维护的时候,对于每一个节点,我们选出至多一个儿子作为实儿子,令节点到实儿子的边为实边,剩余边为虚边。实边组成的链成为实链,因为实链是我们所选择的,所以 LCT 的才是灵活多变的,请牢记我们的实链是不断变化去维护信息的。

3.1.2. 辅助树

我们用 Splay 以节点深度为关键字维护每条实链上的点,虚边连接了一棵棵 Splay,每个 Splay 的根节点都有一条虚边指向其维护原链顶的父节点。以这样的方式连接,一棵树上的多个 Splay 就组成了一棵辅助树,多棵辅助树就构成了我们的 LCT。

一颗辅助树有如下性质:

  • 中序遍历每棵 Splay 得到的点序列,对应其维护链从上至下的一条路径。
  • 辅助树节点与原树节点一一对应
  • 每棵 Splay 根节点父节点并不为空,其用虚边指向了此 Splay 维护原链的父节点。需要注意的,根节点并不是其父节点的子节点(认父不认子)。

由于辅助树的以上性质,我们任何操作都不需要维护原树,辅助树可以在任何情况下提取出一棵原树。

需要注意的:

  • 原树的根不等于辅助树的根,原树的父节点指向不等于辅助树的父节点指向。
  • 虚实链是可以在辅助树上进行变换,实现了动态的树链剖分,这也是前文说到的“实链是不断变化去维护信息的”。

至于为什么在操作过程中需要翻转 Splay,目的是保证复杂度,具体证明大概是论文级的,这里不会提及,只需要记住即可。

3.1.3 实现过程

以洛谷模板题为例,进行实现过程的讲解。
维护一个结构体表示一个节点,其中含有以下信息:

\(fa\) \(son_{1/0}\) \(val\) \(sum\) \(siz\) \(lazy\)
父节点 子节点 此节点权值 此节点在 Splay 中的子树异或和 此节点在 Splay 中的子树大小 翻转标记

介绍 LCT 的核心函数之前先给出两个 Splay 的函数(\(t\) 是我们的节点结构体数组)。

void rotate(int rt)
{
	int fa=t[rt].fa,gfa=t[fa].fa,gs=get(rt),gfs=get(fa);
	bool flag=che_rt(fa);
	t[fa].son[gs]=t[rt].son[!gs],t[t[rt].son[!gs]].fa=fa;
	t[rt].son[!gs]=fa,t[fa].fa=rt,t[rt].fa=gfa;
	if(!flag)
		t[gfa].son[gfs]=rt;
	push_up(fa),push_up(rt);
	return;
}
void splay(int rt)
{
	push_down_(rt);
	for(int now=t[rt].fa;!che_rt(rt);rotate(rt))
		if(!che_rt(now=t[rt].fa))
			rotate((get(now)==get(rt))?now:rt);
	return;
}

rotate() 函数进行单旋操作,splay() 函数将节点旋至其所在 Splay 的根节点。与正常 Splay 不同的是,我们需要判断一个节点是否为其所在 Splay 的根(che_rt() 函数的作用),同时也请注意翻转标记的下传。

接下来我们介绍 LCT 的核心操作。

void access(int rt)
{
	for(int lst=0;rt;lst=rt,rt=t[rt].fa)
	{
		splay(rt);
		t[rt].son[1]=lst;
		push_up(rt);
	}
	return;
}

此函数作用是把某节点到当前辅助树的根路径上的点合并为一棵新的 Splay,具体过程如下:

  • 现将当前节点旋至其所在 Splay 的根。
  • 将上一节点接到当前节点上(这样直接实现了新链的合并与原来 Splay 边的断开)。
  • 更新当前节点信息。
  • 跳转到父亲,重复操作直到跳至根。

我们的剩余函数都以 access() 函数为基础进行操作,接下来介绍其他的函数。

  • tag() 用于对节点打上旋转标记。
void tag(int rt)
{
	swap(t[rt].son[0],t[rt].son[1]);
	t[rt].lazy^=1;
	return;
}
  • che_rt() 用于检查节点是否为 Splay 的根。
bool che_rt(int rt)
{
	return (t[t[rt].fa].son[0]!=rt&&t[t[rt].fa].son[1]!=rt);
}
  • get() 用于找到当前节点是其父节点的哪个儿子。
int get(int rt)
{
	return ((t[t[rt].fa].son[1]==rt)?1:0);
}
  • push_up() 更新当前节点信息。
void push_up(int rt)
{
	t[rt].siz=t[t[rt].son[0]].siz+t[t[rt].son[1]].siz+1;
	t[rt].sum=t[t[rt].son[0]].sum^t[t[rt].son[1]].sum^t[rt].val;
	return;
}
  • push_down() 下传标记。
void push_down(int rt)
{
	if(t[rt].lazy!=0)
	{
		tag(t[rt].son[0]);
		tag(t[rt].son[1]);
		t[rt].lazy=0;
	}
	return;
}
  • push_down_() 下传当前节点至 Splay 根节点的标记(从上至下)。
void push_down_(int rt)
{
	if(!che_rt(rt))
		push_down_(t[rt].fa);
	push_down(rt);
}
  • make_rt() 使当前节点变为辅助树的根。
void make_rt(int rt)
{
	access(rt);
	splay(rt);
	tag(rt);
	return;
}
  • find_rt() 查询当前节点所在辅助树的根(请考虑 splay() 的过程,不难得出结论)。
int find_rt(int rt)
{
	access(rt);
	splay(rt);
	while(t[rt].son[0])
	{
		push_down(rt);
		rt=t[rt].son[0];
	}
	splay(rt);
	return rt;
}
  • check() 查询两节点是否连通。
bool check(int x,int y)
{
	make_rt(x);
	return (find_rt(y)==x);
}
  • link() 连边(先判连通)。
bool link(int x,int y)
{
	make_rt(x);
	return (find_rt(y)==x);
}
  • cut() 断边(判相连,仍然考虑实现过程即可理解)。
void cut(int x,int y)
{
	if(check(x,y)&&t[x].siz<=2)
		t[y].fa=t[x].son[1]=0;
}
  • split() 分离一条链,正是前文提到的“提取路径信息”。
int split(int x,int y)
{
	make_rt(x);
	access(y);
	splay(y);
	return y;
}
  • change() 单点修改(并不是所有的题都有)。
void change(int rt,int val)
{
	splay(rt);
	t[rt].val=val;
	push_up(rt);
	return;
}

对于模板题的构建至此结束,LCT 的拓展性较强,具体题目还需具体分析。

3.2. 常用技巧

3.2.1 基础技巧

对于维护操作应多考虑优化,可以复杂化 push_up() 函数,但要注意代码结构的简洁性,并尽可能减少代码量与数据结构的操作难度,即使可能使复杂度变为稍劣(在无风险前提下)。

3.2.?咕咕咕

咕咕咕

3.2. 题目

\(\color{blueviolet}{P3690\ 【模板】动态树(LCT)}\)

板子。
\(\text{Code:}\)

#include<bits/stdc++.h>
#define LL long long
#define UN unsigned
using namespace std;
//--------------------//
const int N=1e5+5;

struct LCT
{
	struct Tree_Node
	{
		int fa,son[2];
		int val,sum,siz;
		int lazy;
	}t[N];
	void tag(int rt)
	{
		swap(t[rt].son[0],t[rt].son[1]);
		t[rt].lazy^=1;
		return;
	}
	bool che_rt(int rt)
	{
		return (t[t[rt].fa].son[0]!=rt&&t[t[rt].fa].son[1]!=rt);
	}
	int get(int rt)
	{
		return ((t[t[rt].fa].son[1]==rt)?1:0);
	}
	void push_up(int rt)
	{
		t[rt].siz=t[t[rt].son[0]].siz+t[t[rt].son[1]].siz+1;
		t[rt].sum=t[t[rt].son[0]].sum^t[t[rt].son[1]].sum^t[rt].val;
		return;
	}
	void push_down(int rt)
	{
		if(t[rt].lazy!=0)
		{
			tag(t[rt].son[0]);
			tag(t[rt].son[1]);
			t[rt].lazy=0;
		}
		return;
	}
	void push_down_(int rt)
	{
		if(!che_rt(rt))
			push_down_(t[rt].fa);
		push_down(rt);
	}
	void rotate(int rt)
	{
		int fa=t[rt].fa,gfa=t[fa].fa;
		int gs=get(rt),gfs=get(fa);
		bool flag=che_rt(fa);
		t[fa].son[gs]=t[rt].son[!gs];
		t[t[rt].son[!gs]].fa=fa;

		t[rt].son[!gs]=fa;
		t[fa].fa=rt;
		t[rt].fa=gfa;
		if(!flag)
			t[gfa].son[gfs]=rt;
		push_up(fa),push_up(rt);
		return;
	}
	void splay(int rt)
	{
		push_down_(rt);
		for(int now=t[rt].fa;!che_rt(rt);rotate(rt))
			if(!che_rt(now=t[rt].fa))
				rotate((get(now)==get(rt))?now:rt);
		return;
	}
	void access(int rt)
	{
		for(int lst=0;rt;lst=rt,rt=t[rt].fa)
		{
			splay(rt);
			t[rt].son[1]=lst;
			push_up(rt);
		}
		return;
	}
	void make_rt(int rt)
	{
		access(rt);
		splay(rt);
		tag(rt);
		return;
	}
	int find_rt(int rt)
	{
		access(rt);
		splay(rt);
		while(t[rt].son[0])
		{
			push_down(rt);
			rt=t[rt].son[0];
		}
		splay(rt);
		return rt;
	}
	bool check(int x,int y)
	{
		make_rt(x);
		return (find_rt(y)==x);
	}
	void link(int x,int y)
	{
		if(!check(x,y))
			t[x].fa=y;
	}
	void cut(int x,int y)
	{
		if(check(x,y)&&t[x].siz<=2)
			t[y].fa=t[x].son[1]=0;
	}
	int split(int x,int y)
	{	
		make_rt(x);
		access(y);
		splay(y);
		return y;
	}
	void change(int rt,int val)
	{
		splay(rt);
		t[rt].val=val;
		push_up(rt);
		return;
	}
}L;
//--------------------//
int n,m;
//--------------------//
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&L.t[i].val);//L.t[i].sum=L.t[i].val;
	for(int op,x,y,i=1;i<=m;i++)
	{
		scanf("%d%d%d",&op,&x,&y);
		if(op==0)
			printf("%d\n",L.t[L.split(x,y)].sum);
		else if(op==1)
			L.link(x,y);
		else if(op==2)
			L.cut(x,y);
		else 
			L.change(x,y);
	}
    return 0;
}

标签:rt,return,进阶,int,son,Note,fa,数据结构,节点
From: https://www.cnblogs.com/Eon-Sky/p/17646502.html

相关文章

  • 【数据结构】排序 内部排序算法的比较和应用
    1.简单复习一下前面学到的排序算法三种插入排序:直接插入:依次将后面无序序列中头部的元素插入前面的有序序列中(找到插入位置,这个位置后面的元素一律后移)折半插入:相比直接插入只是用折半查找的方式查找插入位置,元素的移动操作不变希尔排序:把相隔一定步长d的元素放入一个子表......
  • 线段树进阶
    多信息合并\(\text{GSS3Solution}\)\(\text{link}\)对于线段树的每个结点,记录其区间和(\(sum\)),区间前后缀最大子段和(\(lmax,rmax\))和区间最大子段和(\(vmax\))。合并:\[vmax=\max\{vmax_{lson},vmax_{rson},rmax_{lson}+lmax_{rson}\}\]\[lmax=\max\{lmax_{lson},sum_{lson}+lm......
  • 「Note」图论方向 - 图论进阶
    1.2-SAT1.1.介绍对于一些节点,每个节点存在两个状态(非\(0\)即\(1\)),我们给出一些如下类型的限制条件:节点\(i\)状态为\(1/0\)。若节点\(i\)状态为\(1/0\),那么节点\(j\)状态为\(1/0\)。节点\(i,j\(i\not=j)\)至少有一个为\(1/0\)。2-SAT算法用于解决类似的......
  • 数据结构-堆排序
    java实现堆排序算法源代码publicclassHeapSortextendsDataCrol{privatevoidheapify(intA[],inti,intsize){//从A[i]向下进行堆调整intleftChild=2*i+1;//左孩子索引intrightChild=2*i+2;//右孩子索引intmax=......
  • 数据结构-希尔排序
    java实现希尔排序算法源代码publicclassShellSortextendsDataCrol{@Overridepublicvoidsort(int[]array){inth=0;intsize=array.length;while(h<=size){//生成初始增量h=3*h+1;}//......
  • 数据结构-归并排序
    java实现归并排序算法源代码publicclassMergeSortextendsDataCrol{//合并两个已排好序的数组A[left...mid]和A[mid+1...right]voidmerge(intA[],intleft,intmid,intright){intlen=right-left+1;int[]temp=newint[len];//辅......
  • 数据结构与算法八股
    讲一讲插入排序讲一讲冒泡排序讲一讲快速排序讲一讲堆排序讲一讲归并排序 dpdp数组的定义及含义:dp[num1.length+1][num2.length+1],为什么要+1呢,因为我们要判断他与前面的关系涉及到i-1,所以遍历需要从1开始return的是什么如果初始化时候size+1了,那么最后一个下标就是size......
  • 【数据结构】排序 归并排序和基数排序
    1.归并排序归并排序中的"归并"的意义就是把多个有序表合并为一个新的有序表。算法思想:二路归并排序:初始情况下将长度为n的待排序表分为n个子表,则每个子表的长度为1,是有序的。每趟排序尽量将这些子表按位置相邻两两归并,重复直到合并为一个长度为n的有序表为止。具体实现:在归......
  • 开发调试更便捷!火山引擎 DataLeap 提供 Notebook 交互式开发体验
    更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群Notebook是一种支持REPL模式的开发环境。所谓「REPL」,即「读取-求值-输出」循环:输入一段代码,立刻得到相应的结果,并继续等待下一次输入。Notebook通常使得探索性的开发和调试更加便捷,在Notebo......
  • 开发调试更便捷!火山引擎 DataLeap 提供 Notebook 交互式开发体验
    更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群 Notebook是一种支持REPL模式的开发环境。 所谓「REPL」,即「读取-求值-输出」循环:输入一段代码,立刻得到相应的结果,并继续等待下一次输入。Notebook通常使得探索性的开发和调试更加便......