关于可持久化 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 进行定期重构就对了。

posted @ 2026-08-19 19:09  zyb_txdy  阅读(10)  评论(0)    收藏  举报