介绍

可并堆是一种特殊的最小/大堆,使得两个堆之间能用 $O(\log n)$ 的时间内进行合并。由于其形态,也叫左偏树。

定义

  1. 距离:一个节点 $x$ 的距离 dis[x] 定义为:$x$ 与其子树中最近的一个children不满(指可以插入)的叶子节点的距离。

性质

  1. 左偏树是一个最小堆,每个节点有 $0-2$ 个child,并且保证节点的值 $\leq$ 其parent。
  2. 对于每一个节点 $x$,其子树均是左偏的,定义 $lc, rc$ 分别为 $x$ 的左右child,那么有 $dis_{lc} \geq dis_{rc}$。
  3. $dis_x = dis_{rc} + 1$。
  4. 如果一个根节点的距离为 $n$,那么这个子树至少有 $2^{n+1}-1$ 个节点(因为 $dis$ 是离最近的一个未满节点的距离,意味着距离为 $n$ 之内的所有节点都满了)。由此也可以推出,拥有 $n$ 个节点的左偏树的根节点距离是 $O(\log n)$ 的。

操作

合并 merge$(x, y)$

合并两个分别以 $x,y$ 作为根节点的左偏树,返回合并之后的根节点。

  1. WLOG 假设 $v_x \leq v_y$。
  2. 将 $y$ 与 $x$ 的右儿子 $rc$ 进行递归合并。
  3. 如果 $x, y$ 之间有任意一个为空节点(为 $0$),那么返回 $x+y$。

• 合并后,为了保持左偏特性,检查是否有 $dis_{lc} \geq dis_{rc}$,如果没有,swap(lc, rc),然后令 $dis_x = dis_{rc} + 1$。

复杂度:由于递归 $rc$,且 $dis_x = dis_{rc} + 1$,所以每次递归一层则 dis 减少 $1$。由于性质 $4$,最多递归 $O(\log n)$ 层。所以合并两个大小分别为 $n,m$ 的堆的复杂度为 $O(\log n + \log m)$。

插入节点 $x$

将 $x$ 看作一个包含一个元素的堆,然后用 merge 解决。

删除根节点 $x$

直接 merge(lc, rc)(合并 $x$ 的左右child节点)即可。

删除任意节点 $x$

merge(lc, rc) 后,自底向上更新 $dis$,当不满足左偏性质时交换左右儿子,当 $dis$ 无需更新时停止递归。

代码:TODO

例题

例1 洛谷P3377 【模板】左偏树/可并堆

题意

给定 $n$ 个最小堆,每个堆一开始只有一个数。有 $m$ 个操作,每个操作有两种:

$1 ~ x ~ y$:将第 $x$ 个数所在的堆和第 $y$ 个数所在的堆合并,如果第 $x$ 个数或者第 $y$ 个数已经被删除,或者这两个数已经在同一个堆内,无视此操作。

$2 ~ x$:删除第 $x$ 个数所在的堆的最小值,并且将这个最小值从堆中删除,如果已经删除则输出 $-1$ 并无视此操作。

其中,$n, m \leq 10^5$。

题解

本题需要与并查集结合,因为堆的最小值本质上是询问一个堆的根,所以每一个节点都要维护其 parent。

几个需要注意的点:

  1. 需要使用路径压缩。
  2. merge$(x,y)$ 后,由于返回的新根一定是 $x$,所以需要让 $lc, rc$ 的 par 都设为 $x$。
  3. del$(x)$ 时,先让左右子树断开(设 par 为它们自己),然后将它们 merge 起来。另外,由于路径压缩,尽管 $x$ 已经被删除了,但可能有其他节点的 par 指向了 $x$,所以 $x$ 的parent需要指向 merge 之后的节点。
代码
#include <bits/stdc++.h>
using namespace std;
struct MergeHeap {
    struct Node {
        int dis, val, par, lc, rc;
    } tr[maxn];
    int merge(int x, int y) {
        if (!x || !y) return x + y;
        if (tuple {tr[x].val, x} > tuple {tr[y].val, y}) swap(x, y);
        tr[x].rc = merge(tr[x].rc, y);
        int lc = tr[x].lc, rc = tr[x].rc;
        tr[lc].par = tr[rc].par = x;  // 注意 merge 过后,需要调整 par
        if (tr[lc].dis < tr[rc].dis) swap(tr[x].lc, tr[x].rc);
        tr[x].dis = tr[tr[x].rc].dis + 1;
        return x;
    }
    void del(int x) {
        tr[x].val = -1;  // 删除这个元素
        int lc = tr[x].lc, rc = tr[x].rc;
        tr[lc].par = lc, tr[rc].par = rc;  // 这里要先断开 (让lc和rc的par等于自己)
        tr[x].par = merge(lc, rc);  // 由于路径压缩,虽然 x 被删除了,但是其子树有可能有指向 $x$ 的
    }

    int finds(int x) {
        if (tr[x].par == x) return x;
        return tr[x].par = finds(tr[x].par);
    }
    void unions(int x, int y) {
        if (tr[x].val == -1 || tr[y].val == -1) return;  // 需要在 finds(x) 之前检查是否删除
        x = finds(x), y = finds(y);
        if (x == y) return;  // finds 之后再检查是否已经union在一起了
        merge(x, y);
    }
} tr;

int n, m;
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> tr.tr[i].val, tr.tr[i].par = i;  // 注意初始化 par
    while (m--) {
        int op; cin >> op;
        if (op == 1) {
            int x, y; cin >> x >> y;
            tr.unions(x, y);
        } else {
            int x; cin >> x;
            if (tr.tr[x].val == -1) cout << -1 << "\n";
            else {
                x = tr.finds(x);
                cout << tr.tr[x].val << "\n";
                tr.del(x);
            }
        }
    }
}

例2 洛谷P1456 Monkey King

题意

有 $n$ 个猴子,每个猴子有一个强壮值。

一开始每个猴子互不认识。有 $m$ 次决斗,每次决斗发生时,两个猴子所认识的所有猴子中最强的会参战,参战后其强壮值会减半。并且每次决斗后,两方的所有猴子都会互相认识了。

对于每次决斗,输出决斗后,它们认识中的所有猴子的最大的强壮值。

其中,$n, m \leq 10^5$。

题解

和上一题一样,只不过这次有一个 decrease-key 的操作,其实就相当于删除,再加入即可。

不过区别在于,这个删除并不是真的删除了,它只不过是value减半了而已,所以不需要调用之前的 del(x),直接再merge回去就行了。

代码
#include <bits/stdc++.h>
using namespace std;
struct MergeHeap {
    struct Node {
        int dis, val, par, lc, rc;
    } tr[maxn];
    int merge(int x, int y) {
        if (!x || !y) return x + y;
        if (tuple {tr[x].val, x} < tuple {tr[y].val, y}) swap(x, y);
        tr[x].rc = merge(tr[x].rc, y);
        int lc = tr[x].lc, rc = tr[x].rc;
        tr[lc].par = tr[rc].par = x;  // 调整 lc, rc 的parent
        if (tr[lc].dis < tr[rc].dis) swap(tr[x].lc, tr[x].rc);
        tr[x].dis = tr[tr[x].rc].dis + 1;
        return x;
    }
    int finds(int x) {
        if (tr[x].par == x) return x;
        return tr[x].par = finds(tr[x].par);
    }
    int decrease(int x) {
        tr[x].val /= 2;
        int lc = tr[x].lc, rc = tr[x].rc;
        int rt = merge(lc, rc);
        tr[x] = {0, tr[x].val, 0, 0, 0};
        tr[x].par = tr[rt].par = merge(x, rt);
        return tr[rt].par;
    }
} tr;

int n, m;
int main() {
    while (cin >> n) {
        for (int i = 1; i <= n; i++) {
            tr.tr[i] = {0, 0, 0, 0, 0};
            cin >> tr.tr[i].val, tr.tr[i].par = i;
        }
        cin >> m;
        while (m--) {
            int x, y; cin >> x >> y;
            x = tr.finds(x), y = tr.finds(y);
            if (x == y) cout << -1 << "\n";
            else {
                x = tr.decrease(x); y = tr.decrease(y);
                tr.merge(x, y);
                cout << tr.tr[tr.finds(x)].val << "\n";
            }
        }
    }
}

例3 洛谷P1552 [APIO2012] 派遣

题意

给定 $n$ 个人,这 $n$ 个人形成了一棵树,每个人有一个工资 $c_i$,以及领导力 $L_i$。

现在总预算有 $m$,我们需要选择一个人 $x$ 作为领导,并且从它的子树内选中一些人,使得这些人的工资总和 $\leq m$,需要最大化 $L_x * sz$,其中 $sz$ 是被选中的人的数量。

求出这个最大值。

其中,$n \leq 10^5, m, c_i, L_i \in [1,10^9]$。

题解

不难发现,枚举每一个可能的领导,然后保留他手下所有工资最低的人即可。

所以对于每棵子树,维护一个最大堆,当堆内的工资和 $> m$ 时,就pop掉。因为被pop掉后一定不会再被考虑进去,所以这样做是对的。

用可并堆,将 $x$ 与它的所有子树的堆合并在一起,递归解决即可。

• 对于本题,删除一个节点后,unions(u,v) 不能直接跳过,因为一个子树被删除的节点有可能是这个子树的根,所以还是需要先 finds(u),然后再merge。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
const int maxm = 2e7+500;

vector<int> adj[maxn];
ll cost[maxn];  // 节点本身的 cost
struct MergeHeap {
    struct Node {
        int dis, val, par, lc, rc, sz;
        ll cost, L;  // cost 的和
    } tr[maxn];
    int merge(int x, int y) {
        if (!x || !y) return x + y;
        if (cost[x] < cost[y] || (cost[x] == cost[y] && x < y)) swap(x, y);  // 这里不要比较错了
        tr[x].rc = merge(tr[x].rc, y);
        int lc = tr[x].lc, rc = tr[x].rc;
        if (tr[lc].dis < tr[rc].dis) swap(tr[x].lc, tr[x].rc);

        tr[lc].par = tr[rc].par = x;  // 注意 merge 过后,需要调整 par
        tr[x].sz = tr[lc].sz + tr[rc].sz + 1;
        tr[x].cost = tr[lc].cost + tr[rc].cost + cost[x];
        tr[x].dis = tr[tr[x].rc].dis + 1;
        return x;
    }
    void del(int x) {
        tr[x].val = -1;  // 删除这个元素
        int lc = tr[x].lc, rc = tr[x].rc;
        tr[lc].par = lc, tr[rc].par = rc;  // 这里要先断开 (让lc和rc的par等于自己)
        tr[x].par = merge(lc, rc);  // 由于路径压缩,虽然 x 被删除了,但是其子树有可能有指向 $x$ 的
    }

    int finds(int x) {
        if (tr[x].par == x) return x;
        return tr[x].par = finds(tr[x].par);
    }
    void unions(int x, int y) {
        // if (tr[x].val == -1 || tr[y].val == -1) return;  // 需要在 finds(x) 之前检查是否删除
        x = finds(x), y = finds(y);
        if (x == y) return;  // finds 之后再检查是否已经union在一起了
        merge(x, y);
    }
} tr;

int n, m;
ll ans = 0;
void dfs(int u, int p) {
    for (int v : adj[u]) {
        if (v == p) continue;
        dfs(v, u);
        tr.unions(u, v);
    }
    int rt = tr.finds(u);
    while (tr.tr[rt].cost > m) {
        tr.del(rt);
        rt = tr.finds(u);
    }
    rt = tr.finds(u);
    ans = max(ans, tr.tr[rt].sz * tr.tr[u].L);
}
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int p; cin >> p;
        adj[p].push_back(i);
        adj[i].push_back(p);
        cin >> cost[i] >> tr.tr[i].L;
        tr.tr[i].par = i;  // 注意初始化 par
        tr.tr[i].sz = 1;
        tr.tr[i].cost = cost[i];
    } 
    dfs(1, 0);
    cout << ans << endl;
}

例4 洛谷P3273 [SCOI2011] 棘手的操作

题意

有 $N$ 个节点,标号从 $1$ 到 $N$,这 $N$ 个节点一开始相互不连通。第 $i$ 个节点的初始权值为 $a_i$,有 $m$ 个如下操作:

$U ~x ~y$: 加一条边,连接第 $x$ 个节点和第 $y$ 个节点

$A_1 ~x ~v$: 将第 $x$ 个节点的权值增加 $v$

$A_2 ~x ~v$: 将第 $x$ 个节点所在的连通块的所有节点的权值都增加 $v$

$A_3 ~v$: 将所有节点的权值都增加 $v$

$F_1 ~x$: 输出第 $x$ 个节点当前的权值

$F_2 ~x$: 输出第 $x$ 个节点所在的连通块中,权值最大的节点的权值

$F_3$: 输出所有节点中,权值最大的节点的权值

其中,$n, m \leq 3\times 10^5, v, a_i \in [-1000,1000]$。

题解

虽然可并堆可做,但做起来太阴间了,所以我们利用离线,然后用线段树来做。


怎么离线呢?

注意到我们唯一的连接操作是链接两个联通块。那么易知存在一种映射方式,将 1-n 映射到另外一个 1-n 的permutation,使得每次链接的两个联通块一定是两个相邻的区间,这也意味着在任意时刻,每个联通块都只由一个区间组成。

如何映射?

我们先把所有的 U x y 操作都拿出来,用并查集模拟这个链接过程,用链表来维护。

在初始情况下,每个节点都是一个链表,在并查集链接时,将两个链表接在一起,这样就保证了每次链接后,每个联通块都是一个链表。在全部链接结束后,遍历所有的链表,按照顺序给它们编号即可,如 1 -> 4 -> 2 -> 36 -> 5 这两个链表,我们就可以映射成 1 -> 2 -> 3 -> 45 -> 6

在重新映射后,所有的询问和修改操作都变成了对一个区间进行,当然我们需要快速的找出第 $x$ 个节点所在的区间,这个在并查集的时候维护一个 L, R 即可。

时间复杂度就是 $O(n \log n)$ 了。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e5+5;
const int maxm = 2e5+500;

struct UnionFind {
    int n, par[maxn], L[maxn], R[maxn];
    UnionFind(int n) : n(n) {
        for (int i = 1; i <= n; i++) par[i] = i, L[i] = R[i] = i;
    }
    int finds(int u) {
        if (u == par[u]) return u;
        return par[u] = finds(par[u]);
    }
    void unions(int u, int v) {
        u = finds(u), v = finds(v);
        if (u == v) return;
        par[v] = u;
        L[u] = min(L[u], L[v]);
        R[u] = max(R[u], R[v]);
    }
};

struct LinkedList {
    int pre, nxt;
} b[maxn];

int n, a[maxn], Q;
struct Query {
    char type[2];
    int x, v;
} queries[maxn];

int ori_to_time[maxn];
void preprocess() {
    UnionFind uf = UnionFind(n);
    for (int i = 1; i <= n; i++) uf.L[i] = uf.R[i] = i, b[i].pre = b[i].nxt = i;
    for (int i = 1; i <= Q; i++) {
        int u = queries[i].x, v = queries[i].v;
        if (queries[i].type[0] != 'U') continue;
        u = uf.finds(u), v = uf.finds(v);
        if (u == v) continue;
        int utail = uf.R[u], vhead = uf.L[v];
        b[utail].nxt = vhead;
        b[vhead].pre = utail;
        uf.par[v] = u;
        uf.R[u] = uf.R[v];
    }
    int id = 0;
    for (int i = 1; i <= n; i++) {
        if (b[i].pre == i) {
            int p = i;
            while (b[p].nxt != p) {
                ori_to_time[p] = ++id;
                p = b[p].nxt;
            }
            ori_to_time[p] = ++id;
        }
    }
}

struct SegmentTree {
    struct Node {
        int mx, tag;
    };
    Node tr[maxn<<2];
    void push_up(int cur) {
        tr[cur].mx = max(tr[cur<<1].mx, tr[cur<<1|1].mx);
    }
    void push_down(int cur, int l, int r) {
        if (!tr[cur].tag) return;
        ll tag = tr[cur].tag;
        tr[cur].tag = 0;
        int lc = cur<<1, rc = lc + 1;
        tr[lc].mx += tag;
        tr[rc].mx += tag;
        tr[lc].tag += tag;
        tr[rc].tag += tag;
    }
    void update(int cur, int l, int r, int L, int R, ll x) {
        if (L <= l && R >= r) {
            tr[cur].tag += x;
            tr[cur].mx += x;
            return;
        }
        push_down(cur, l, r);
        int mid = (l+r) >> 1;
        if (L <= mid) update(cur<<1, l, mid, L, R, x);
        if (R > mid) update(cur<<1|1, mid+1, r, L, R, x);
        push_up(cur);
    }
    ll query(int cur, int l, int r, int L, int R) {
        if (l >= L && r <= R) return tr[cur].mx;
        push_down(cur, l, r);
        int mid = (l+r) >> 1;
        ll res = -1e18;
        if (L <= mid) res = max(res, query(cur<<1, l, mid, L, R));
        if (R > mid) res = max(res, query(cur<<1|1, mid+1, r, L, R));
        push_up(cur);
        return res;
    }
    void build(int cur, int l, int r) {
        if (l == r) {
            tr[cur].mx = a[l];
            return;
        }
        int mid = (l+r) >> 1;
        build(cur<<1, l, mid);
        build(cur<<1|1, mid+1, r);
        push_up(cur);
    }
} tr;


int FLAG = 0;
ll MX = -1e18;
int ttmp[maxn];
int main() {
    fastio;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    cin >> Q;

    for (int i = 1; i <= Q; i++) {
        string tmp; cin >> tmp;
        queries[i].type[0] = tmp[0];
        if (tmp.size() > 1) queries[i].type[1] = tmp[1];
        if (queries[i].type[0] == 'U') {
            int x, y; cin >> x >> y;
            queries[i].x = x, queries[i].v = y;
        } else {
            if (queries[i].type[0] == 'A' && queries[i].type[1] <= '2') cin >> queries[i].x >> queries[i].v;
            if (queries[i].type[0] == 'A' && queries[i].type[1] == '3') cin >> queries[i].v;
            if (queries[i].type[0] == 'F' && queries[i].type[1] <= '2') cin >> queries[i].x;
        }
    }
    preprocess();

    for (int i = 1; i <= n; i++) ttmp[i] = a[i];
    for (int i = 1; i <= n; i++) a[ori_to_time[i]] = ttmp[i];
    UnionFind uf = UnionFind(n);
    tr.build(1, 1, n);
    MX = tr.query(1, 1, n, 1, n);

    for (int i = 1; i <= Q; i++) {
        char* type = queries[i].type;
        int x = queries[i].x, v = queries[i].v;
        x = ori_to_time[x];
        if (type[0] == 'U') {
            uf.unions(x, ori_to_time[v]);
        } 
        if (type[0] == 'A' && type[1] == '1') {
            tr.update(1, 1, n, x, x, v);
            MX = tr.query(1, 1, n, 1, n);
        }
        if (type[0] == 'A' && type[1] == '2') {
            x = uf.finds(x);
            int L = uf.L[x], R = uf.R[x];
            tr.update(1, 1, n, L, R, v);
            MX = tr.query(1, 1, n, 1, n);
        }
        if (type[0] == 'A' && type[1] == '3') {
            FLAG += v;
        }
        if (type[0] == 'F' && type[1] == '1') {
            cout << tr.query(1, 1, n, x, x) + FLAG << "\n";
        }
        if (type[0] == 'F' && type[1] == '2') {
            x = uf.finds(x);
            int L = uf.L[x], R = uf.R[x];
            cout << tr.query(1, 1, n, L, R) + FLAG << "\n";
        }
        if (type[0] == 'F' && type[1] == '3') {
            cout << MX + FLAG << "\n";
        }
    }
}