可持久化平衡树详解及实现方法分析
Posted chy-2003
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了可持久化平衡树详解及实现方法分析相关的知识,希望对你有一定的参考价值。
前置要求
带旋转的平衡树会改变祖先关系,这令可持久化变得困难。所以需要使用非旋的平衡树,如非旋treap。本文以非旋treap为例。
核心思想
可持久化的数据结构,其核心都是不改变历史的信息。当需要对信息进行修改的时候就新开一个节点,继承历史信息,然后再进行修改。
对于非旋treap来说,主要是对 Split 和 Merge 两个操作进行可持久化。剩下的操作不会对数据产生影响。而考虑到非旋treap上的操作在 Split 后一定跟随着对应的 Merge ,所以不需要对每个 Split 和 Merge 操作都进行可持久化。只要实现 Insert 和 Delete 的可持久化即可。
Split
Split 操作的可持久化比较容易实现,直接复制节点即可:
std::pair<int, int> Split(int Root, int Key) {
if (!Root) return std::pair<int, int>(0, 0);
std::pair<int, int> Temp;
int New = ++Used;
Pool[New] = Pool[Root];
if (Key < Pool[New].Value) {
Temp = Split(Pool[New].LeftChild, Key);
Pool[New].LeftChild = Temp.second;
Pool[New].Update();
Temp.second = New;
} else {
Temp = Split(Pool[New].RightChild, Key);
Pool[New].RightChild = Temp.first;
Pool[New].Update();
Temp.first = New;
}
return Temp;
}
Merge
Merge 操作过程中是否需要新建节点颇有争议。这一点会在下面 Merge 是否应该新建节点 中做详细的分析。在这里先给上新建节点的方式,毕竟这样一定不会错:
int Merge(int x, int y) {
if ((!x) || (!y)) return x ^ y;
int New = ++Used;
if (Pool[x].Priority <= Pool[y].Priority) {
Pool[New] = Pool[x];
Pool[New].RightChild = Merge(Pool[New].RightChild, y);
Pool[New].Update();
} else {
Pool[New] = Pool[y];
Pool[New].LeftChild = Merge(x, Pool[New].LeftChild);
Pool[New].Update();
}
return New;
}
这两个操作是整个可持久化平衡树的核心。在这两个操作的基础上,可以实现其他的操作,例如 Insert :
void Insert(int History, int Version, int Key) {
Versions[Version] = Versions[History]; //先从历史版本复制信息。
std::pair<int, int> Temp = Split(Versions[Version], Key); //将树分为不超过 Key 和大于 Key 两部分
int New = ++Used; Pool[New] = node(Key); //新建节点
Temp.first = Merge(Temp.first, New);
Versions[Version] = Merge(Temp.first, Temp.second); //依次合并
return;
}
在理解的基础上,你就可以解决这道题了:
实现方法比较:指针 与 数组模拟指针
脱开这道题,指针的速度和数组的速度应当是相差不大的。在 Ubuntu18.04 系统, g++ 7.4.0 版本上,两个范围的测试结果如下(使用 chrono 库计时):
数组大小 1000, 进行 10000000 次访问及赋值,30 次测试取平均值。下标访问为 22ms ,指针访问为 25ms 。
数组大小 10000,进行 100000000 次访问及赋值,30 次测试取平均值。下标访问为 277ms ,指针访问为 263ms 。
可能系统和编译器对测试结果有影响,但可见两种方式其实差别不大。
但是在这道题目里,数组实现明显优于指针实现,原因有如下几点:
虽然指针实现不需要对数组初始化,但是每次 new 一个节点也需要时间。new 操作不会快于数组的初始化;
指针实现需要更多的对空指针的判断,这会耗去许多时间。而数组实现只需要将其赋值为 0 即可;
在程序结束时,数组申请的连续的空间,所以销毁速度极快。而指针申请的空间不一定是连续的,所以销毁速度慢于数组。同样的,如果通过 --fsanitize=address 来分析内存,它会告诉你最后内存有溢出。
指针实现不易于调试。
下面的分析基于上面那题。对于随机的极限数据,指针需要 26ms ,而数组只要 19ms (1000组数据取平均),指针耗时是数组的 1.36 倍。
而同样神奇的一点是在luogu上,相同的算法,指针消耗的空间远大于数组(不清楚原因)?同时指针速度远远慢于数组。(差了近两倍?)
有dalao指出,在某些平台上,指针消耗的时空都是数组的两倍
所以还是建议写数组吧。只要在不MLE的情况下把数组往大开就好了。
Merge 是否应该新建节点
下面来仔细分析一下 Merge 的时候是否应该新建节点。
上面提到了, Split 和 Merge 可以看做一次操作。那么如果 Merge 中需要改变的节点都是 Split 中新建的节点,那么 Merge 就不需要新建节点。
实际上,这和维护的信息有关。如果将相同的关键字维护成不同的节点,那么 Merge 的时候就需要新建节点。考虑到删除的时候会分出来一棵权值都为关键字的树,而这棵树只有通向最左节点的那条链和最右节点的那两条链是新的节点,其他节点都是历史中的节点。如果是通过 Merge(LeftChild, RightChild)
来实现,就会访问到从根到叶子的随机一条路径(由于随机的优先级)。显然这条路径不一定是 Split 中新建的节点。或者,你可以手动删除最左边或最右边的节点。而这样实现并不那么方便与自然。
而如果将有相同关键字的信息维护成相同的点,那么 Merge 时就不需要新建节点。由于真正要改变的节点至多只有一个,而这个节点到根的路径都是 Split 时新建的节点,所以可以直接合并,不新建节点。
显然后一种做法更省空间。(前一种需要433MB,而后一种只需要225MB,2019.11.9luogu数据。)
luogu的数据较为宽松,上边错误的做法(也就是相同的关键字维护成不同的节点,但 Merge 的时候没有新建节点)也可以通过(见提交记录)。不过已经联系管理员加强数据了。(2019.11.9)或者你可以通过构造一些操作数有大量相同的数据来验证(如有 1000 次操作,类型随机,操作数 rand() %3+1
,稍微拍一会儿就能够hack掉了)。
参考程序
这份代码用数组实现,并且将关键字相同的数据记在同一个节点。
#include <cstdio>
#include <ctime>
#include <unistd.h>
#include <algorithm>
#define INF 2147483647
#define Maxn 500010
#define MaxSize 30000000
struct node {
int Value, Priority, Size, Count;
int LeftChild, RightChild;
node();
node(int _Value);
inline void Update();
};
node Pool[MaxSize];
int Versions[Maxn], Used;
std::pair<int, int> Split(int Root, int Key);
int Merge(int x, int y);
int Find(int Root, int Key);
int Update(int Root, int Key, int State);
void Insert(int Version, int Key);
void Delete(int Version, int Key);
inline int Rank(int Root, int Key);
inline int Query(int Root, int Key);
inline int Precursor(int Root, int Key);
inline int Successor(int Root, int Key);
int n, Version, Opt, Key;
int main() {
srand(time(NULL));
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d%d%d", &Version, &Opt, &Key);
Versions[i] = Versions[Version];
if (Opt == 1) Insert(i, Key);
else if (Opt == 2) Delete(i, Key);
else if (Opt == 3) printf("%d
", Rank(Versions[i], Key));
else if (Opt == 4) printf("%d
", Query(Versions[i], Key));
else if (Opt == 5) printf("%d
", Precursor(Versions[i], Key));
else printf("%d
", Successor(Versions[i], Key));
}
return 0;
}
node::node() { return; }
node::node(int _Value) {
Value = _Value;
Priority = rand();
Size = Count = 1;
LeftChild = RightChild = 0;
return;
}
inline void node::Update() {
Size = Count + Pool[LeftChild].Size + Pool[RightChild].Size;
return;
}
std::pair<int, int> Split(int Root, int Key) {
if (!Root) return std::pair<int, int>(0, 0);
std::pair<int, int> Temp;
int New = ++Used;
Pool[New] = Pool[Root];
if (Key < Pool[New].Value) {
Temp = Split(Pool[New].LeftChild, Key);
Pool[New].LeftChild = Temp.second;
Pool[New].Update();
Temp.second = New;
} else {
Temp = Split(Pool[New].RightChild, Key);
Pool[New].RightChild = Temp.first;
Pool[New].Update();
Temp.first = New;
}
return Temp;
}
int Merge(int x, int y) {
if ((!x) || (!y)) return x ^ y;
if (Pool[x].Priority <= Pool[y].Priority) {
Pool[x].RightChild = Merge(Pool[x].RightChild, y);
Pool[x].Update();
return x;
} else {
Pool[y].LeftChild = Merge(x, Pool[y].LeftChild);
Pool[y].Update();
return y;
}
}
int Find(int Root, int Key) {
while (Root) {
if (Pool[Root].Value == Key) return Root;
if (Key < Pool[Root].Value) Root = Pool[Root].LeftChild;
else Root = Pool[Root].RightChild;
}
return Root;
}
int Update(int Root, int Key, int State) {
if (!Root) return 0;
int New = ++Used; Pool[New] = Pool[Root];
Pool[New].Size += State;
if (Pool[New].Value == Key) {
Pool[New].Count += State;
return New;
}
if (Key < Pool[New].Value) Pool[New].LeftChild = Update(Pool[New].LeftChild, Key, State);
else Pool[New].RightChild = Update(Pool[New].RightChild, Key, State);
return New;
}
void Insert(int Version, int Key) {
if (Find(Versions[Version], Key)) {
Versions[Version] = Update(Versions[Version], Key, 1);
return;
}
std::pair<int, int> Temp = Split(Versions[Version], Key);
int New = ++Used; Pool[New] = node(Key);
Temp.first = Merge(Temp.first, New);
Versions[Version] = Merge(Temp.first, Temp.second);
return;
}
void Delete(int Version, int Key) {
int Temp = Find(Versions[Version], Key);
if (!Temp) return;
if (Pool[Temp].Count > 1) {
Versions[Version] = Update(Versions[Version], Key, -1);
return;
}
std::pair<int, int> Temp1 = Split(Versions[Version], Key);
std::pair<int, int> Temp2 = Split(Temp1.first, Key - 1);
Versions[Version] = Merge(Temp2.first, Temp1.second);
return;
}
inline int Rank(int Root, int Key) {
static int Ans; Ans = 0;
while (Root) {
if (Pool[Root].Value == Key) return Ans + Pool[Pool[Root].LeftChild].Size + 1;
if (Key < Pool[Root].Value) Root = Pool[Root].LeftChild;
else Ans += Pool[Pool[Root].LeftChild].Size + Pool[Root].Count, Root = Pool[Root].RightChild;
}
return Ans + 1;
}
inline int Query(int Root, int Key) {
while (Root) {
if (Pool[Pool[Root].LeftChild].Size < Key && Pool[Pool[Root].LeftChild].Size + Pool[Root].Count >= Key) return Pool[Root].Value;
if (Pool[Pool[Root].LeftChild].Size >= Key) Root = Pool[Root].LeftChild;
else Key -= Pool[Pool[Root].LeftChild].Size + Pool[Root].Count, Root = Pool[Root].RightChild;
}
return 0;
}
inline int Precursor(int Root, int Key) {
static int Ans; Ans = -INF;
while (Root) {
if (Pool[Root].Value < Key) Ans = Pool[Root].Value, Root = Pool[Root].RightChild;
else Root = Pool[Root].LeftChild;
}
return Ans;
}
inline int Successor(int Root, int Key) {
static int Ans; Ans = INF;
while (Root) {
if (Pool[Root].Value > Key) Ans = Pool[Root].Value, Root = Pool[Root].LeftChild;
else Root = Pool[Root].RightChild;
}
return Ans;
}
以上是关于可持久化平衡树详解及实现方法分析的主要内容,如果未能解决你的问题,请参考以下文章