首页 > 其他分享 >替罪羊树:暴力美学

替罪羊树:暴力美学

时间:2022-10-09 11:25:27浏览次数:70  
标签:cnt return 暴力 int siz 替罪羊 美学 fac now

替罪羊树

简述

替罪羊树是一种体现代码暴力美学的数据结构。

虽然暴力,但它不是像分块、莫队那样的根号算法,它是一种 \(\log\) 算法。

多了解几个平衡树,会发现每棵树都有自己的特点,比如:

  1. Treap 是 BST 与堆的结合体;
  2. Splay 特有的伸展到根;
  3. Fhq_Treap 类似于拼图;
  4. 红黑树的节点染色;
  5. 替罪羊树暴力重构。

这里的暴力重构,就是指将一个子树转换为序列,再重新建树。

注:本文例题为 P3369,代码以 A 掉此题为目的。

定义与储存

和正常的 BST 差不多,只是多了一个实际节点个数 fac。至于用处,等下再解释。

struct Tzys{
	int lc,rc;
	int val,cnt;
	int siz,fac;
}T[inf];

然后是一些基本的辅助函数:

新建一个节点:

int new_(int k)
{
	T[++cnt].val=k;T[cnt].cnt=1;
	T[cnt].siz=T[cnt].fac=1;
	return cnt;
}

更新子树大小:

void pushup(int i)
{
	T[i].siz=T[T[i].lc].siz+T[T[i].rc].siz+T[i].cnt;
	T[i].fac=T[T[i].lc].fac+T[T[i].rc].fac+T[i].cnt;
}

但是,有时候需要将某个点到根的路径上所有点的子树大小都更新一遍。因为没有维护父指针,所以需要一个递归实现:

void maintain(int now,int to)
{
	if(now==to)return;
	if(T[to].val<T[now].val)
		maintain(T[now].lc,to);
	else maintain(T[now].rc,to);
	pushup(now);
}

插入、删除

插入和 BST 的插入方式相同,只是在操作之后需要检验是否平衡。

而删除操作并不是真正的删除,而是惰性删除,即只减小 fac 而不改变 siz,等暴力重构的时候在重新统计 siz

void insert(int &now,int k)
{
	if(!now)
	{
		now=new_(k);
		check(rot,now);
		return;
	}
	T[now].siz++,T[now].fac++;
	if(k==T[now].val)
	{
		T[now].cnt++;
		check(rot,now);
		return;
	}
	if(k<T[now].val)
		insert(T[now].lc,k);
	else insert(T[now].rc,k);
}
void remove(int now,int k)
{
	T[now].fac--;
	if(T[now].val==k)
	{
		T[now].cnt--;
		check(rot,now);
		return;
	}
	if(k<T[now].val)
		remove(T[now].lc,k);
	else remove(T[now].rc,k);
}

检查平衡

注意到,重构是将整棵子树拍平,所以将深度较浅的点重构之后,深度较深的点绝对不需要再次重构;但深度较深的点重构之后,深度较浅的点可能需要再次重构。

所以每次重构需要找到最浅的需要重构的节点。

而且,可以发现,只有插入的点经过的路径可能需要重构,对其他的点的平衡没有影响。

所以就用一个递归,从根开始找。

void check(int &now,int to)
{
	if(now==to)return;
	if(pd(now))
	{
		h.clear();assign(now);
		rebuild(now,0,h.size()-1);
		maintain(rot,now);
		return;
	}
	if(T[to].val<T[now].val)
		check(T[now].lc,to);
	else check(T[now].rc,to);
}

那么,怎么判断当前节点是否需要重构呢?

有两点:

  1. 当前节点的左右子树中存在一棵子树的节点个数大于当前节点子树的节点个数乘平衡因子。

  2. 当前节点的子树中非真实存在的节点个数大于子树节点个数的 \(30\%\)。

其中平衡因子常取 \(0.75\)。

bool pd(int i){return (max(T[T[i].lc].siz,T[T[i].rc].siz)>T[i].siz*0.75)||(T[i].siz-T[i].fac>T[i].siz*0.3);}

好像不是很可读……

bool pd(int i)
{
    return (max(T[T[i].lc].siz,T[T[i].rc].siz)>T[i].siz*0.75)||
           (T[i].siz-T[i].fac>T[i].siz*0.3);
}

应该能好点吧……

重构

重构比较简单,分为两个步骤:拍平和重建。

拍平很简单,按照中序遍历来就好:

vector<int>h;
void assign(int now)
{
	if(!now)return;
	assign(T[now].lc);
	if(T[now].cnt)h.push_back(now);
	assign(T[now].rc);
}

当然,被惰性删掉的点不需要再加进去了。

重建则和 Splay 实现区间平衡树的建树时一样的。

void rebuild(int &i,int l,int r)
{
	if(l>r){i=0;return;}
	int mid=(l+r)>>1;
	i=h[mid];
	rebuild(T[i].lc,l,mid-1);
	rebuild(T[i].rc,mid+1,r);
	pushup(i);
}

至此,替罪羊树的核心就结束了。

查询

查询操作就和普通的 BST 一样了,此处不在赘述。

但需要注意的是,替罪羊树的某棵子树真实存在的节点个数是 fac 个而不是 siz 个。

int ask_rnk(int k)
{
	int now=rot,ans=1;
	while(now)
	{
		if(k==T[now].val)return ans+T[T[now].lc].fac;
		if(k<T[now].val)now=T[now].lc;
		else ans+=T[T[now].lc].fac+T[now].cnt,now=T[now].rc;
	}
	return ans;
}
int ask_kth(int k)
{
	int now=rot;
	while(1)
	{
		if(T[T[now].lc].fac+1<=k&&k<=T[T[now].lc].fac+T[now].cnt)
			return T[now].val;
		if(k<=T[T[now].lc].fac)now=T[now].lc;
		else k-=T[T[now].lc].fac+T[now].cnt,now=T[now].rc;
	}
}

至于前驱后继,没必要在单独打函数,可以和权值线段树那里一样的实现方式。

if(op==3)wr(ask_rnk(k),'\n');
if(op==4)wr(ask_kth(k),'\n');
if(op==5)wr(ask_kth(ask_rnk(k)-1),'\n');
if(op==6)wr(ask_kth(ask_rnk(k+1)),'\n');

不过想自己打也不是不可以(笑。

完整代码

const int inf=1e5+7;
int n;
struct Tzys{
	int lc,rc;
	int val,cnt;
	int siz,fac;
}T[inf];
int rot,cnt;
int new_(int k)
{
	T[++cnt].val=k;T[cnt].cnt=1;
	T[cnt].siz=T[cnt].fac=1;
	return cnt;
}
void pushup(int i)
{
	T[i].siz=T[T[i].lc].siz+T[T[i].rc].siz+T[i].cnt;
	T[i].fac=T[T[i].lc].fac+T[T[i].rc].fac+T[i].cnt;
}
bool pd(int i){return (max(T[T[i].lc].siz,T[T[i].rc].siz)>T[i].siz*0.75)||(T[i].siz-T[i].fac>T[i].siz*0.3);}
vector<int>h;
void assign(int now)
{
	if(!now)return;
	assign(T[now].lc);
	if(T[now].cnt)h.push_back(now);
	assign(T[now].rc);
}
void rebuild(int &i,int l,int r)
{
	if(l>r){i=0;return;}
	int mid=(l+r)>>1;
	i=h[mid];
	rebuild(T[i].lc,l,mid-1);
	rebuild(T[i].rc,mid+1,r);
	pushup(i);
}
void maintain(int now,int to)
{
	if(now==to)return;
	if(T[to].val<T[now].val)
		maintain(T[now].lc,to);
	else maintain(T[now].rc,to);
	pushup(now);
}
void check(int &now,int to)
{
	if(now==to)return;
	if(pd(now))
	{
		h.clear();assign(now);
		rebuild(now,0,h.size()-1);
		maintain(rot,now);
		return;
	}
	if(T[to].val<T[now].val)
		check(T[now].lc,to);
	else check(T[now].rc,to);
}
void insert(int &now,int k)
{
	if(!now)
	{
		now=new_(k);
		check(rot,now);
		return;
	}
	T[now].siz++,T[now].fac++;
	if(k==T[now].val)
	{
		T[now].cnt++;
		check(rot,now);
		return;
	}
	if(k<T[now].val)
		insert(T[now].lc,k);
	else insert(T[now].rc,k);
}
void remove(int now,int k)
{
	T[now].fac--;
	if(T[now].val==k)
	{
		T[now].cnt--;
		check(rot,now);
		return;
	}
	if(k<T[now].val)
		remove(T[now].lc,k);
	else remove(T[now].rc,k);
}
int ask_rnk(int k)
{
	int now=rot,ans=1;
	while(now)
	{
		if(k==T[now].val)return ans+T[T[now].lc].fac;
		if(k<T[now].val)now=T[now].lc;
		else ans+=T[T[now].lc].fac+T[now].cnt,now=T[now].rc;
	}
	return ans;
}
int ask_kth(int k)
{
	int now=rot;
	while(1)
	{
		if(T[T[now].lc].fac+1<=k&&k<=T[T[now].lc].fac+T[now].cnt)
			return T[now].val;
		if(k<=T[T[now].lc].fac)now=T[now].lc;
		else k-=T[T[now].lc].fac+T[now].cnt,now=T[now].rc;
	}
}
int main()
{
	n=re();
	while(n--)
	{
		int op=re(),k=re();
		if(op==1)insert(rot,k);
		if(op==2)remove(rot,k);
		if(op==3)wr(ask_rnk(k),'\n');
		if(op==4)wr(ask_kth(k),'\n');
		if(op==5)wr(ask_kth(ask_rnk(k)-1),'\n');
		if(op==6)wr(ask_kth(ask_rnk(k+1)),'\n');
	}
	return 0;
}

指针版等有空再打吧。

时间复杂度证明

替罪羊树的优势

树套树都听说过吧。

但是你知道平衡树做外层树吗?

如果平衡树的每个节点都有套有一个内层树的话,那么用普通的旋转的平衡树的话,我们就不得不在旋转的时候合并子节点的内层树,而这样的单次操作时间复杂度为 \(O(n)\)。

但用替罪羊树就不一样了。它的结构基本稳定,只需要偶尔重构,且子树大小较大的节点重构一次之后很久才需要进行下一次重构,所以在树套树中可以作为外层树使用。

我不会啊,OI-wiki 上没写 qwq。

标签:cnt,return,暴力,int,siz,替罪羊,美学,fac,now
From: https://www.cnblogs.com/Zvelig1205/p/16770913.html

相关文章

  • 防止SSH暴力登录
    #!/bin/bash#DenyhostsSHELLSCRIPT#2022-9-27#可以添加定时任务crontab-lcrontab-e*/1****sh/home/shell-code/secure_check.sh#如果担心自己密码输......
  • 暴力枚举--津津的储蓄计划
    题目描述津津的零花钱一直都是自己管理。每个月的月初妈妈给津津300元钱,津津会预算这个月的花销,并且总能做到实际花销和预算的相同。为了让津津学习如何储蓄,妈妈提出,津......
  • 质数-暴力优化然后通过
    暴力方法#include<cstdio>#include<iostream>#include<cmath>usingnamespacestd;intmain(){intb;inta;cin>>a;///暴力方法证明......
  • ABC 249 D - Index Trio(暴力/倍增)
    https://atcoder.jp/contests/abc249/tasks/abc249_d题目大意:给定n个数字,问我们能够满足Ai/Aj==Ak的数量有多少?i,j,k只需要在下标的范围内即可,无硬性要求。SampleInp......
  • 寻找2个有序数组的中位数,暴力解法
    寻找2个有序数组的中位数[4.寻找两个正序数组的中位数-力扣(LeetCode)][https://leetcode.cn/problems/median-of-two-sorted-arrays/]doublefindMedianSortedArrays(v......
  • ABC 246 D - 2-variable Function(数论/暴力)
    https://atcoder.jp/contests/abc246/tasks/abc246_d题目大意:给定一个数字N,让我们求出X,这个X要满足X>=N,并且X内部可以有一对(a,b)满足a^3+a^2*b+b^2*a+b^3。找出最......
  • BF(暴力求解算法)
    BF(暴力求解算法)即是暴力(BruteForce)算法,是普通的模式匹配算法,BF算法的思想就是将目标串T的第一个字符和主串的S的第一个字符进行匹配,若相等,则则继续匹配T串和S串的第二......
  • 暴力匹配算法、KMP算法
    应用实例暴力匹配算法代码实现publicclassViolenceMatch{publicstaticvoidmain(String[]args){//测试暴力匹配算法Stringstr1="硅硅谷尚硅谷你尚硅......
  • 面试题 01.09. 字符串轮转【暴力模拟】【尾插】
    题目字符串轮转。给定两个字符串s1和s2,请编写代码检查s2是否为s1旋转而成(比如,waterbottle是erbottlewat旋转后的字符串)。难度:简单提示:字符串长度在[0,100000]范围内......
  • 暴力匹配算法、KMP算法
    应用实例暴力匹配算法代码实现publicclassViolenceMatch{ publicstaticvoidmain(String[]args){ //测试暴力匹配算法 Stringstr1="硅硅谷......