关于可持久化 treap 的区间复制操作
考虑区间复制操作:对维护序列(如果是权值就必定是有交合并了)的可持久化平衡树,将序列的一个区间复制,并在未来的版本中,令这个区间参与合并 / 分裂 / 查询等操作。
从 https://flamire.blog.uoj.ac/blog/9767 中可以得知,对可持久化 treap 做区间复制是错的,不过我们不太能获得其他的信息。
询问 Chatgpt 5.6 Sol,我们就成功得到了对这件事情的更详细的说明!
考虑 treap 的复杂度为什么是对的。这依赖于 treap 的随机键值。随机键值间相互独立,因此每棵子树的树根都将在子树中均匀地随机选取。
因此,如果在复制节点的过程中,同时将键值复制,则未来若出现了合并原节点和复制的节点的时,因为其键值完全相同,则优先级完全固定,于是爆了,树高可以被轻松卡到 \(O(n)\)。为了解决这个问题,一种比较流行的写法是,不显式地记录随机键值,而是通过在合并时,让随机数对子树大小取模的方式得到与随机键值等价的值来比较,合并部分如下:
int merge(int x, int y) {
if (!x || !y) {
return x | y;
} else if ((int)(rnd() % (a[x].sz + a[y].sz)) < a[x].sz) {
int p = copy(x);
a[p].r = merge(a[p].r, y);
push_up(p);
return p;
} else {
int p = copy(y);
a[p].l = merge(x, a[p].l);
push_up(p);
return p;
}
}
然而,这种看似正确的做法仍然不能避免问题:一般的 treap 的正确性依赖于其随机独立性。而在区间复制的过程中,区间内的子树形态信息同样会被复制,两棵相同的子树的形态也完全相同。因此它并不是随机独立的。
e.g. 考虑若平衡树内有两个节点 \(1\),\(2\),且节点 \(1\) 为节点 \(2\) 的父亲。此时,复制节点 \(2\) 并令新的节点为节点 \(3\),并合并 \(\{1, 2\}\) 与 \(\{3\}\) 这两棵子树,则在新树中,三个节点为根的概率分别为 \(frac{2}{3}\),\(0\) 和 \(\frac{1}{3}\),而对于一般的 treap,三个节点为根的概率都应当是 \(\frac{1}{3}\),于是全错了。 注意这个例子并不能说明其错误性(显然普通 treap 也会遇到这个例子),考虑到 \(1\) 是 \(2\) 的父亲的那个 \(\frac{1}{2}\) 的概率,三个节点为根的概率就又相同了。本质原因可能是,因为 \(2\) 这个单个节点并不含有树形态的信息?而下面的 hack 则是,将单个节点扩展为了一个更大的区间?
进一步地,考虑不断进行,对当前大小为 \(n\) 的子树,复制 \(\{1, 2\}\) 和 \(\{2, \dots, n\}\) 这两个区间并合并的操作。可以证明,最终取出 \(\{2, \dots, n\}\) 构成的子树,其根结点的位置是 \(n - O(\log n)\) 的,因此这棵树是严重左偏的,在对 \(n\) 不超过 \(10^5\) 的实际测试中,树高会以不低的概率取到 \(O(\sqrt n)\) 量级。关于是否真的是 \(O(\sqrt n)\) / 能否证明不会将树高卡得太高?我不知道。详细内容
upd:发现有一种随机化的奇技淫巧:在 \(\text{merge}(l, r)\) 的时候,先把 \(l\) 和 \(r\) 分别随机拆开,再合并到一起。这样操作后的 fhq 就很难被卡掉了!这个做法来自 人类,树哥太牛了/bx。
CF2242F
来尝试 hack 一下这道题的朴素可持久化平衡树 做法!
考虑这道题的可持久化平衡树在做什么:维护下标为 \(0\) 到 \(2n\) 的 dp 数组,每次操作进行 \([w, 2w - 1] \rightarrow [0, w - 1]\) 和 \([0, 2n - w] \rightarrow [w, 2n]\) 的区间复制与合并。因为操作的形式是固定的,因此我们就不能再套用上面的那种 hack 了!不过询问 GPT 后,我们同样可以得到卡树高的数据,它甚至比前文的构造方式更有劲!
generator:
#include <array>
#include <cassert>
#include <iostream>
#include <string>
#include <vector>
// Generates data.in for the exact program in the question.
// The target executes a[n], a[n-1], ..., a[1], so the desired
// chronological update sequence u[t] is stored as a[n-t].
int main(int argc, char **argv) {
freopen("data.in", "w", stdout);
int n = 100000;
if (argc >= 2) n = std::stoi(argv[1]);
assert(8 <= n && n <= 100000 && n % 8 == 0);
const int b = n / 4;
const std::array<int, 4> q = {3 * b + 1, 2 * b + 1, b + 1, 1};
std::vector<int> a(n + 1);
for (int t = 0; t < n; ++t) {
// Halfway through, rotate the four-term cycle by one position.
const int phase = (t >= n / 2 ? 1 : 0);
const int u = q[(t + phase) & 3];
assert(1 <= u && u <= n);
a[n - t] = u;
}
std::cout << n << '\n';
for (int i = 1; i <= n; ++i) {
std::cout << a[i] << (i == n ? '\n' : ' ');
}
}
当然,这大概并不意味着这题完全不能可持久化 treap 了,考虑这题中你只会用到前一个版本的数,因此对 treap 进行定期重构就对了。

浙公网安备 33010602011771号