Loading

替罪羊树

基本概念

替罪羊树,又称 \(Scapegoat\ Tree\) ,是一种 平衡树 。这种平衡树的单次时间复杂度是 \(O(logn)\) 。若替罪羊树单位时间内最多同时存在 \(m\) 个结点,空间复杂度由于其特殊的 非指针内存回收 机制也可以达到 \(O(m)\)

替罪羊树的思想较为暴力,但是代码是除 \(fhq\ treap\) 外较为简洁的平衡树,相较于 \(Treap, Splay\) 等平衡树更容易快速实现。所以,如果 懒癌发作 想在较短的时间内实现平衡树的功能,同时题目对时间没有特别严苛的要求,这是就可以考虑使用替罪羊树。

算法思想

替罪羊树的主要思想就是 重构 。具体来说,如果替罪羊树中的某棵子树失去了平衡,那么我们直接暴力地将这棵子树排列成一个 线性序列 ,然后再通过这个序列把该子树重建成一棵 完全平衡 的二叉搜索树。这样,通过次数较小的调整,我们可以令大部分的子树都实现完全平衡,从而使整棵树尽量平衡。

最暴力的情况下,在每次操作后都会尝试重建一棵子树,但是这样的修改时间复杂度达到了惊人的 \(O(n)\) 。因此,我们需要判断一棵子树是否失衡,之后再决定是否需要重构。在这里引入一个新的概念 平衡因子。平衡因子根据需要的不同,取值范围大多在 \([0.5, 1]\) 之间,通常选用中间值 \(0.7\) 。根据平衡因子,我们判断:

令平衡因子为 \(\alpha\) ,假设需要判断结点 \(x\) 的子树是否失衡。设 \(x\) 的左右子树大小分别为 \(n, m\) ,当前结点子树大小为 \(k\) ,若 \(\frac{\max(n, m)}{k} > \alpha\) ,我们就重构当前子树。否则,认为这棵子树是平衡的,不进行任何操作。判断和控制重构可以在一个函数内实现。

接下来,我们一个个分析一下替罪羊树的操作。

操作详解

判断结点可用

如果当前结点没有左右子树,并且自身个数为 \(0\) ,说明替罪羊树中这一部分已经被彻底删除,只是还没有被重构函数过滤。此时我们不可以使用这些结点,因此,我们需要根据上述条件判断这些结点是否可用。

bool exist(int x)
{
	return !(tree[x].l == 0 && tree[x].r == 0 && tree[x].cnt == 0);
}

建立序列

把结点 \(x\) 的子树重组成一个线性序列,每一个递归子结构都满足根结点是区间中点,左子树在序列左半部分,右子树在序列右半部分。最后返回根结点在序列中的下标。

还是直接模拟。如果当前结点左子树不为空,先将左子树建成一个线性序列。接着判断根结点是否为空,若不空,记录根结点的下标为当前序列的长度,再在序列末尾加入根结点。最后判断右子树,与左子树同理。

请注意,如果存在个数为 \(0\) 的结点,在这个函数就会被清理掉。所以,替罪羊树中一般是不存在不合法结点的。因此,替罪羊树即使删除次数较多,时间复杂度也不会太坏。而且,并不是每次操作都能引起序列失衡的。所以,就算是暴力,替罪羊树也通常比普通的二叉搜索树跑得更快。

int flatten(int x)
{
	if (exist(tree[x].l))
		flatten(tree[x].l);
	int id = tp.size();
	if (tree[x].cnt)
	{
		tp.push_back(x);
		tn.push_back(tree[x].cnt);
		tv.push_back(tree[x].val);
	}
	if (exist(tree[x].r))
		flatten(tree[x].r);
	return id;
}

重建子树

假设目前已经得到了重建的线性序列,要将其建成一个完全平衡的二叉树,并接回原来的替罪羊树。

假如当前结点存在左右子树,也就是当前待建树的区间不是单点,那么判断是否可以建立左右子树,再分别建树即可。注意此时因为之前的结点信息没有清空,所以不可以直接访问左右子树的大小。如果存在左右子树,那么将它们的信息更新以后才可以用变量记录它们使用。最后,记得更新当前结点的信息。

void rebuild(int x, int l, int r)
{
	int mid = (l + r) / 2;
	int sizel = 0, sizer = 0;
	if (l < mid)
	{
		tree[x].l = tp[(l + mid - 1) / 2];
		rebuild(tree[x].l, l, mid - 1);
		sizel = tree[tree[x].l].size;
	}
	else
		tree[x].l = 0;
	if (r > mid)
	{
		tree[x].r = tp[(mid + 1 + r) / 2];
		rebuild(tree[x].r, mid + 1, r);
		sizer = tree[tree[x].r].size;
	}
	else
		tree[x].r = 0;
	tree[x].cnt = tn[mid];
	tree[x].val = tv[mid];
	tree[x].size = sizel + sizer + tree[x].cnt;
}

维护平衡

这个函数既要判断子树是否失衡,又要在失衡时重建子树。

显然,我们首先要根据平衡因子判断是否失衡。如果失衡,我们首先先进行初始化,其中 tp 数组表示子树中结点下标组成的线性序列,tn 表示子树中结点个数组成的线性序列,tv 表示子树中结点权值组成的线性序列。

接着,我们将子树重构成线性序列,然后根据序列重构子树即可。值得注意的易错点 是,因为我们重构子树是从序列的中点开始的,如果根结点的下标不在中点(左右子树可能为空),我们必须先将它交换到中点,才能进行建树。

void restr(int x)
{
	double val = max(tree[tree[x].l].size, tree[tree[x].r].size) * 1.0 / tree[x].size;
	if (val > alpha)
	{
		tp.clear();
		tn.clear();
		tv.clear();
		int id = flatten(x);
		swap(tp[id], tp[(tp.size() - 1) / 2]);
		rebuild(x, 0, tp.size() - 1);
	}
}

其他函数

替罪羊树的其余函数与普通的二叉搜索树相差不大,如果理解的平衡树的思想,不难通过代码理解这些内容。由于 笔者实在太懒 篇幅限制,此处不再对其他的操作进行赘述。如果对平衡树的思想不是非常理解,建议先阅读笔者的 Splay FHQ_Treap Treap 这几篇博文。

最后,感谢在 洛谷 提供优质博客链接的同学们。同时放上笔者推荐的 替罪羊树学习笔记 。在此处感谢这篇文章的作者,为本文提供了优质的参考资料。

参考代码

例题链接

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;

const int maxn = 1e5 + 5;
const double alpha = 0.7;

struct node
{
	int l, r;
	int cnt, val, size;
} tree[maxn];

int n, m, cnt = 1;
vector<int> tp, tn, tv;

bool exist(int x)
{
	return !(tree[x].l == 0 && tree[x].r == 0 && tree[x].cnt == 0);
}

int flatten(int x)
{
	if (exist(tree[x].l))
		flatten(tree[x].l);
	int id = tp.size();
	if (tree[x].cnt)
	{
		tp.push_back(x);
		tn.push_back(tree[x].cnt);
		tv.push_back(tree[x].val);
	}
	if (exist(tree[x].r))
		flatten(tree[x].r);
	return id;
}

void rebuild(int x, int l, int r)
{
	int mid = (l + r) / 2;
	int sizel = 0, sizer = 0;
	if (l < mid)
	{
		tree[x].l = tp[(l + mid - 1) / 2];
		rebuild(tree[x].l, l, mid - 1);
		sizel = tree[tree[x].l].size;
	}
	else
		tree[x].l = 0;
	if (r > mid)
	{
		tree[x].r = tp[(mid + 1 + r) / 2];
		rebuild(tree[x].r, mid + 1, r);
		sizer = tree[tree[x].r].size;
	}
	else
		tree[x].r = 0;
	tree[x].cnt = tn[mid];
	tree[x].val = tv[mid];
	tree[x].size = sizel + sizer + tree[x].cnt;
}

void restr(int x)
{
	double val = max(tree[tree[x].l].size, tree[tree[x].r].size) * 1.0 / tree[x].size;
	if (val > alpha)
	{
		tp.clear();
		tn.clear();
		tv.clear();
		int id = flatten(x);
		swap(tp[id], tp[(tp.size() - 1) / 2]);
		rebuild(x, 0, tp.size() - 1);
	}
}

void insert(int x, int val)
{
	if (!exist(x))
	{
		tree[x].cnt = 1;
		tree[x].val = val;
	}
	else if (val < tree[x].val)
	{
		if (!exist(tree[x].l))
			tree[x].l = ++cnt;
		insert(tree[x].l, val);
	}
	else if (val > tree[x].val)
	{
		if (!exist(tree[x].r))
			tree[x].r = ++cnt;
		insert(tree[x].r, val);
	}
	else
		tree[x].cnt++;
	tree[x].size++;
	restr(x);
}

void del(int x, int val)
{
	tree[x].size--;
	if (val < tree[x].val)
		del(tree[x].l, val);
	else if (val > tree[x].val)
		del(tree[x].r, val);
	else
		tree[x].cnt--;
	restr(x);
}

int rank_pre(int x, int val)
{
	if (val < tree[x].val)
		return (exist(tree[x].l) ? rank_pre(tree[x].l, val) : 0);
	else if (val > tree[x].val)
		return tree[tree[x].l].size + tree[x].cnt + (exist(tree[x].r) ? rank_pre(tree[x].r, val) : 0);
	else
		return tree[tree[x].l].size;
}

int rank_nxt(int x, int val)
{
	if (val > tree[x].val)
		return ((exist(tree[x].r) ? rank_nxt(tree[x].r, val) : 0));
	else if (val < tree[x].val)
		return tree[tree[x].r].size + tree[x].cnt + (exist(tree[x].l) ? rank_nxt(tree[x].l, val) : 0);
	else
		return tree[tree[x].r].size;
}

int rank(int val)
{
	return rank_pre(1, val) + 1;
}

int find(int x, int rk)
{
	if (rk <= tree[tree[x].l].size)	
		return find(tree[x].l, rk);
	else if (rk > tree[tree[x].l].size + tree[x].cnt)
		return find(tree[x].r, rk - tree[tree[x].l].size - tree[x].cnt);
	else
		return tree[x].val;
}

int pre(int x)
{
	int rk = rank_pre(1, x);
	return find(1, rk);
}

int nxt(int x)
{
	int rk = rank_nxt(1, x);
	return find(1, tree[1].size - rk + 1);
}

int main()
{
//	freopen("P3369_2.in", "r", stdin);
//	freopen("P3369_2.out", "w", stdout);
	int opt, x;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
	{
		scanf("%d%d", &opt, &x);
		if (opt == 1)
			insert(1, x);
		else if (opt == 2)
			del(1, x);
		else if (opt == 3)
			printf("%d\n", rank(x));
		else if (opt == 4)
			printf("%d\n", find(1, x));
		else if (opt == 5)
			printf("%d\n", pre(x));
		else
			printf("%d\n", nxt(x));
	}
	return 0;
}
posted @ 2021-07-24 23:33  kymru  阅读(631)  评论(0编辑  收藏  举报