FHQ Treap
FHQ Treap
前言 | Preface
前置知识
二叉搜索树、堆、递归
简介
Treap 的思路是通过赋予节点随机的优先级,然后让优先级满足堆的条件,以约束树高来保障时间复杂度
因为我们发现随机值性质十分的弱,可以轻松的卡过,所以我们就会想到利用随机
但是数据肯定不会是随机,不然暴力都能过
但是我们又可以自己为数据附上随机值,Treap 就是这种思路
由于涉及随机的赋值,复杂度分析会稍难一些
根据具体的实现,有无旋(FHQ,和有旋两种
先学的较为简单且通用的 FHQ Treap
实现
FHQ Treap 主要就是两个操作:split 和 merge
框架
/*
key of left child node <= key of node <= key of right node
priority of left or right >= this priority
*/
struct node {
// children
int left, right;
// value
int key, priority, size;
// tag
int rev;
} Treap[maxn];
int totalNode = 0, root = 0; // totalNode : the number of nodes, root : root
这里我们让这棵树的优先级满足小根堆的性质
接下来用 left 表示裂开的“左”树,right 表示“右”树,如果分裂两次,则用 mid 表示“中”树
maintain
void maintain(int id) {
return Treap[id].size = Treap[lson(id)].size + Treap[rson(id)].size + 1, void();
}
向上传递 size
pushdown
void pushdown(int u) {
if (Treap[u].rev) {
swap(lson(u), rson(u));
if (lson(u)) Treap[lson(u)].rev ^= 1;
if (rson(u)) Treap[rson(u)].rev ^= 1;
Treap[u].rev = 0;
}
}
这个是后面的翻转操作所需要的
New Node
int newNode(int value) {
Treap[++totalNode] = {0, 0, value, (int)rnd(), 1, 0};
return totalNode;
}
Split
有别于线段树的 split 操作,这里是将所有的键值 key 小于等于 value 的拎出,重新构成了两棵树
void split(int u, int value, int &left, int &right) {
if (!u) {
left = right = 0;
return ;
}
pushdown(u);
if (Treap[u].key <= value) {
left = u;
split(rson(u), value, rson(u), right);
} else {
right = u;
split(lson(u), value, left, lson(u));
}
maintain(u);
}
这个得手动模拟一番才能很好的理解,left 和 right 表示分裂后的两棵树
它的思路是将 u 的 key 与 value 比较,利用搜索树的性质发现已将确定的子树,遍历另外一棵重复此操作
然后利用一边的空节点回溯,将本来在不应在的位置的节点抹去(赋值为 \(0\)
自己手撕一下会很清晰(这里不用考虑 priority ,因为一直保持着原树的祖先关系
Merge
将优先级小的放在上面
int merge(int left, int right) {
if (!left || !right) return left | right;
if (Treap[left].priority < Treap[right].priority) {
pushdown(left);
rson(left) = merge(rson(left), right);
maintain(left);
return left;
} else {
pushdown(right);
lson(right) = merge(left, lson(right));
maintain(right);
return right;
}
}
Insert
将 \(\le v\) 和 \(\gt v\) 的分开,在中间插入
void insert(int value) {
int left, right;
split(root, value, left, right);
root = merge(merge(left, newNode(value)), right);
}
Erase
分成三棵,一棵 \(\lt v\),一棵 \(= v\),另一棵 \(\gt v\)
将中间的根节点删去(合并它的左右儿子
void erase(int value) {
int left, mid, right;
split(root, value, left, right);
split(left, value - 1, left, mid);
if (mid) mid = merge(lson(mid), rson(mid));
root = merge(merge(left, mid), right);
}
如果是将全部值为 \(v\) 的删去,则直接合并 left 和 right
root = merge(left, right);
rank
将 \(\lt v\) 的裂开,Treap[left].size + 1 就是它的排名
int Rank(int value) {
int left = 0, right = 0, result;
split(root, value - 1, left, right);
result = Treap[left].size + 1;
root = merge(left, right);
return result;
}
第 \(k\) 大
和线段树的操作差不多
int kth_element(int u, int k) {
if (k == Treap[lson(u)].size + 1) return u;
if (k <= Treap[lson(u)].size) return kth_element(lson(u), k);
else return kth_element(rson(u), k - Treap[lson(u)].size - 1);
}
前驱、后继
将 \(\lt v\) 的裂开,left 中第 Treap[left].size 大的键值就是前驱
同理,将 \(\gt v\) 的裂开,right 中第 \(1\) 大的键值就是后继
/*
precursor
*/
int precursor(int value) {
int left = 0, right = 0, result;
split(root, value - 1, left, right);
result = Treap[kth_element(left, Treap[left].size)].key;
root = merge(left, right);
return result;
}
/*
successor
*/
int successor(int value) {
int left = 0, right = 0, result;
split(root, value, left, right);
result = Treap[kth_element(right, 1)].key;
root = merge(left, right);
return result;
}
汇总
总的代码就是如此了
View Code
mt19937 rnd(chrono::steady_clock::now().time_since_epoch().count());
namespace FHQ_Treap{
/*
key of left child node <= key of node <= key of right node
priority of left or right <= this priority
*/
struct node {
// children
int left, right;
// value
int key, priority, size;
// tag
int rev;
} Treap[maxn];
int totalNode = 0, root = 0; // totalNode : the number of nodes, root : root
int newNode(int value) {
Treap[++totalNode] = {0, 0, value, (int)rnd(), 1, 0};
return totalNode;
}
#define lson(u) Treap[u].left
#define rson(u) Treap[u].right
void maintain(int id) {
return Treap[id].size = Treap[lson(id)].size + Treap[rson(id)].size + 1, void();
}
// split (by some value)
/*
push down the tag "rev"
*/
void pushdown(int u) {
if (Treap[u].rev) {
swap(lson(u), rson(u));
if (lson(u)) Treap[lson(u)].rev ^= 1;
if (rson(u)) Treap[rson(u)].rev ^= 1;
Treap[u].rev = 0;
}
}
/*
if the key of root is less than the splitting standard,we only need consider right sub-tree and vice versa
the complexity of splitting is O(height of the tree) = O(log n)
why the expection of the height is O(log n)
the probility of that node x which is d far away from u is an ancetor of u is 1/d
so E[depth(u)] = \sum_{i = 1}^{n} 1/i = O(log n)
*/
void split(int u, int value, int &left, int &right) {
if (!u) {
left = right = 0;
return ;
}
pushdown(u);
if (Treap[u].key <= value) {
left = u;
split(rson(u), value, rson(u), right);
} else {
right = u;
split(lson(u), value, left, lson(u));
}
maintain(u);
}
/*
split by rank
*/
void splitByRank(int u, int k, int &left, int &right) {
if (!u) {
left = right = 0;
return ;
}
pushdown(u);
if (Treap[lson(u)].size + 1 <= k) {
left = u;
splitByRank(rson(u), k - (Treap[lson(u)].size + 1), rson(u), right);
} else {
right = u;
splitByRank(lson(u), k, left, lson(u));
}
maintain(u);
}
/*
smilar to segment tree merging
*/
int merge(int left, int right) {
if (!left || !right) return left | right;
if (Treap[left].priority < Treap[right].priority) {
pushdown(left);
rson(left) = merge(rson(left), right);
maintain(left);
return left;
} else {
pushdown(right);
lson(right) = merge(left, lson(right));
maintain(right);
return right;
}
}
/*
reverse
*/
void reverse(int L, int R) {
int left = 0, mid = 0, right = 0;
splitByRank(root, L - 1, left, right);
splitByRank(right, R - L + 1, mid, right);
Treap[mid].rev ^= 1;
root = merge(merge(left, mid), right);
}
/*
insert
*/
void insert(int value) {
int left, right;
split(root, value, left, right);
root = merge(merge(left, newNode(value)), right);
}
/*
delete
*/
void erase(int value) {
int left, mid, right;
split(root, value, left, right);
split(left, value - 1, left, mid);
if (mid) mid = merge(lson(mid), rson(mid));
root = merge(merge(left, mid), right);
}
/*
rank
*/
int Rank(int value) {
int left = 0, right = 0, result;
split(root, value - 1, left, right);
result = Treap[left].size + 1;
root = merge(left, right);
return result;
}
/*
k-th element
*/
int kth_element(int u, int k) {
if (k == Treap[lson(u)].size + 1) return u;
if (k <= Treap[lson(u)].size) return kth_element(lson(u), k);
else return kth_element(rson(u), k - Treap[lson(u)].size - 1);
}
/*
precursor
*/
int precursor(int value) {
int left = 0, right = 0, result;
split(root, value - 1, left, right);
result = Treap[kth_element(left, Treap[left].size)].key;
root = merge(left, right);
return result;
}
/*
successor
*/
int successor(int value) {
int left = 0, right = 0, result;
split(root, value, left, right);
result = Treap[kth_element(right, 1)].key;
root = merge(left, right);
return result;
}
/*
???
*/
void inorder(int u) {
if (!u) return ;
pushdown(u);
inorder(lson(u));
cout << u << " ";
inorder(rson(u));
}
#undef lson
#undef rson
}
例题
P3369 【模板】普通平衡树
纯模版
cin >> n;
for (int i = 1; i <= n; i++) {
int opt, x;
cin >> opt >> x;
if (opt == 1) FHQ_Treap::insert(x);
else if (opt == 2) FHQ_Treap::erase(x);
else if (opt == 3) cout << FHQ_Treap::Rank(x) << "\n";
else if (opt == 4) cout << FHQ_Treap::Treap[FHQ_Treap::kth_element(FHQ_Treap::root, x)].key << "\n";
else if (opt == 5) cout << FHQ_Treap::precursor(x) << "\n";
else cout << FHQ_Treap::successor(x) << "\n";
}
P3391 【模板】文艺平衡树
还是模版(直接用 reverse
void inorder(int u) {
if (!u) return ;
pushdown(u);
inorder(lson(u));
cout << u << " ";
inorder(rson(u));
}
cin >> n >> m;
for (int i = 1; i <= n; i++) insert(i);
for (int i = 1; i <= m; i++) {
int left, right;
cin >> left >> right;
FHQ_Treap::reverse(left, right);
}
inorder(root);
P1486 [NOI2004] 郁闷的出纳员
这个就要动一下脑子了
我们拿一个 delta 记录全局的工资,然后 实际的工资 = Treap 中的工资 + delta
不过新入职的员工的工资要减去 delta(好悲催
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; i++) {
char type;
cin >> type;
int k;
cin >> k;
if (type == 'I') {
if (k >= m) {
k -= delta;
insert(k);
}
} else if (type == 'A') {
delta += k;
} else if (type == 'S') {
delta -= k;
int L = 0, R = 0;
split(root, m - delta - 1, L, R);
root = R;
cnt += Treap[L].size;
} else {
if (k > Treap[root].size) cout << -1 << "\n";
else cout << Treap[kth_element(root, Treap[root].size - k + 1)].key + delta << "\n";
}
}
cout << cnt << "\n";
[SPOJ227] ORDERS - Ordering the Soldiers
这个就要大动一下脑子了
正着走会被后面的兵影响到,不妨倒着走
cin >> n;
root = 0;
for (int i = 1; i <= n; i++) cin >> arr[i];
for (int i = 1; i <= n; i++)
root = merge(root, newNode(i)); // 初始化
for (int i = n; i >= 1; i--) {
int pos = i - arr[i];
int left, mid, right;
splitByRank(root, pos - 1, left, right); // 分裂成排名 [1, pos - 1] 和 [pos, n] 的
splitByRank(right, 1, mid, right); // 将第一个弄出来,这个就是我们要的
answer[i] = Treap[mid].key;
root = merge(left, right); // 合并回去
}
for (int i = 1; i <= n; i++) cout << answer[i] << " \n"[i == n];
CF702F T-Shirts
这个更是要动一一一下脑子
既然人去匹配 T shirts 很复杂,我们不妨反过来,让 T shirts 去匹配人
将 T shirts 按照品质从高到低排序
假设现在的 T shirts 的品质是 p,价格是 c
我们让所有买得起的都买下这一件
现在先粗略的分成两个部分:\([1,c]\),\((c,\infin)\)
然后我们发现,在后面一个部分中,\((c,2c]\) 的部分在减去 \(c\) 后会变成 \([1,c]\) 的部分
所以我们分成三个部分:\([1,c]\),\((c,2c]\),\((2c, \infin)\)
第一个不用管,第二个暴力减了合并到第一个上面,第三个直接减
现在考虑暴力加到第一个部分上的复杂度
假设 \(x \in (c,2c]\),考虑更新后的比例 \(\frac{x - c}{x}\),在 \(x = 2c\) 的时候取得最大,为 \(\frac12\)
所以,\(x\) 至少都减小了 \(\frac12\),所以最多 \(\mathcal O(\log n)\) 次修改
所以复杂度为 \(\mathcal O(n\log n\log w)\),其中 \(w\) 是值域大小
struct node {
// children
int left, right;
// value
int key, priority, size;
// tag
int rev;
// problem
int tag1, tag2;
int number;
int id;
} Treap[maxn];
int totalNode = 0, root = 0; // totalNode : the number of nodes, root : root
int newNode(int value, int id) {
Treap[++totalNode] = {0, 0, value, (int)rnd(), 1, 0, 0, 0, 0, id};
return totalNode;
}
// ----------------
void pushdown(int u) {
if (Treap[u].rev) {
swap(lson(u), rson(u));
if (lson(u)) Treap[lson(u)].rev ^= 1;
if (rson(u)) Treap[rson(u)].rev ^= 1;
}
if (Treap[u].tag1) {
int t1 = Treap[u].tag1;
if (lson(u)) Treap[lson(u)].tag1 += t1, Treap[lson(u)].key += t1;
if (rson(u)) Treap[rson(u)].tag1 += t1, Treap[rson(u)].key += t1;
}
if (Treap[u].tag2) {
int t2 = Treap[u].tag2;
if (lson(u)) Treap[lson(u)].tag2 += t2, Treap[lson(u)].number += t2;
if (rson(u)) Treap[rson(u)].tag2 += t2, Treap[rson(u)].number += t2;
}
Treap[u].rev = Treap[u].tag1 = Treap[u].tag2 = 0;
}
// -------------
void add(int u, int &rt) {
if (!u) return ;
pushdown(u);
add(lson(u), rt);
add(rson(u), rt);
lson(u) = rson(u) = 0;
insert(u, rt);
}
void getAnswer(int u) {
if (!u) return ;
pushdown(u);
getAnswer(lson(u));
getAnswer(rson(u));
answer[Treap[u].id] = Treap[u].number;
}
// ------------
struct Node {
int q, c;
} shirts[maxn];
bool cmp(Node a, Node b) {
if (a.q != b.q) return a.q > b.q;
else return a.c < b.c;
}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> shirts[i].c >> shirts[i].q;
sort(shirts + 1, shirts + n + 1, cmp);
cin >> m;
for (int i = 1; i <= m; i++) {
int x;
cin >> x;
int u = newNode(x, i);
insert(u, root);
}
for (int i = 1; i <= n; i++) {
int c = shirts[i].c, q = shirts[i].q;
int left = 0, mid = 0, right = 0;
split(root, c - 1, left, mid);
split(mid, 2 * c - 1, mid, right);
if (right) {
Treap[right].key -= c;
Treap[right].number++;
Treap[right].tag1 -= c;
Treap[right].tag2++;
}
if (mid) {
Treap[mid].key -= c;
Treap[mid].number++;
Treap[mid].tag1 -= c;
Treap[mid].tag2++;
add(mid, left);
}
root = FHQ_Treap::merge(left, right);
}
getAnswer(root);
for (int i = 1; i <= m; i++) cout << answer[i] << " \n"[i == m];
}
P5338 [TJOI2019] 甲苯先生的滚榜
这里假设 \(k\) 是通过数,\(\text{punish}\) 是罚时,常数 \(\text{base} = 1.5 \times 10 ^ 6\)
考虑到 \(\sum \text{punish}_i \le 1.5 \times 10^6\),所以我们可以构建一个 1.5e6 进制数
由于 \(\text{punish}\) 相较于 \(k\) 来言对于排名的贡献是负的,所以取它的相反数
而 \(k \le 10^6\),所以整个数 \(\le 10^{13}\),要用 long long(这个沙子没开 long long 交了五六次
其他的,由于我不想重构代码,写一个第 \(k\) 小,所以去了一个相反数
struct it{
ll tot, punish;
} manba_out[maxn];
// ----
/*
insert
*/
void insert(ll value, int punish) {
int left = 0, right = 0;
ll key = -(value * base - punish);
split(root, key, left, right);
root = merge(merge(left, newNode(value, punish)), right);
}
/*
delete
*/
void erase(ll value, int punish) {
int left, mid, right;
ll val = -(1ll * value * base - punish);
split(root, val, left, right);
split(left, val - 1, left, mid);
if (mid) mid = merge(lson(mid), rson(mid));
root = merge(merge(left, mid), right);
}
/*
1
*/
void update(ui Ria, ui Rib) {
erase(manba_out[Ria].tot, manba_out[Ria].punish);
manba_out[Ria].tot++;
manba_out[Ria].punish += Rib;
insert(manba_out[Ria].tot, manba_out[Ria].punish);
}
// --------
/*
get answer
*/
int getRank(ll value, int punish) {
ll key = -(1ll * value * base - punish);
int left = 0, right = 0;
split(root, key - 1, left, right);
int result = Treap[left].size;
root = merge(left, right);
return result;
}
// -------
/*
solve
*/
ui seed;
int m;
ui randNum( ui& seed , ui last , const ui m){
seed = seed * 17 + last ; return seed % m + 1;
}
ui last = 7;
void solve() {
Treap[1] = node{0, 0, 0, 0, 0, 0};
totalNode = 0;
root = 0;
cin >> m >> n >> seed;
for (int i = 1; i <= m; i++) manba_out[i].tot = manba_out[i].punish = 0;
ui mm = m;
for (int i = 1; i <= n; i++) {
ui Ria = randNum(seed, last, mm), Rib = randNum(seed, last, mm);
update(Ria, Rib);
last = getRank(manba_out[Ria].tot, manba_out[Ria].punish);
cout << last << "\n";
}
//
// inorder(root);
}
注意 insert 里面一定要用 key = -(value * base - punish),不要直接按照 value 分裂(这是有血泪教训的
复杂度分析
简要分析
考虑节点 x,y,假设 y 是 x 的祖先,则在 \([x, y]\) 中,y 的优先级是最小的
而优先级又是均匀取值的(这里这么假设
则“距离”(优先级)节点 x 的 节点 y ,\(\text{Pr}\{y = \operatorname{ancetor}(x)\} = \frac{1}{d}\)
则期望为:
严格分析
考虑分析树高的期望(算法复杂度由树高保证
但是如果采用均值:\(\mathbb E[h] \le \mathbb E[\text{left}] + \mathbb E[\text{right}]\),过于松了
考虑随机变量 \(\mathbb I_{i,j}\) 表示 \(n_i\) 是否是 \(n_j\) 的祖先
我们发现,若 \(n_i\) 是 \(n_j\) 的祖先,那么 在区间 \([\min(i,j),\max(i,j)]\) 中,\(n_i\) 的优先级最高
由于优先级均匀分布,\(\operatorname{Pr}\{\mathbb I_{i,j} = 1\} = \frac{1}{|j - i| + 1}\)
所以:
其实到这里我们就可将树高粗略的估计了:
现在,我们考虑更进一步,我们想要证明:
这里考虑从组合计数的角度证明
由于具体的分布并不会怎么影响最后的结果,所以这里假设选取某个数作为优先级的概率是均匀分布的
具体的,我们构建出来的树仅仅是由大小关系确定的
考虑具体的情况,我们容易知道 \(\text{depth} \ge t\) 等价于存在一个大小为 \(t\) 的祖先集合
而我们容易知道(其实在前面:
我们发现,若 \(n_i\) 是 \(n_j\) 的祖先,那么 在区间 \([\min(i,j),\max(i,j)]\) 中,\(n_i\) 的优先级最高
所以一个祖先链肯定满足一个严格的全序关系(单调递增,且每个数在它们的区间内是最大的
我们一共有 \(\binom{n}{t}\) 种选取方案,由于取值是均匀地,所以在所有的排列中,仅有一种是满足要求的
所以选到正确的选择的概率是 \(\frac{1}{t!}\)
所以有:
考虑近似
利用上界(可由 Stirling 公式推出):
更进一步,Stirling 近似 \(t! \sim \sqrt{2\pi t}(t/e)^t\) 给出:
更紧的常用上界是:
所以这里我们使 \(t = c \log n\),并使 \(c\) 足够大,使得 \(\exist \epsilon \gt 0, \, \text{ s.t. } \left(\frac{en}{c^2\log^2n} \right) \le n^{-\epsilon}\)
所以就有:
教材上的断言是 \(\operatorname{Pr}\{\text{depth} \ge c \log n \} \le n^{-2}\)
有了这个,我们就可以发现,树高是以高概率保证 \(\mathcal O(\log n)\) 的

浙公网安备 33010602011771号