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  阅读(784)  评论(0)    收藏  举报